README from GitHub, MPL-2.0
THIS IS EARLY ALPHA, WORK IN PROGRESS, CONTROLS ARE CHAOTIC, CODE IS A MESS AND EVERYTHING IS SUBJECT TO CHANGE.
typhoon_fm1 — FM-1 as the voice of the Artinoise re.corder (or other EWU)
Custom firmware for the M-VAVE FM-1 that makes it the sound of the re.corder, a breath MIDI controller with no sound
of its own: breath is loudness and tone, the re.corder's tilt is pitch bend, and the FM-1 plays a stack of up to
three layered voices through its own effects. On
fm1-base and felucca-bsp
(base/), from Felucca by Leo Kuroshita (Hügelton Instruments). Earlier
named tornado: its preset backups restore here too.
The PANFLUTE engine is Denis Rosset's pan flute model
(toy-pan-flute, MIT), with the changes typhoon made to it listed in
firmware/src/engines/pan/denis/CHANGES.md.
On the web (https://ignis32.github.io/typhoon_fm1/, Chrome or Edge for USB): install it on your FM-1, edit presets live, or try it in the simulator with no FM-1.
Versions
The version is in VERSION (every build carries it: the header at power-on, SysEx INFO, tools/presets.py list).
- 0.3 (the first typhoon_fm1 release): on fm1-base, MPL-2.0 throughout;
engines SINE, ANALOG, NOISE, FM, FOLD, VOWEL, ERS, EVA; reverbs PLATE and HALL; 96 preset slots; the preset editor
(
web/editor/); LEAPS; USB audio in and out; Bluetooth BLE-P / BLE-C on their own (below). - 0.4: everything since 0.3 below, and the engine PANFLUTE, Denis Rosset's physical model of a pan flute (25 stopped pipes, G4 .. G6; other notes folded into them by octaves). It runs on the FM-1's second core, on one layer at a time. It keeps all three of his controls: breath (his PP / MF / FF markers as knobs), BEND CC (the lip covering the pipe) and the vibrato. VIB MODE TILT is his vibrato, from a moving controller such as a TEControl's tilt. LFO is a sine at RATE whose depth is set by how far VIB CC is from CENTRE, for example the re.corder's rotation. Factory presets 28 PANFLUTE_TEC (his settings) and 29 PANFLUTE_ART (the re.corder). PERFORMANCE shows each core in its own column.
- 0.5: the controls (
notes/05-controls.md). Breath is just a controller: the sources BEND and A .. D are set once in GLO → CONTROLS, each with its curve, SMOOTH and OVER (AO .. DO: a source's top end, for an overblow). Each layer's ROUTES (HOME) send them to its targets, one to many: its level, pitch, pan, its engine's EXCITE (what breath moved in it) and the knobs that may move while playing. No engine reads breath by itself any more: BR>AMP, BR>TONE, BR>SHP, BR>LP, TONE TALK's DEPTH and SUB OCT's BREATH are routes now; EVA's and ERS's vibrato no longer waits for 55 % breath (a route from A makes it deeper blown harder, one from B gives it to another controller); EVA's waves follow a WAVE MOD knob. PANFLUTE's BEND CC, VIB MODE and VIB CC gave way to its route targets LIP, WOBBLE and LFO DEPTH. Saved presets and settings from before are not read (the factory presets sound as they did). - Since 0.3: the engines FM, ANALOG, ERS and EVA (SINE, NOISE, FOLD and VOWEL removed: FM at INDEX 0 is the
sine, EVA has the noises, NS_*); 20 knobs an engine (EDIT 5), older presets read as they were. The mix's
BREATH LP is ERS's and FM's own (EDIT 5). Every delay on breath is a knob, 0 (none) on a new layer: EVA LAG and
VIB SWELL, ERS LAG and VIB SWELL; LAG is 0 in every factory preset (the breath at once), the only delay they
carry is the vibrato's swell. Every delay between breath and sound is listed in
notes/03-lag-watch.md. EVA has a SINE wave (after TRI). QUIRKS (ARP's third page): each fingered note a little off in pitch and level, the same way every time, as a real instrument's tone holes; QUIRK SET picks another instrument's (EVA's hidden random humanise is gone). - 0.2 (2026-10-17, as tornado, on its earlier GPL platform; not in this repository) — the first usable one. BLE central, tilt → pitch
bend, three layers per preset with seven engines, the effects (then also an OLD reverb), 64 slots, SAVE / SAVE AS,
backup and restore over USB-MIDI (
tools/presets.py); no editor page yet.
Playing it
The link. GLO → BLUETOOTH, two sides on their own: BLE-P (KNOB 1) ON lets a computer or phone find "TYPHOON 1F2D"
(four hex digits of each FM-1's own, so two are told apart) and connect; BLE-C (KNOB 2) connects the FM-1 to a Bluetooth MIDI device: OFF, FIRST (the default: the first it finds), CHOOSE
(the one you picked). KNOB 3 opens the list of devices around: REFRESH, the devices (the remembered one
marked *, the signal in bars), FORGET; OCT+ takes the entry, OCT- shuts the list. AUTO_RECONNECT (KNOB 4) ON:
whenever no device is connected (power-on, one gone) it looks until one is; OFF: one look. Under each side its state
in words, below them the log. Both OFF: the radio off. USB-MIDI and TRS MIDI always play too, on any channel. Notes are mono,
the last one wins, a slur is legato.
Controls (GLO → CONTROLS; the design: notes/05-controls.md). The controller is described once, as sources:
BEND (MIDI pitch bend, or a CC centred on 64 such as the re.corder's tilt, CC52: GLO → BEND) and A, B, C, D,
each a CC, AT (channel aftertouch) or OFF. By convention A is the breath (CC11 by default) and B the re.corder's
rotation (CC52, the same controller as BEND). EDIT on CONTROLS opens the curve of the source turned last (ENV opens
A's): LOW, HIGH, SHAPE and OVER; EDIT again: SMOOTH (0, none, by default) and, for A, where it comes from (AUTO:
its controller, else the note-on velocity). Past OVER a source's top end is a source of its own, AO .. DO, from 0
to full (an overblow: growl, drive); the source itself goes on to full. Curves are global: no preset has its own.
USB audio (GLO → USB AUDIO): USB IN, the computer's audio through the FM-1, and USB OUT, the FM-1 recorded on the computer, each OFF (the default: the computer gets no such audio device, so it keeps its own defaults), MUTE (there, silent) or on at a level of 0 .. 200 %; USB OUT records the MIX (the computer's audio in it) or the SYNTH alone. MASTER is the speaker's and the headphones' volume only. A change to or from OFF applies once the knob rests: the FM-1 drops off USB for about a second (MIDI too) and comes back with the other audio devices.
USB host (GLO → USB HOST): ON makes the FM-1's USB port a host for a USB MIDI device, such as the re.corder by cable, plugged in directly or behind one hub. The FM-1 does not power the port, so use a powered hub or a powered OTG adapter. While it is ON, no computer can use that port: no USB audio, and no editor or installer over USB. Turn it OFF to get them back, or hold REC at power-on to enter update mode. The setting is saved; the FM-1 starts as a USB device and switches to host on its first frame. OFF by default. Untested on an FM-1 yet.
Performance (GLO → PERFORMANCE): is the sound keeping up. Two columns: CORE 1 (its own work, without its
waits for core 2) and CORE 2 (NONE when no engine uses it). Each shows AVERAGE, the load over about 90 ms; WORST 1 S,
the slowest block in the last second; and WORST, the slowest since the reset. All are % of a block's time, red from
90 %. BLOCK and 1 S below them are the whole render, waits included: that is what must fit in time. XRUNS counts the
blocks not done in time since the reset, each heard as a click, and says how long ago the last one came. Turn KNOB 1
to reset. Over USB: tools/presets.py stats.
The sound. A preset is the whole sound: three layers (engine and its knobs, level, transpose, pan, envelope, its routes: what the sources do to it) and a name. The sound after the layers (drive, detune, delay, reverb, volume) is in groups: each has a global copy (the effects on REC; an amber header), and a preset uses it (values dim, a GLOBAL tag) until a knob is turned on the preset's page — then the group is the preset's own (OWN tag). Holding the page's button gives it back to the global one.
| Control | Does |
|---|---|
| PRESETS encoder | the preset (loads it) |
| SELECT encoder | the layer 1-2-3 |
| ALGORITHM encoder | the layer's engine (the last place: OFF) |
| KNOB 1-4 | the page's four values |
| HOME | ROUTES, the layer's grid: what the sources do to it. Rows its targets (LEVEL: the source's share of the level, PITCH, PAN, EXCITE: what breath moves in the engine, TONE's knobs, the engine's knobs that may move), columns the sources (PB: BEND, the assigned A .. D, AO .. DO). KNOB 1 the row, 2 the column, 3 the amount (-100 .. +100 %); one source to many, many to one. A new layer: BEND → PITCH, and its engine's own (A → EXCITE, A → its filter) |
| ENV | A's curve (the breath's) → A + (SMOOTH, A FROM) |
| EDIT | the engine's knobs, page by page (EDIT 1 .. 5, the empty ones passed by) → TONE (after the engine: OFF, FILTER LP/BP/HP, SWEEP, TALK, SUB OCT) |
| FX | DRIVE → DETUNE → DELAY → REVERB → REVERB + (TYPE: PLATE / HALL, PRE-DLY), again and again · hold: back to global |
| LFO | ENVELOPE (per layer, drawn): ATTACK, DECAY, SUSTAIN, RELEASE → ENVELOPE + (DELAY). Restarts on a fresh attack, legato runs on |
| ARP | LAYER (engine, level, transpose, pan) → LAYER + (filter type, fine) |
| SEQ | MIX: the 3 levels, each with its engine, and the preset's VOLUME (KNOB 4: the whole sound, effects too; 100 = as it is) · hold: VOLUME back to global |
| GLO | the global settings: GLOBAL (bend range, transpose) → BLUETOOTH (BLE-P, BLE-C, the list, AUTO_RECONNECT) → BEND (the tilt's CC, DEAD, RANGE, INVERT) → CONTROLS (A .. D; EDIT: the one turned last's curve, then its +) → LEAPS → USB AUDIO → USB HOST → PERFORMANCE (load, xruns) · hold: GLOBAL TO FACTORY ? |
| REC | the global effects: DRIVE → DETUNE → DELAY → REVERB → REVERB + (what a preset without its own group plays) · hold: all notes off |
| OCT− / OCT+ | press: transpose a semitone (when let go) · hold: an octave (or SEL + press) · both: 0. Cuts the sounding note |
| PLAY | mute the layer · hold: solo it (again: back; PLAY blinks meanwhile) |
| SAVE | SAVE TO nn ? (PRESETS picks the slot) · hold: SAVE AS (the keys type the name — a legend on screen — PLAY deletes, SELECT the cursor, ALGORITHM 0-9 - . /, PRESETS the slot; only OCT+ saves, OCT- cancels) |
A question on the screen is answered with OCT+ = YES, OCT− = NO (the status row says so; SAVE again also saves in the SAVE prompt; any other button, or 10 s, is NO; while asked, OCT does not transpose).
Presets on a computer (USB-MIDI, nothing to install but Python 3): tools/presets.py list, backup [FILE] [--slot N], restore FILE [--slot N] [--to M], rename N NAME, delete N, stats (the audio's CPU load while
you play). Stock V15 wipes the presets (not the globals): back them up before installing it.
The preset editor (web/editor/, served from any web server; Chrome, the FM-1 on the computer's USB): every
parameter of the sound that plays, live (the FM-1's knobs show there too), each sound group OWN / GLOBAL, and
the slots: save, save as / to the next free slot, copy, rename, delete, backup / restore (the files presets.py
writes and reads). ?sim (the link "try it without an FM-1") runs it on the firmware in the page.
Building and working on it
-
git submodule update --init --recursiveonce: fm1-base inbase/, felucca-bsp inbase/bsp/. -
./build.sh [--engines PROFILE | a,b,c] [--out DIR] [--release X.Y]— the device build, fm1-base's (base/tools/build.pyonapp/fm1.toml; the JieLi toolchain and AC79 SDK; its checks: no doubles, no 64-bit multiply inlined beside a load, register access only in felucca-bsp, the sizes), with Bluetooth, and itsreport.json. A profile isprofiles/<name>.txt: engines one a line,include NAMEfor another profile. -
Tests:
sh tools/test.sh(web/sim/build.sh, then tests/sim_test.mjs; zig 0.14.1, fetched if missing, Node 20; after./build.sh, which makes the generated headers):tests/sim/*.test.mjs(core, transpose, presets, fx, reverbs, ers, eva, sysex, the editor page's logic, Bluetooth, buttons, USB audio; each on a fresh firmware, numbers by name fromtools/consts.mjs) on the simulator (web/sim/typhoon_sim.c: the app on fm1-base's host platform and its browser emulator's clock, as WebAssembly). -
./build.sh --web— the browser emulator (fm1-base's page with typhoon's part,app/web/emu/; emcc) intoapp/build/emu/. -
An engine is a folder
firmware/src/engines/<name>/with a stable id inengines/engines.h; nothing outside it depends on it. The UI is data:firmware/src/ui/layout.h. -
Releases and the site (GitHub Actions): a tag
vX.Y(VERSION) builds, tests and attests the package and publishes it as a release (.github/workflows/release.yml, The Firmware Hub's sharedzvenson/fm1-build); thenpages.ymlmakes the site (tools/site.sh:install/for that package,editor/,sim/) on GitHub Pages.
Layout
app/— typhoon on fm1-base:fm1.toml(the build's settings: name, identity, Bluetooth, USB audio),app.h/app.c(the platform's callbacks, and the calls typhoon's code makes, in fm1-base's terms),ble.c(the BLUETOOTH modes and the remembered device),web.candweb/emu/(typhoon's part of fm1-base's browser emulator,./build.sh --webintoapp/build/emu/: Web MIDI in, a breath slider, the presets kept).firmware/src/— typhoon itself:core/(layers, effects, reverbs, parameters, presets, SysEx),engines/,dsp/,ui/.base/— fm1-base (a pinned git submodule), with felucca-bsp inbase/bsp/: the platform. typhoon's changes to them are patches inpatches/(applied before every build:patches/README.md,tools/patches.sh).tools/—engine_flags.sh(profiles),patches.sh(patches/ontobase/),presets.py(the presets over USB-MIDI; installs:base/tools/fm1_install.py),consts.mjs(the firmware's numbers by name, for the tests),gen_engine_tables.py(firmware/gen/fe_tables.h),test.sh(the checks),site.sh(the site).tests/,web/sim/(the test simulator: the app on fm1-base's host platform, WebAssembly),web/presets/(the preset protocol in JS),web/editor/(the preset editor page:core.mjsits logic,app.jsthe page,simport.mjsthe firmware as a MIDI port).profiles/(which engines a build has);notes/— write-ups for the community. The platform's API and rules are fm1-base's (base/DESIGN.md,base/include/plat.h).- Licences:
LICENSE(MPL-2.0, typhoon's own code),LICENSING.md(what is under which licence),LICENSES/.