Firmware

FM1VST

Loud90/100

VST for Felucca base FM-1 Firmware

FM1VST is custom firmware for the M-VAVE FM-1, by @Quixotic7. Open source under GPL-3.0, latest release v0.6.2 (October 2026), 9 stars on GitHub.

forM-VAVE FM-1·by@Quixotic7

Unclaimed. Is this yours? Sign in with GitHub as @Quixotic7 to claim it.

Downloads
0
Stars
9
Forks
3
Commits
119
Last commit
Download v0.6.2

FM1VST-0.6.2-macOS-arm64.pkg· 42 MB· +2 files

GPL-3.0· Quixotic7/FM1VST·downloads come from the creator’s GitHub releases

101
Commits
8
Releases
101 commits and 8 releases in the last year
Activity · last 12 months

FM1VST

The FM-1 emulator from ChoralRoot FM-1 as a plugin, planned as VST3, AU and Standalone. The emulator compiles the firmware's own C source against a host HAL, so the plugin plays exactly what the device plays. The plan, the architecture and the phases are in FM1-VST-PLAN.md.

Status: phase 4. Three firmwares build as loadable core modules behind a versioned C ABI: ChoralRoot (build/cores/choralroot.fm1core), Felucca (felucca.fm1core) and Melodee (melodee.fm1core), each from its unmodified source; the plugin's firmware dropdown switches between them in place. A JUCE-free engine loads a core, runs its device clock and resamples its output to the host rate, and a JUCE plugin (AU, VST3, Standalone) plays it with the eight physical knobs as host parameters that follow the firmware's screen (the Roto-Control map, plan 4.5; the firmware's own parameters by name as an opt-in) and the panel's 41 buttons and keys as host parameters, MIDI in and out, the flash image as its state, and presets per firmware. The editor is the FM-1 panel itself (phase 3): LEDs, LCD, mouse and keyboard as in the emulator, with colour themes.

Layout

pathwhat
core-api/fm1core.hthe core ABI (FM1CORE_ABI 5): the descriptor fm1core_t (id, name, version, source, family, licence, gated, flash size, button labels, the parameter map fm1param_t, param_epoch and knob_target, the panel fm1panel_t, every entry point, shutdown, halted, and the flash image: flash, flash_dirty, flash_stage to boot from bytes, flash_sync) and the one symbol a module exports, fm1core_get. ABI 5 includes no firmware header: fm1panel_t is FM1VST's own (keys, buttons, taps, encoders, MASTER, key velocity and aftertouch in; LED brightness per (column, line), PLAY green and the LCD out), each glue translates its firmware's HAL to and from it. The threading contract is in its header comment.
core-glue/common/what every glue shares (its README documents each and how a core is added): the NOR image and the staged boot, exit / atexit inside a plugin, the audio FIFO, the tick units, a coroutine, the panel translation helpers, the version defines
cmake/FM1Cores.cmakefm1_add_core(); each core's call is in core-glue/<family>/core.cmake, found by a glob
core-glue/choralroot/the ChoralRoot core: core_choralroot.c (the submodule's tools/emu/emu_fw.c, unmodified, with exit / atexit redirected so a firmware reboot halts the core instead of the host), core_choralroot_params.c (the Tier 2 parameter map and the knobs' targets, included into the same unit so it reaches the firmware's UI code) and core_choralroot_desc.c (the descriptor)
core-glue/felucca/, core-glue/melodee/the Felucca and Melodee cores, the same three files (core_<id>.c, core_<id>_params.c, core_<id>_desc.c) plus, because these firmwares have no emulator of their own, its equivalent written here: <id>_fw.c (the emu_fw_* hooks, as ChoralRoot's emu_fw.c), <id>_firmware.h (the include list, as emu_firmware.h) and <id>_hal.h (the HAL on the host, as emu_hal_fw.h)
cores/the firmwares, git submodules, read-only, pinned (below)
remote/RemoteFM1RemoteFM1 (submodule, pinned at v0.2.2 (59a5b52), read-only): the protocol and libremotefm1, the host library behind Remote FM-1 (below)
plugin/the JUCE plugin: Processor.{h,cpp} (FM1Processor: the core and its Device on the audio thread, parameters and the Tier 2 feedback loop, MIDI, state, presets, the firmware switch; the threading and file layout are in its header comment), Tier2Parameter.{h,cpp} (a rebindable host parameter slot, and KnobParameter: a physical knob that follows the firmware's screen), Editor.{h,cpp} (the device and screen views, the corner button), Sheet.{h,cpp} (the settings sheet: firmware browser, presets, Remote FM-1, playing, host control, themes, view, device, about), Widgets.{h,cpp} and Look.{h,cpp} (the sheet's controls; the palette, the type and the LookAndFeel), StandaloneHooks.{h,cpp} (the Standalone app's audio / MIDI settings in the sheet), fonts/ (Inter Tight, OFL 1.1, and its build step), PanelComponent.{h,cpp} (the FM-1 panel: the emulator's drawing, layout, mouse and key map), Theme.{h,cpp} (colour themes: the derived palette, the presets, custom theme files) and RemoteLink.{h,cpp} (Remote FM-1: the session thread, the unit lock, the event queue and the screen snapshot)
engine/C++17, no JUCE: cores.{h,cpp} (find and load modules, one private copy per instance), device.{h,cpp} (the single-thread device clock, input, LEDs, LCD, render at any host rate), resampler.h (4-point Lagrange)
tests/core_smoke, param_test and core_conformance (per core), common/ (the glue helpers), replay_test, double_load_test, halt_test, plugin_test, tier2_test, panel_render_test, remote_test, and script_runner (the emulator's script grammar on a Device)

Cores

A core is the firmware compiled as a module (*.fm1core, a Mach-O bundle exporting only fm1core_get). The plugin looks for them in its bundle (<bundle>/Contents/Resources/cores/; the build puts build/cores/*.fm1core there, and a test or a dev build falls back to build/cores/) and in the user folder ~/Library/Application Support/fm1emu/cores/ (created when first scanned): dropping a module there installs it (a user module with the same id as a bundled one replaces it). Because the firmware's state is C globals, every loaded instance runs its own copy of the module, copied to ~/Library/Caches/fm1emu/instances/<uuid>/ and deleted when the instance goes away.

The bundled cores, each built from its firmware's own, unmodified source (a pinned submodule under cores/):

corewhatsource (submodule, pin)licence
choralrootChoralRoot FM-1: a chord instrument (strum, arp, pattern, harp, a bass part, a looper), forked from Felucca; its own emulator (tools/emu) is the corecores/ChoralRootFM1, https://github.com/Quixotic7/ChoralRootFM1 at 7d97982a7db3dad2da456a3e4d4478548b6571d9 (tag v0.26.1)GPL-3.0-only, Quixotic7 (on Felucca and Melodee: their lines below)
feluccaFelucca 1.0.1: the multi-engine synth (13 engines: ANALOG, PHASE, LOFI, SAMPLE, VOICE, TRIO, WHEEL, GRAIN, PHYS, DRUM, NOISE, FM6, SLICE; the CC0 samples), four parts, a 64-step sequencer, the song chaincores/Felucca, https://github.com/hugelton/Felucca at 20c275e39f75fa820978032efaceddfc5283c8cb (tag v1.0.1)GPL-3.0-only, Copyright (C) 2026 Leo Kuroshita (@kurogedelic), Hügelton Instruments
tonitorTonitor 0.55: acousmatic / live-ambient firmware (six tracks of generated sound objects on seven engines, free loops, a granulator, a reverb and a delay, a modulation matrix per track), built for low vision. Its own glue (core-glue/tonitor): its main loop on a coroutine, its second core's jobs inlinecores/tonitor, https://github.com/nzimas/tonitor at a240bc182b55e17fadc004d17e0b64c9eb42185f (no tags: the commit of the Hub's package, 0.55)GPL-3.0-only, Nuno Zimas (on Felucca and SLOOP; msfa Apache-2.0, Rings MIT, LIRA-8's architecture BSD-3)
melodeeMelodee 1.4.5: Felucca's fork with the FM6 (Dexed's synthesis) and CZ-1 engines, pattern banks and USB audio (the sample engines removed)cores/melodee, https://github.com/keremimo/melodee at 398ada1e09d864c12f3b778a43e45c7c2a8031b8 (tag v1.4.5)GPL-3.0-only, Felucca's copyright plus modifications Copyright (C) 2026 Kerem Kilic (Ellic Studio)
pigeonPIGEON 0.4.1: a performance sampler on Felucca 1.5.1 (two chopped songs, MP3 or IMA ADPCM, and a 16-pad kit; a 64-step sequencer, patterns, an arrangement); the firmware's own browser unit through the felucca_web glue, its MP3 decoder (libmad) built from its tree. No samples ship with it: load a bank with its sample loader or tools/ko_bank.py (below)cores/pigeon, https://github.com/TheShrimpBlimp/pigeon at 8f34e81b6d7b03fa45ffd7443b71f9ba838ef0f7 (tag v0.4.1)GPL-3.0-only, The Shrimp Blimp, on Felucca (Leo Kuroshita, Hügelton Instruments); libmad GPL-2.0-or-later; the splash logo by Dendy0097

The other cores and their pins are in CORES-PLAN.md section 1 (tools/release/cores.tsv: the public ones). Added 2026-10-22, all public: minitone (Mini-Tone 1.0.1, Stephane Schmitz: Felucca 1.5 with six tracks and SID, BRAIDS, TB303 ..), fmini (FMini-1 1.8.0, ruppert: a Minimoog-style monosynth), skiff (Skiff 1.4, illuminus5: a six-track groovebox) on the felucca_web glue, grimm (GR!MM 1.0, GRiMMBIENT: dark ambient on Dinghy) on the dinghy glue; and duell ("DUELL (unofficial build)", OSCILL/R) is un-gated. Mini-Tone's tree lacks the MIT / Apache-2.0 texts its LICENSING.md names: FM1VST supplies them (core-glue/felucca_web/licences/minitone/).

Felucca and Melodee have no emulator, only their host tests (tests/hostsim.c, the DSP; tests/ui_test.c, the UI against HAL stubs). Their cores are this repo's equivalent of ChoralRoot's emulator unit, written against the firmware's own power-on and main loop (firmware/src/main.c): the unit list follows felucca.c / melodee.c (hostsim's sound side, the HAL, audio.c, the UI, storage.c on a RAM image of the 1 MiB NOR, the user presets and projects; Melodee also its FM6 and CZ-1 stores), the 1 ms tick delivers the panel's edges and encoder steps as fm1_input_tick does, MIDI goes through usb.c's queues, a flash erase plays the device's ~45 ms of silence, the power-on splash lasts as on the device (430 ms), and the device's UBOOT entries (two failed boots, OCT- + OCT+ held 5 s, the USB UBOOT SysEx) halt the core. Built out, as the host tests build them: the update entry (OTA) and with it the web editor's SysEx, the serial console, USB audio (Felucca's UAC input, Melodee's "Melodee Out / In"), the TRS UART poll. On the device, Felucca's and Melodee's song (the parts' sounds and patterns) is RAM until SAVE > PROJECT, and so it is in the core: the plugin state keeps the flash (settings, projects, user presets), not unsaved edits.

PIGEON's sample bank. On the FM-1 the bank (500 KiB: a 4 KiB header, 496 KiB of audio) lives in the 1 MiB NOR, in the extents its package frees for it (hal/ko_layout.h), and the core keeps it there: it is part of the core's 1 MiB flash image, so it is saved with the plugin state and presets and survives a power cycle like every other byte. A fresh core has no bank (the release ships none) and is silent until one is loaded the way the device loads it: PIGEON's sample loader (https://theshrimpblimp.com/pigeon/webapp/pigeon/) or tools/ko_bank.py upload, over SysEx (the editor protocol's bank commands 90..97). Turn on Show to editors and pick the port "PIGEON (FM1VST)" in the loader.

Editors. Show to editors (the settings sheet) opens a virtual MIDI port named like the loaded core's USB port plus " (FM1VST)", so the firmware's own web manager finds the plugin as if it were an FM-1 (plugin/EditorPort.h). The name and the linked page come from the loaded core's id (EditorPort::usbProductName, managerUrl), never from the installer identity it answers: several cores share one (the Felucca family's FM-1_900), and the Hub matches a package by its SHA-256. What the public cores answer (tests/sysex_test.cpp checks each):

coreportlinked pageanswers
choralrootChoralRoot FM-1ChoralRoot's editorits editor, backup, identity FM-1_920
purplemonkeyChoralRoot FM-1identity FM-1_927
coralflowerCoralFlower FM-1backup, identity FM-1_930
felucca, ghoulbox, jiant, melodeeFelucca (JIANT FM, Melodee FM-1)each one's web editorthe web editor (INFO, backup, user presets), identity FM-1_900
duellDUELLFelucca's web editor protocol, identity FM-1_900
minitone, skiffFeluccaSkiff's web editorthe web editor, identity FM-1_910 / FM-1_714
fminiFM-1 MidiFMini-1's editorthe web editor, identity FM-1_918
pigeonPIGEONits sample loaderthe bank (upload, export, erase), backup, identity FM-1_900
loomLOOM-8 FM-1its samples pagethe sample bank, fm1-base's backup, identity FM-1_10000601
grimmFeluccaGR!MM's editorits editor (sounds, slots, settings, samples), identity FM-1_966
tonitorFM-1 Tonitorits backup pagesessions and presets (LIST, READ, WRITE), identity FM-1_700
amb, fomni, fimba, obervave, frodhesAMB-1 FM-1, FoMni FM-1, FiMba FM-1, Feluccathe installer's identity
speak37Speak37its installer pageidentity FM-1_510 only (no editor)

Adding a core. A firmware becomes a core with its glue folder core-glue/<family>/ and a core.cmake there (found by the top-level glob: no shared file is edited):

fm1_add_core(<id> ${CMAKE_CURRENT_LIST_DIR} SOURCE ${CMAKE_SOURCE_DIR}/cores/<repo> UNIT core_<id>.c
             DESC core_<id>_desc.c FAMILY <family> LICENSE GPL-3.0-only
             GEN <the headers its tools/build.py generate() writes>      # or NO_GEN
             VERSION_FILE firmware/src/<unit>.c VERSION_REGEX "^#define <NAME>_VERSION \"v?([^\"]*)\"")   # or VERSION x

which runs the submodule's generate step into its gitignored build/gen, builds the unit, the descriptor and core-glue/common's sources once (OBJECT) and links core_<id> (STATIC) and build/cores/<id>.fm1core (MODULE, exporting only fm1core_get), puts it into every plugin bundle (signed) and registers core_smoke_<id>, param_test_<id> and core_conformance_<id>. cmake/FM1Cores.cmake lists every option (NO_GEN, EXTRA_GEN, GEN_ROOT, PYTHON_NEEDS, VERSION, FLAGS, SUBMODULES_RECURSIVE, GATED, ..); -DFM1_PUBLIC_BUILD=ON leaves the GATED cores out. The glue files: the unit (core_<id>.c: the firmware with exit / atexit redirected, its HAL translated to the panel, the map included), the parameter map (core_<id>_params.c, optional: nparams 0 is valid), the descriptor (core_<id>_desc.c) and conformance.txt (the conformance test's script for this core). core-glue/common/README.md walks through it, with the pitfalls.

The plugin

FM1VST as AU (aumu Fm1v Qx7u, an instrument with MIDI out), VST3 (Instrument|Synth) and a Standalone app. The build installs the plugins (ad-hoc signed, with the cores inside) to ~/Library/Audio/Plug-Ins/VST3/FM1VST.vst3 and ~/Library/Audio/Plug-Ins/Components/FM1VST.component; the Standalone stays in build/FM1VST_artefacts/Standalone/.

In Live: add FM1VST (VST3 or AU) to a MIDI track and play notes 53..79 (F3..G5): they press the FM-1's 27 keys, so ChoralRoot plays its chords as on the device. The firmware needs under a second to power on after the plugin is loaded. The track's MIDI output carries what the firmware sends (ChoralRoot by default: the chord stream on channel 1; the bass on channel 2 once a bass sound is chosen), so it can drive other instruments.

  • Parameters (49): first the eight physical knobs (knob_master knob_select knob_presets knob_algo knob_1 .. knob_4, panel order: below), then the 41 panel controls: the 14 buttons (btn_fx .. btn_octup, named with the firmware's role and the printed label, e.g. "KEY (SEL)"; what their value does is the Button params setting, below) and the 27 keys (key_00 .. key_26, named by note, "F3" .. "G5"; a key is held when its parameter or a MIDI note holds it). Only the knobs ever report a change to the host: a press on the panel never moves a button or key parameter, and nothing else is a parameter, so Live's Configure mode collects exactly the knobs you turn. The firmware's own parameters by name (79 Tier 2 slots, below) are an opt-in: -DFM1_TIER2_SLOTS=79 (8 + 79 + 41 = 128, Live's limit for showing a plugin's parameters without configuring them).

  • Button params: Tap / Hold / Toggle-hold (the editor bar; stored per instance in the state). A Roto-Control button, like most controller buttons mapped in Live, is a toggle: it sends 1 and the value stays 1 until the next press. Read as "held", that is a button held forever: OCT+ shifts the octave only on its release (so it acts on every second press, like a toggle), both OCTs held is panic, and a layer button held past 300 ms locks its layer instead of doing its tap. So:

    • Tap (the default): every rising edge (the value from below 0.5 to 0.5 or more) is one tap on the device: the button pressed for 90 ms of device time, then released; the falling edge does nothing. A toggle button and a momentary button both give one tap per press. 90 ms is comfortably a tap: the firmware needs 9 ms of press (its debounce) and reads the buttons once per 15 ms UI frame; a layer locks only after its hold time (300 ms or more). A rising edge while a tap is still down (or in the 30 ms release after it) queues one more tap. The length is a hidden setting in the state (buttonTapMs, 20 .. 1000) in case a firmware wants more.
    • Hold: the value is the held state (the old behaviour): for momentary buttons, when you want long presses and layer locks from the controller.
    • Toggle-hold: each rising edge toggles held on / off, so a toggle button can hold a layer open.

    The panel's own mouse and computer keys are real presses and releases whatever the mode; the key parameters (key_NN) are always value = held.

  • Encoders: Absolute / Stepped and Steps (the editor bar; stored per instance in the state; default Absolute, 24 steps, 8..64). For a controller whose knobs click in steps, such as a Roto-Control knob set to 24 steps so it feels like the FM-1's own detented encoders. Each click of such a knob moves the Live parameter 1/24 of its travel, so in Absolute mode one click on Knob Select (Tempo, 20..300 BPM) jumps about 12 BPM, and on a short range (Voicing, a 4-way switch) several clicks do nothing and then one jumps. In Stepped mode a write to one of the seven encoder parameters (Knob Select, Knob Presets, Knob Algo, Knob 1..4) is not a value but clicks: its change in steps (whichever way the controller quantises, k/23 or k/24, and a skipped step counting 2) is sent to the device as that many detents of the real encoder, the firmware's own knob path, whatever the knob turns on the screen: Tempo moves 1 BPM per click, Voicing one step, an Options or editor menu one row, PRESETS one sound. The firmware's resulting value is pushed back (the motor goes to it, the value text reads "Function: value" as before) and the next click counts from there, so the knob never runs out of travel on a long range. A value the controller echoes back after a push is never a click. Where the knob has no value (a menu cursor) it springs back to the centre after 400 ms as before, and that is never a click either. Knob Master is the pot and stays absolute in both modes. Switching the mode turns nothing. To set it up on the Roto-Control: map the eight knobs as usual (Knob Master .. Knob 4 on the first page), set Knob Select, Knob Presets, Knob Algo and Knob 1..4 to 24 steps in the Roto-Control's knob settings (leave Knob Master smooth, not stepped), then pick Encoders: Stepped and 24 steps on the plugin's bar (set the plugin's Steps to whatever step count the controller uses).

  • The knobs (the Roto-Control's first page, plan 4.5). One host parameter per physical knob, in panel order, with names that never change (a Roto-Control binds by name): Knob Master, Knob Select, Knob Presets, Knob Algo, Knob 1, Knob 2, Knob 3, Knob 4. Knob Master is the pot (0..1023, default 724 as the emulator powers on; its value text "Master: 724"). The other seven are always whatever that knob does on the firmware's screen right now, and that goes into the value text: "Function: value", e.g. on ChoralRoot's view Knob 1 reads "Voicing: -1", Knob Select "Tempo: 147 BPM", Knob 3 "Strum Rate: 126 ms", Knob 4 "Chord Reverb: 33"; hold PERF and Knob 1..4 read the perform mode's row ("Strum Rate: .." .. "Strum Hold: Off"; pick Arpeggiate and they become Division, Dir, Gate, Swing); the FX layer gives the effect's row, the KEY layer Tonic / Scale / Transpose / Single Notes, the sound editor the cells of the lane on screen; on Felucca's HOME the engine's four knobs, on its ENV page Attack .. Release, FX held its macros. Each knob is a continuous 0..1 parameter over its current function's range (quantised inside to the firmware's steps), so nothing the host caches about it (name, steps, default) ever changes and the plugin never sends "parameter info changed" for a knob; when the screen changes (within a UI frame, whatever caused it) the new value is pushed without a gesture and a VST3 host is asked to re-read the values (restartComponent(kParamValuesChanged): Live ignores a value change outside a gesture), so the Roto-Control's motor moves to it and its display shows the new text. For 150 ms after a knob's function changed, host writes to it are dropped (they were meant for the old function: the motor had not moved yet) and the host is told the new value again. Who reports: a knob turned on the unit, the panel GUI or its keys reports on that knob, inside a gesture (a touch: Live's Configure collects it, automation records when armed); a knob whose function's value changed for another reason (a PRESETS turn loads a sound whose reverb send differs from the one Knob 4 shows; a MIDI CC) is updated without a gesture; a value the host already has is not sent again. Where a knob has no value on the current screen (SELECT scrolling Options or the editor's lanes, PRESETS and ALGORITHM browsing sounds, a page's empty column), it is a relative control (value text "turn"): a host change becomes detents at the device (the change of the 0..1 value x 24 per full travel, the knobDetents setting) and the parameter springs back to 0.5 after 400 ms (that re-centre turns nothing), so the knob still scrolls; turned on the unit, it is nudged off the centre by its detents (inside a gesture) and springs back the same way. The core says what each knob is (fm1core_t.knob_target, ABI 4); the per-screen tables are in the headers of core-glue/*/core_*_params.c. Entries that exist only as knob targets (the sound editor's pages, Options rows, FX Effect, Felucca's FX macros and T1..T4 levels, the cells no named entry covers) are FM1P_HIDDEN: they never get a Tier 2 slot of their own.

  • The firmware's parameters by name (Tier 2, plan 4.5; opt-in: -DFM1_TIER2_SLOTS=79, off by default). For direct mappings of a specific parameter whatever the screen shows. Each slot reports its own entry's changes, inside a gesture, so with the slots on Live's Configure also collects the sends and parameters a knob or a preset moves. Absolute values with the firmware's own ranges and value texts ("1/8", "120 ms", "Up-down", "-10.0dB"), read from and written to the running firmware, so a motorised controller such as the Melbourne Instruments Roto-Control follows them (the knobs above are the same values, bound to whatever is on screen). A host change goes through the firmware's own panel code (the screen, the knob row's hot cell, the settings record and the trace behave as for a knob turned on the unit); a change on the firmware's side (its knobs, a preset, a MIDI CC, a mode change) moves the host's value, as a touch (inside a change gesture). After every power-on (a preset load, a state restore, a flash reset) every value is reported once, without gestures, so the controller sweeps to the unit's real state. The values are not stored separately in the plugin state: the flash image is the truth. Slots beyond a core's visible map are "(unused)". They come after the knobs (t2_00 .. t2_78), eight a page:

    ChoralRoot (73 visible, 109 with the hidden knob targets):

    the slots, in order
    Voicing, Tempo, Transpose, Chord Level, then the perform parameters of every mode, the ones its engine uses: Strum (Rate, Dir, Range, Hold), Slop (Amount, Rate, Dir, Range, Hold), Arp (Division, Dir, Gate, Swing, Range, Retrig, Hold), Pattern (Type, Div, Gate, Swing, Range, Rotate, Retrig, Hold), Harp (Rate, Dir, Gate, Range, Hold)
    chord and global: Perform, Perform Mode, Latch, Key Mode, Key Tonic, Key Scale, Single Notes, Split Point, Play Style, Ext Addition, Secret Chords, Velocity, Bass, Bass Mode, Bass Register, Bass Level, Metronome, Click Level, Time Signature, Loop Length, Loop Quantize, Count-In, Loop Level
    FX: FX on, the chord part's sends (Chord Drive / Chorus / Delay / Reverb: the FX amounts), the bass part's sends, the shared buses (Reverb Size / Damp / Type, Chorus Rate / Depth, Delay Time / Feedback / Colour)

    Felucca (73 visible, 85 in all) and Melodee (the same layout): the selected part's parameters, as the device's pages show them, and the song's. "The selected part" follows the unit: pick another part (the ALGORITHM knob, or the Part parameter) and every part slot reads and writes that one. A host write is the knob's own write (the value clamped to the firmware's range, motion_capture: a live edit is the motion sequencer's new base, recorded while the part records; Melodee shares SCALE / QUANT across the parts as its knob does).

    the slots, in order
    Level, Drive, Delay Send, Reverb Send; ENV: Attack, Decay, Sustain, Release
    ENV DEST: Env>Filter, Env>Pitch, Env>Shape; Pan
    the engine (EDIT 1 / EDIT 2): E1..E8, named, ranged and texted as the selected part's engine has them ("E1 WAVE" SIN..S&H for ANALOG, "E1 ALG" 0..32 for FM6; the host is told when the engine or the part changes)
    LFO: Rate, Wave, Phase, Fade; LFO DEST: >Pitch, >Filter, >Shape, >Amp
    Chorus Send, Voice Mode, Glide, Glide Mode, Priority, Allocation, Detune, Mute
    ARP: Mode, Rate, Octaves, Gate, Swing, Chance, Hold, Order
    SCL: Scale Root, Scale, Quantize, Transpose; CHORD: Chord, Chord Voicing; Part; Tempo
    Swing, Tune; the FX buses: Delay Time / Feedback / Colour / Mix, Reverb Type / Size / Damp, Chorus Rate / Depth; PATTERN: Length, Div, Swing, Gate (not while a song plays, as the knob); MOD slots 1 and 2: Source, Dest, Amount

    No named slot (but the knobs reach them on their pages, through the hidden cells): the SLICER insert, MOD slots 3 and 4, the clock source and MIDI routing (GLO > SYSTEM), the operator pages, Melodee's FM6 patch pages (a copy of the patch, written back by fm6_page_put) and CZ-1 tone pages (the same through cz_ed_put), its BOOT / DRUM device settings. Not a value at all (relative on the knobs): the engine and preset choice (a sound load, with its undo and the engine's fade), the transport (PLAY / REC are buttons). Everything is part or song (the project, saved by SAVE > PROJECT on the unit), except Melodee's Tune, which it keeps as last used in its settings.

    param_test prints the whole map with ranges, flags (H: hidden) and the firmware path each entry is written through. -DFM1_EXPOSE_EDITOR=ON (off by default) gives ChoralRoot's sound editor entries (ENV, LFO, MOD and MIX for the chord and the bass part, 30) slots of their own (off they are hidden: the knobs reach them in the editor); build the plugin with -DFM1_TIER2_SLOTS=103 (or more) to fit them (that goes past Live's 128). A sound edit is kept only by SAVE on the unit.

  • MIDI in: with MIDI notes play keys on (default), note n on any channel holds key n - 53 + 12 x Transpose (Transpose: -2..+2 octaves; velocity 0 is a release). Every other message (CCs, program changes, clock, SysEx, and notes outside the keys or with the setting off) goes to the firmware's own MIDI in as USB-MIDI packets. A note that played a key is not also sent to the firmware (ChoralRoot's CHORD channel is 1, so it would sound twice); the toggle "... and go to the firmware's MIDI in" sends it as well.

  • MIDI out: the firmware's USB-MIDI packets, decoded (SysEx reassembled), at the position in the block where they were produced.

  • State: the firmware's id and version, the settings (transpose, the MIDI toggles, the theme, the editor size and big-LCD view) and the whole 1 MiB flash image, gzip-compressed (a few KB), so a Live set reopens with the unit exactly as it was saved.

  • Firmware dropdown: every core found in the two folders; switching saves the old one's flash, loads the new one and powers it on from its own flash.

  • Presets per firmware (the preset dropdown, Save / Save as / Rename / Delete / Reset flash / Export / Import, and the host's program list):

    ~/Library/Application Support/fm1emu/
      cores/                       user-installed *.fm1core
      <core-id>/flash.bin          the working flash: what a new instance of that firmware starts from (written when
                                   an instance is released or closed, before a firmware switch, and on Save)
      <core-id>/presets/*.fm1preset  a snapshot of the whole flash
      <core-id>/backups/*.fm1preset  written before a preset load or a flash reset (the newest 50 kept)
      themes/*.json                custom colour themes (one per file), settings.json: the default theme
    

    A .fm1preset is one line of JSON ({"fm1preset":1,"core":"choralroot","version":"0.1","name":"..","date":"..", "size":1048576}), a newline, then the flash image gzip-compressed (tail -n +2 X.fm1preset | gunzip > flash.bin).

  • FM1EMU_HOME: when set, it replaces ~/Library/Application Support (data under $FM1EMU_HOME/fm1emu/) and ~/Library/Caches ($FM1EMU_HOME/Caches/fm1emu/). The tests set it to a folder in the build.

The panel (phase 3)

The editor is the FM-1 itself: a port of the ChoralRoot emulator's window (tools/emu/emu.c, keymap.c) as a JUCE component (plugin/PanelComponent.{h,cpp}), edge to edge, with nothing around it. The host parameters have no generic list any more; they remain the host's.

  • Two views (saved with the set): Device, the whole panel (resizable, its aspect locked), and Screen, the 240 x 240 LCD alone in a thin bezel in a square window (resizable; nearest-neighbour at whole multiples of the screen's pixels, else scaled up nearest-neighbour and smoothed down, so it stays crisp; the computer keys still play the panel). Each view remembers its own size.

  • Settings are out of sight. A small round button fades in at the top-right corner while the mouse moves over the editor (on the body's empty corner: never over a control) and fades out 1.5 s after it stops. While Remote FM-1 is on, the button carries a small LED that stays when it fades (green: connected; amber: searching, in use or lost); a halted firmware keeps it shown, ringed in red. The button, or a right-click anywhere off the controls (on a key or button a right-click still latches it), opens the settings sheet from the right: FIRMWARE (a browser with search, category chips and a card per firmware), PRESET (‹ name ›, Save, Save as and Rename typed in place, Delete confirmed in place, Export, Import), REMOTE FM-1, PLAYING (Transpose, the two MIDI options), HOST CONTROL (Button parameters, Encoders, steps), LOOK (the themes as swatches, a custom theme), VIEW (Device / Screen, the big LCD), DEVICE (Restart, Reset flash, confirmed in place), ABOUT (the firmware's descriptor: version, licence, source, gating, credits); in the Standalone app also its audio / MIDI settings and state files (its own "Options" bar is hidden). Esc or a click beside the sheet closes it; while it is open the computer keys still play the panel.

  • Drawing. The panel in the designer geometry (904 x 566 units, the 876 x 538 view shown), drawn with the emulator's own rasteriser (anti-aliased rounded boxes, rings, round-capped pointers, its 5x7 font): the body, the key and button beds, the 27 keys with their LED bars, the 14 buttons with the firmware's labels on the caps (ChoralRoot: FX KEY BASS LATCH EDIT OPT / HOME SAVE PERF METRO LOOP REC) and the panel's printed labels small on the bed where they differ (SEL ENV LFO GLO over the top row, ARP SEQ PLAY under the bottom one), the OCT buttons, the 8 knobs with their names and pointers (a knob bound to a firmware value points at that value's place in its range over 270 degrees, as MASTER shows its 0..1023 position, and moves whenever the value does, from the host, a Roto-Control, the panel or the firmware; its function is a small caption under its name, e.g. KNOB1 / VOICING; a relative knob is endless: its pointer turns 15 degrees per detent the device is given, from the panel or the host), the computer-key hints in green, and the LEDs as the firmware sets them: lit / dim / off, REC red, PLAY orange plus its green LED. The panel is rasterised once per size and theme at the screen's physical pixels (sharp on Retina at any size); after that only the controls whose state changed (held, LED, pointer, selection) and the LCD are redrawn, polled at 60 Hz.

  • The LCD is the firmware's 240 x 240 RGB565 framebuffer on the panel's screen. The window is resizable with the panel's aspect kept and snaps to an integer multiple of the LCD when one is within 8 %: the LCD is then drawn nearest-neighbour, pixel for pixel (at other sizes it is smoothed). The default size puts the panel at 1.6 points per unit (1401 x 861 points: about 80 % of a 1080p screen's height; the LCD at 1x, 2x on Retina). Big LCD (the sheet's VIEW, or the backtick key) shows the LCD above the panel as a square as wide as the panel (snapped down to a multiple of 240 when one is within 10 %; the frame letterboxes in the window). The size and the big-LCD view are remembered per instance in the state.

  • Mouse: click a key or a button to press it (released on mouse up); right-click or ctrl-click latches it down until clicked again; the wheel over a knob turns it one detent per notch (MASTER: 16 of 1023); dragging a knob up / down turns it (8 points a detent); a click on a knob selects it for Up / Down (the green ring).

  • Keyboard (click the panel first; physical US-layout keys, keymap.c's map; the F keys may need fn):

    computer keysFM-1
    A S D F G H J K L ; ' ]white keys C4 D4 E4 F4 G4 A4 B4 C5 D5 E5 F5 G5
    W E T Y U O Pblack keys C#4 D#4 F#4 G#4 A#4 C#5 D#5 (F#5: the mouse only)
    F1 F2 F3 F4 / 2 3 4 5 / TabF#3 G#3 A#3 C#4 / F3 G3 A3 C4 / B3 (ChoralRoot's LOCK)
    Z X (also Esc / Return)OCT- / OCT+; End: both (panic)
    F5 .. F10, or C V B N M ,the top button row (FX SEL ENV LFO EDIT GLO; ChoralRoot: FX KEY BASS LATCH EDIT OPT)
    7 8 9 0 - =the bottom row (HOME SAVE ARP SEQ PLAY REC; ChoralRoot: HOME SAVE PERF METRO LOOP REC)
    Page Down / Page Upselect the next / previous of SELECT, KNOB1 .. KNOB4
    Up / Downturn the selected knob one detent (repeats; MASTER: 32 of 1023)
    Shift + Up / Downfine steps (the firmware's SHIFT: GLO held around the detent); outside the editor, OPT's second function
    `the big LCD view

    Inside a host the F keys often never reach the plugin (macOS's media keys without fn; Live: F1 .. F8 are its track activators), so the top button row has a fallback row, C V B N M , (not keymap.c's: the bottom letter row, left to right as the buttons); the panel prints both on those buttons ("F5/C" .. "F10/,"). A host may also keep Tab, Esc and Return for itself: OCT- / OCT+ are Z / X too. Click the panel once so it has the keyboard focus (the settings sheet passes the keys on to it while open).

    A held key is pressed once (the key repeat is ignored) and let go on its release; when a host swallows the release, the panel finds the key up anyway: it checks on every key event and 30 times a second while a key is held, by the physical key's own state (macOS CGEventSourceKeyState, the key learnt at its press) where it can, else by JUCE's. The emulator's screenshot, recording and dump keys are not mapped. Cmd shortcuts stay the host's. Losing the keyboard focus releases every key the keyboard or the mouse holds (latches stay); closing the editor releases everything.

  • How the panel reaches the firmware. The panel is its own "held" source, as the emulator merges its keyboard, mouse and latch sources: FM1Processor::panelKey / panelButton set atomics that the audio thread merges at each block start with the host parameters and MIDI (keys: key_NN | MIDI notes | panel; buttons: btn_* | panel). A panel press never moves the btn_* / key_* host parameters, so automation and the Roto-Control are unaffected, and a press released before the next block still reaches the firmware as a tap. panelEnc queues encoder detents for the audio thread; MASTER is the host's master parameter, which the panel moves as any plugin GUI moves a parameter (inside a gesture). Shift + Up / Down holds GLO around the detent on the device clock (emu.c fine_turn). The GUI never reads the core: the audio thread copies the LCD, the LEDs and the held state into a snapshot after each block in which they changed, under a lock it only tries.

Themes

The real FM-1 comes in several colours; the panel's colours are a theme (plugin/Theme.{h,cpp}): three picked colours, Base (the body), Membrane (the silicone keys and buttons) and Knobs, plus two optional overrides, Bed (the recessed plate the keys sit in; else a darker shade of the membrane) and Label (the printed labels; else white or dark, whichever contrasts more with the body). Everything else is derived so a theme stays legible: the edge lines from the base, the pressed cap (lightened), the LED-off slot (darkened) and the cap outlines from the membrane, the knob pointer by contrast with the knob, the text on caps and beds by contrast. The LEDs' lit colours (white, red, orange, green) are fixed. panel_render_test checks every preset against contrast floors (label / body 3.0, pointer / knob 2.0, LED-off / cap 1.3, pressed / cap 1.3).

  • Presets (themes/presets.json, embedded in the plugin): Black, Black/Green, Cool Gray, Orange, Purple, White/Blue (sampled from M-VAVE's product photos in reference/MVaveOfficialColors/) and Emulator (the ChoralRoot emulator's own palette).

  • Custom: the sheet's LOOK + opens colour pickers for Base, Membrane and Knobs, Bed and Label with an "override" switch each, and a name; every change previews live. Save writes ~/Library/Application Support/fm1emu/themes/<name>.json; Save as default also makes it the theme new instances start with (fm1emu/settings.json, "defaultTheme"); Delete removes the file. Custom themes are listed after the presets. A theme file has the keys of a presets entry (bed and label optional):

    {"name": "My Teal", "base": "#1F3B3D", "membrane": "#E07A3C", "knob": "#E8E2D6", "bed": "#B85F2C", "label": "#F4F0E8"}
    
  • In the state: the theme's name and colours are stored with the instance, so a set reopens looking the same even when its custom theme file is gone (the sheet then names it " (this set)").

Remote FM-1

A real M-VAVE FM-1 can be the panel of the firmware running inside the plugin: its keys, buttons, knobs and MASTER play the core, and the core's screen and LEDs show on the unit. It needs:

  • The unit on the RemoteFM1 firmware (RemoteFM1, installed with its fm1_install.py), on USB or (RemoteFM1 0.2+) Bluetooth MIDI, paired with the Mac as "FM-1 ...". Without a session it shows "REMOTE FM1 / WAITING FOR HOST".
  • The toggle: "Remote FM-1" in the editor's top bar. The status next to it says searching, connected (bulk | MIDI | Bluetooth, N fps), unit lost: searching, or in use by another instance. The toggle is saved with the plugin state, so a Live set reopens with it on. Off (or the instance deleted, the plugin unloaded, the host quitting) ends the session and the unit goes back to its waiting screen.
  • One driver at a time: a lock file (~/Library/Application Support/fm1emu/remote.lock) lets exactly one FM1VST on the Mac drive the unit; another instance, or the Standalone next to Live, shows in use and takes over when the owner lets go.
  • Live's MIDI: the unit's USB-MIDI port is also a MIDI input of the DAW, and its panel sends plain MIDI (notes 53..79 on channel 16, buttons as notes 0..13 on channel 15, CC 7 / 39 / 16..22 on channel 15). With "All Ins" on the track, the plugin would see each key twice; "Ignore the FM-1's own MIDI" (the ▾ menu next to the toggle, on by default) drops exactly those messages (and RemoteFM1's SysEx) while this instance drives the unit. Any other input plays as before. To keep the unit off the track entirely, pick another MIDI input than "All Ins".

Frames go over the unit's vendor bulk interface when it answers (libusb, linked statically: the bundles need no Homebrew library), else over SysEx; the panel events always come as plain MIDI. A unit on USB is used over USB even when it is also connected over Bluetooth. Over Bluetooth, libremotefm1 paces the frames for the radio (about 30 KB/s for RemoteFM1 0.2.2, 5.5 KB/s for older units), sends changed tiles in 4 colours first and their exact colours once the screen has been still for a second, and with 0.2.2 shows each screen change whole; the settings sheet counts these. While searching, the plugin sends RemoteFM1's HELLO (a SysEx with the non-commercial ID 7D) once a second, and only to what can be an FM-1: a USB-MIDI device whose USB IDs are 1209:0001 (RemoteFM1's, shared with ChoralRoot), or, for a device without the USB property, one named "FM-1" (plugin/RemoteMidi.c, plugin/RemoteCandidates.h). Other MIDI hardware (an audio interface, a controller, an FM-1 on the stock firmware) never gets a message, and with no candidate nothing is sent at all. libusb opens only 1209:0001 with a vendor-class interface. The physical knobs are device turns for the knob parameters (the host feedback and a Roto-Control's motor follow them as they follow a turn on the plugin's panel); MASTER moves the "Knob Master" parameter. The design (threads, the merge with the panel GUI, the parameters and MIDI) is in plugin/Processor.h and plugin/RemoteLink.h, and in FM1-VST-PLAN.md 4.10.

Building

The firmware sources come in as a git submodule, so clone recursively:

git clone --recursive https://github.com/Quixotic7/FM1VST.git
# or, in an existing clone: git submodule update --init --recursive

Then:

cmake -B build && cmake --build build && ctest --test-dir build --output-on-failure

The build installs the VST3 and the AU into ~/Library/Audio/Plug-Ins after each build. -DFM1_COPY_PLUGIN=OFF leaves them in the build folder (a second build tree next to a working install; plugin_test then checks the built bundles). Remote FM-1's vendor bulk transport links Homebrew's static libusb-1.0.a (brew install libusb); without it the plugin builds with SysEx frames only.

A build of some cores only: -DFM1_ONLY_CORES="choralroot;melodee" (a cache variable) includes only the core-glue/<folder> glue of the folders named, so a work tree needs just those cores' submodules checked out (and remote/RemoteFM1). A folder is a family: choralroot also builds PurpleMonkey and CoralFlower, felucca_web the Felucca-based cores, and each of them needs its submodule. Empty, the default, builds every core; set it back with -DFM1_ONLY_CORES="". A core left out has no tests (its core_smoke, core_conformance, ..), and hub_test and fm1emu_test are not registered without fm1emu.

A release build is the public build (-DFM1_PUBLIC_BUILD=ON: the 21 un-gated cores and the fm1emu engine, CORES-PLAN.md 6) for Apple silicon, made by .github/workflows/release.yml when a tag v<version> is pushed (the version is project()'s here; CHANGELOG.md has the notes) and by hand the same way:

cmake -B build-release -DCMAKE_BUILD_TYPE=Release -DFM1_PUBLIC_BUILD=ON -DFM1_COPY_PLUGIN=OFF \
      -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
      -DFM1_LIBUSB_PREFIX=$(tools/libusb_static.sh build-libusb 11.0)
cmake --build build-release -j && tools/release_tests.sh build-release && tools/package_release.sh build-release

tools/libusb_static.sh uses Homebrew's libusb when it is built for the deployment target, else builds libusb from its release tarball (a bottle built for macOS 15 would not load on macOS 11). tools/package_release.sh checks the bundles (exactly the public cores, arm64, no Homebrew paths, signatures) and writes build-release/dist/FM1VST-<version>-macOS-arm64.zip (the three bundles, INSTALL.txt, README.txt, LICENSE, licences/), FM1VST-<version>-macOS-arm64.pkg (the installer: one component package per bundle, the app into /Applications, the VST3 and the AU into /Library/Audio/Plug-Ins, not relocatable; its templates are in tools/release/pkg/; it is expanded and checked, never installed), SHA256SUMS and the release notes. The pkg is unsigned unless FM1_INSTALLER_IDENTITY names a Developer ID Installer identity (productsign).

The Tier 2 slots (the firmware's parameters by name) are off by default; cmake -B build -DFM1_TIER2_SLOTS=79 turns them on (FM1_TIER2_SLOTS is a cache variable: a build folder configured before the default became 0 keeps 79 until it is set again, -DFM1_TIER2_SLOTS=0).

The tests:

  • core_smoke_<id> (choralroot, felucca, melodee): the module, loaded as the plugin loads it, boots, draws, sounds when D4 is held (and not before), does not halt; its screen goes to build/cores/<id>.ppm.

  • core_conformance_<id> (every core CMake builds): the descriptor filled (ABI 5, id, family and licence as fm1_add_core gave them, button names, the keymap, the panel table after init), the screen not blank within 3 s, a note key sounding within 1 s, the flash round trip (a saved change survives a reboot from the image), a double load, the scripted reboot; per core core-glue/<family>/conformance.txt (tests/core_conformance.cpp documents it). The screens go to build/conformance/<id>.ppm.

  • common_fifo_test, common_coro_test, common_nor_test: core-glue/common's helpers on their own.

  • replay_test: tools/emu/scripts/cr_dmaj.txt and cr_allsynth.txt through a Device on the loaded module must give the reference emulator's samples bit for bit at 44.1 kHz and the same expect results; the same run rendered at 48 kHz must match the reference resampled by a windowed sinc within a stated tolerance.

  • double_load_test: the module loaded twice in one process, two scripts interleaved millisecond by millisecond, each equal to its single-load run (separate globals); one unloaded while the other plays on; ChoralRoot and Felucca loaded together, both sounding.

  • halt_test: the firmware's two exit() paths (the boot guard's UBOOT, SAFE MODE's Flash Data reboot) halt the core and leave the host running.

  • param_test_<id>: a core's parameter map on a Device: every entry set to min, max and its default reads back at once and 50 ms later, the enum texts are distinct, exactly the entries the settings record holds reach the flash image; it prints the map. ChoralRoot: off-grid targets land exactly, one entry per group prints the trace line a panel turn prints; for every core every knob's target on the boot screen is an entry or -1, the hidden entries pass the same checks. ChoralRoot: the view's KNOB3 follows the perform mode. Felucca / Melodee: param_format's texts, the HOME knob and engine entries follow the selected part and its engine.

  • tier2_test (skipped unless built with -DFM1_TIER2_SLOTS=79): the Tier 2 slots on the plugin's processor: a host write reaches the firmware and is not echoed, a panel turn reaches the host (coalesced, inside a gesture), a state restore and a preset load report every slot without gestures, the perform mode picked on the panel; after a switch to Felucca its map on the same slots, ALGORITHM on the panel relabelling the engine slots (with the engine's range).

  • knob_test: the eight knob parameters: their fixed names, order and host info (continuous, default steps, never parameterInfoChanged); ChoralRoot's view (the functions in the value texts, a host write of Knob 1 moves the voicing, not echoed); the turn rule for all seven knobs on the view and in the KEY, PERF and FX layers (Device::enc(role, +2) reports on that knob first, inside a gesture; a relative one is nudged and springs back; any other knob reports only if its function's value changed, without a gesture; nothing else reports), Knob 4 on the view explicitly; PRESETS on the view (only Knob Presets touched; Knob 4 follows its send untouched) and in the KEY layer (the sends change, no knob shows one: only Knob Presets reports); page switches clicked on the panel (the view -> EDIT -> HOME, PERF open / mode changed from the panel and from the host / closed, FX, KEY, Options open / closed, Felucca HOME -> ENV -> HOME): every knob's host value is its new function's before anything is turned, the first turn then reports a step from it (EDIT: Level 98 -> 99), a stale host write within 150 ms of the switch is dropped, the switch asks a VST3 host to re-read the values and a turn alone does not; the PERF layer and the perform mode switched from the host; a panel turn; the FX layer; Options (SELECT relative: 0.5 -> 0.75 is 6 detents at the device, it springs back to 0.5 with no detents; a row with no named entry: the hidden Option), the editor; Felucca's HOME, ENV, ENV DEST, SLICER (a hidden cell), EDIT 1, GLO and FX held; Melodee's HOME; Stepped encoders (24 steps): Knob 1 written 12/23 13/23 14/23 13/23 is 0, +1, +1, -1 detents (Voicing), clicks with the firmware's value pushed back between them count from it, a skipped index is 2 detents, 100 clicks of Knob Select take Tempo 120 -> 220 BPM (10 clicks: +10) with the host's value always the tempo's place, a push echoed back quantised (k/23, k/24) is never a detent, the Options cursor moves one row per click and the re-centre turns nothing, a click within the settle window is dropped and the next counts from the pushed value, Knob Master stays absolute, switching the mode turns nothing, the state keeps the mode and the steps; Felucca's Knob 1 and Tempo.

  • plugin_test: the plugin's processor, headless: MIDI notes press the keys and sound at 48 and 44.1 kHz, the panel parameters reach the HAL, MIDI out is well formed, the flash survives the state round trip, presets save / reset / load / rename / export / import / delete with backups, the installed bundles resolve their cores folder (all three cores load from it), the parameter list (49: the knobs, the buttons, the keys; 128 with the opt-in slots), and the firmware switch: ChoralRoot -> Felucca -> Melodee -> ChoralRoot relabels the buttons, follows each core's screen on the knobs (and rebinds the opt-in Tier 2 slots), plays a note on each, and comes back to ChoralRoot's working flash byte for byte. The Button params modes, driving btn_fx 0 -> 1 (2 s) -> 0 as a toggle controller does: Tap gives one 90 ms press and FX toggled once, nothing on the falling edge, two rises inside one tap two taps; btn_octup held at 1 is exactly one octave up and no panic (CC 123 never sent; the check sees a real panic); Hold holds 2 s and locks the FX layer; Toggle-hold holds from the first rise to the second; the state keeps the mode and the tap length. After the build, auval -v aumu Fm1v Qx7u validates the installed AU.

  • panel_render_test: the panel, headless (no window: painted offscreen into images). The computer keys at the component (keyPressed / keyStateChanged with the key-down probe faked): a key and its repeats press once, a release the host swallowed is caught by the check, the fallback row (C = FX as F5) and its "F5/C" hints, Z and Esc both holding OCT-, Cmd ignored, the focus lost lets go, and no key or button bit ever sticks. It writes the pictures build/panel/<theme>.png for every preset and custom-Test-Teal.png (904 x 566 at scale 2, key D4 held from the panel), Emulator-released.png, lcd.png (the LCD alone), lcd-1x.png, lcd-2x.png, big-lcd.png and editor.png (the whole editor), and checks: the LCD on the panel at 1x, 2x and in the big view (software and native renderers), sampled back to 240 x 240, equals the device's framebuffer (RGB565 to RGB888) within 1 per channel; D4's LED pixel is the lit colour while panelKey holds it and follows the device after the release, and its key_09 parameter never moves; a panel button and MASTER reach the HAL; every preset keeps the contrast floors (the table is printed); a custom theme saves, lists, becomes the default for a new instance, and a state naming it restores its colours into the editor after its file is deleted; a host write of Knob 1 (Voicing) to 0.25 and to 0.75 turns KNOB1's pointer by the firmware values' distance on the 270 degree sweep (knob1-025.png, knob1-075.png: the knob's pixels differ), the panel's own turn moves it too, and the relative PRESETS turns 15 degrees per detent the host gives it. And one picture per other core, build/panel/core-felucca.png and core-melodee.png (1.5 s after the power-on, D4 held), its LCD checked against its framebuffer.

  • remote_test: Remote FM-1 against libremotefm1's in-process fake unit (FM1EMU_REMOTE_FAKE=1: no USB or CoreMIDI device is opened): the host MIDI filter (exactly the unit's panel messages and RemoteFM1 SysEx); discovery's candidates (a table of endpoints with and without the USB property: only 1209:0001, or an "FM-1" name without the property, never an updater port, never the Onyx, a Roto-Control or a stock FM-1); the unit lock (two links on one lock file: the second is in use and sends nothing, then takes over when the first stops); Bluetooth (the fake as a RemoteFM1 0.2.2 unit over Bluetooth MIDI: the status says so, a change larger than a frame shows whole, tiles go in 4 colours, then exact); the processor with Remote on, in real time: the session starts on the (fake) bulk pipe, the unit's shadow equals the panel's lcd once settled, the LEDs are the panel's levels (each in the LEDS plane of its class), a key, a button, an encoder (PRESETS: a device turn, reported on Knob Presets inside a gesture, no host detents) and MASTER (the parameter follows) from the unit reach the core, the MIDI filter on and off, a firmware switch resends the screen whole (Felucca's screen on the unit), a second processor restored from the state is in use, the toggle off sends SESSION 0.

The reference emulator. replay_test compares against the submodule's own headless emulator, cores/ChoralRootFM1/build/host/emu --headless --script ... --wav .... The build target reference_emu builds it with the submodule's tools/emu/build.sh when it is missing (it needs SDL2: brew install sdl2; it writes only to the submodule's gitignored build/), and the test runs it to make build/reference/*.wav when they are missing or stale. Without SDL2, configure with -DFM1_REFERENCE_TESTS=OFF (or it is turned off with a warning) and replay_test is not built.

Requirements: CMake 3.22+, clang, network access on the first configure (JUCE 8.0.15 via FetchContent), a C++17 standard library (macOS only for now: the core uses os/lock.h, the loader dlopen and <uuid/uuid.h>), SDL2 from Homebrew for the reference emulator, and a python3 with Pillow and fontTools for the submodules' header generators (each repo's tools/build.py generate(); CMake picks the first python3 on PATH that has both, or pass -DCR_PYTHON=/path/to/python3). The generated headers go to each submodule's build/gen (Felucca's generate step also writes build/genwav), which those repos gitignore, so the submodules stay clean. Felucca's sample tables are generated from its assets/samples-cc0/ (in its repo: no download).

Licensing

GPL-3.0-only (see LICENSE), as required by the firmware it builds.

The firmware is the work of several projects, each included as an unmodified git submodule:

  • Felucca (cores/Felucca): GPL-3.0-only, Copyright (C) 2026 Leo Kuroshita (@kurogedelic), Hügelton Instruments. The firmware ChoralRoot and Melodee are forked from. Its third-party parts (Inter Tight, OFL 1.1; the Versilian Studios CC0 instrument samples, which the Felucca core contains; DaisySP and Rings, MIT; msfa, Apache-2.0; the Fukiai icons, MIT) are listed in cores/Felucca/LICENSING.md.
  • Melodee (cores/melodee): GPL-3.0-only, Felucca's copyright plus modifications Copyright (C) 2026 Kerem Kilic (Ellic Studio). Its FM6 and CZ-1 engines (MSFA / Dexed, Apache-2.0 / GPL-3.0-or-later; MAME's uPD933 model, BSD-3-Clause; the Casio CZ-1 factory tones as sound data) and the rest are listed in cores/melodee/LICENSING.md.
  • ChoralRoot FM-1 (cores/ChoralRootFM1, Quixotic7), which carries Felucca's code and Melodee's FM6 and CZ-1 engines. The full table of what comes from where is in cores/ChoralRootFM1/LICENSING.md.

The glue in core-glue/ (this repo's, GPL-3.0-only) compiles those sources as they are.

The editor's type is Inter Tight (plugin/fonts/InterTight-VF.ttf, Copyright 2022 The Inter Project Authors), under the SIL Open Font License 1.1 (plugin/fonts/OFL.txt); the build cuts four static weights from it (subset to Latin) and embeds them. Each bundle carries the licence as Contents/Resources/licenses/InterTight-OFL.txt.

This project is independent of and unaffiliated with M-VAVE.