fm1-ctrl
A thin, transparent MIDI controller for the M-VAVE FM-1: every control sends its own event, unchanged, and the mapping lives in the receiving host (a web emulator, a DAW, or a map pushed over SysEx). The firmware has no business logic.
Status: ported to fm1-base (MPL-2.0),
which brings Bluetooth MIDI. The app is one file, app/app.c.
The map
| control | message |
|---|---|
| 27 keys | Note On/Off, note 53..79, velocity from the key's own sensing |
| 20 of them | poly aftertouch 0xA0 note pressure — the keys on the analog rows only |
| 7 encoders | CC 20..26, relative, 64 + detents (split at ±63) |
| 14 buttons | CC 40..53, 127 down / 0 up, labels FX..OCT+ |
| MASTER | CC 27 (7-bit) plus the 14-bit pair CC 7 / CC 39 |
| LEDs / screen | echo of this unit's own panel: held keys and buttons, and each held key's pressure |
Buttons are sent as plain CC, not 0xFA/0xFC and not MMC. Measured reasons:
the FM-1 emulators drop every byte >= 0xF0 (melodee/Felucca
index.html:439,472 — if (d[0] >= 0xF0) return;), and no firmware in the ecosystem
reads MMC. DriftSeq implements this exact
map on purpose (web/emu.js:184), so it is the baseline this firmware must not break.
Build
git submodule update --init --recursive # the platform
export AC79_SDK=/path/to/AC79_AIoT_SDK # only for Bluetooth
python3 platform/tools/build.py app # firmware + package
python3 platform/tools/build.py app --host # the host simulator
python3 platform/tools/build.py app --web # the browser emulator
Output: app/build/ctrl.bin, app/build/ctrl.fwsc (identity FM-1_9300 in a dev
build).
Verify without the hardware
Two independent ways.
The contract tests — the real app/app.c, on a fake platform, no device and no
fm1-base build:
sh app/tests/run.sh # 15 cases, ~0.2 s
app.c is included rather than linked, so what is tested is the source that ships,
and the test can read the state the app keeps (the LED masks). A "frame" there is
app_render then app_frame, because the platform drains MIDI in from the audio
interrupt.
The host simulator — the whole platform, writing the actual bytes it sent:
python3 platform/tools/build.py app --host
app/build/host/ctrl_host app/tests/map.txt app/build/host
cat app/build/host/midi_out.txt
See platform/README.md for the script language (wait, key, press, turn,
shot, expect).
Repository shape
app/ the firmware: app.c, app.h, fm1.toml, words.tsv, tests/
platform/ fm1-base (submodule): bsp/, device/, include/, tools/, web/
tools/ this project's own scripts (map generation, checks)
web/ fm1-shim.js: drives a third-party emulator from the real FM-1
docs/ specs, maps, research notes
Everything under platform/ is MPL-2.0 and belongs to its authors; our own files are
MPL-2.0 too. No GPL-3.0 code is in this tree — see LICENSING.md for how that
came about and what it replaced.
What it does not do
- It does not drive other firmwares' browser emulators by itself. Their worklets accept
{type:"buttons"|"keys"|"enc"|"master"}and have no MIDI input;web/fm1-shim.jsis the translation layer, and it needs the page to expose itssend(). - It does not make sound. A controller sends events.
- Its own screen and LEDs echo this unit: the keys and buttons it is holding, and
the pressure each analog key reports. What the host sends back is counted (
IN n) and otherwise ignored — one FM-1, one source of truth for what the panel shows. - It sends aftertouch for the 20 keys on the analog rows (F3..C5) only. The seven
keys of the top row (C#5..G5) have no converter of their own — their velocity comes
from a probe through the LED line — so they never send
0xA0.
Docs
docs/base-and-mcu.md— what fm1-base gives a firmware, and what the AC79 can actually do: the audio path, the second core (and its hardware defect), memory, the analog keys. Read this before designing an engine.docs/research/porting-to-fm1-base.md— what the port took, the ten pitfalls, and what DriftSeq will meet doing the same.docs/research/timer5-isr-trap.md— the TIMER5 wrapper trap of the old tree.docs/research/upstream-audit.md— the old tree's code lineage.
Licence
MPL-2.0. See LICENSING.md.
