Documentation
The canonical, versioned documents live in docs/ in the source
tree. This page is the map and the short version.
Overview
Reed puts a Wurlitzer 200A physical model on the M-VAVE FM-1, a 27-key MIDI keyboard built around the JieLi AC79 (pi32v2, 360 MHz, no FPU). The project has three pieces:
- Engine — portable C99, fixed point only, 44.1 kHz, 8 active voices by default (32-slot capacity), mono. It is the same source on the host, in WebAssembly and on the device.
- Firmware — the FM-1 application: key scan, encoders, LCD, USB MIDI,
audio DMA and settings, built on
felucca-bsp(the hardware layer extracted from Felucca). - Tooling — the table generator, the offline WAV/MIDI renderer, the test suite and the browser emulator.
Working title. Reed is unofficial and not affiliated with M-VAVE, JieLi, Hügelton Instruments or the OpenWurli authors.
Architecture
tools/tablegen (Rust, OpenWurli) ──► engine/src/wurli_tables.c
│
engine/src/wurli_engine.c (C99, fixed point)
│ │ │
tools/render web/wasm_glue.c firmware/src
(WAV / MIDI) (AudioWorklet) (felucca-bsp)
The engine is deliberately freestanding: no libc, no libm, no allocation, no floats.
That is what lets one binary-identical model run on a desktop, in a browser and on the
AC79. The only platform input is engine/include/wurli_tables.h, generated
data.
Number formats
| Stage | Format | Note |
|---|---|---|
| Reed state, pickup | Q30 | 1.0 = 230; 32×32→64 products |
| FX and gain | Q24 | 1.0 = 224 |
| Biquad coefficients | Q28 | DF-II transposed |
| Public output | Q20 | 1.0 = 220, mono |
| Phase | Q32 | 232 = one cycle |
Voice
Each voice has seven quadrature modes (Q30). Mode amplitudes include the per-note
variation and the velocity scale; the hammer dwell is a Gaussian applied at note-on.
The pickup is a natural cubic spline over a 2D field-solved capacitance law, followed by
the trapezoidal node recursion. The pickup's m (node capacitance) is
evaluated per sample; the reciprocal is held at its rest value because it moves by less
than 0.3 % over a full swing (documented simplification, below audibility).
Shared chain
After the voice sum: input coupling high-pass, preamp asymmetry, Tremolo (Twin-T rate 5.63 Hz → CdS LDR attack/release → depth divider), the drawn 10 kΩ volume pot with R-11, the 33 Hz feedback shelf, the power amp's closed-loop gain and crossover knee, and the speaker polynomial + 30 Hz/6.5 kHz filters. A final 12 Hz DC blocker removes the quantised filter's residual offset.
Voices and stealing
Eight active voices by default (voice_limit; 32-slot capacity). A new note takes a free slot; when all are busy, the
quietest released/oldest voice is copied into a 5 ms crossfade and the slot restarts.
Top-five reeds have no damper, as on the real instrument. Sustain (CC64) is supported.
Porting notes
What is a direct port
- Per-note mode ratios, amplitudes, decay rates, detune and variation (OpenWurli
tablesandvariation). - Hammer dwell attenuation and onset ramp, including the velocity curve.
- The pickup overlap law tables (four keyboard sections, 2D field solve), band polarity, rest offset, sensitivity and node recursion.
- Attack noise burst: LCG, band-pass and exponential envelope.
The fixed-point voice was validated against OpenWurli's float voice before any FX were added: C4 at velocity 100 matched the reference peak to 0.2 dB, the spectral bands from 20 Hz to 20 kHz to 0.3 dB, and the decay curve to 0.2 dB over two seconds.
What is behavioural in v0.1
- Preamp, power amp and speaker are calibrated behavioural stages, not the nine-node MNA/Newton solver. The transfer, coupling, crossover and cabinet shape are matched; the exact nonlinear circuit is v0.2.
- Tremolo uses the real rate, LDR attack/release and depth law, but a sine Twin-T approximation instead of the generated circuit oscillator.
- MLP per-note corrections are off. They are an optional research layer trained on recordings; the underlying physics does not depend on them.
- 64 voices become 8; 2× oversampling is omitted in v0.1. Both are CPU decisions for the 360 MHz target and are visible in the plan.
- 44.1 kHz only. The table generator is rate-aware; v0.2 can emit a 48 kHz set if the USB path needs it.
Calibration and table generation
All physics data is generated from a pinned OpenWurli commit, not transcribed by
hand. The generator is a small Rust crate that depends on openwurli-dsp:
cd tools/tablegen cargo run --release > ../../engine/src/wurli_tables.c
The generated file is committed so normal builds need neither Rust nor network access. It contains 64 notes × (7 modes, phase/rotation, decay, damper, velocity drive, output gain), four pickup laws, the shared curves and the amplifier/speaker coefficients. Regenerating it after an OpenWurli calibration change is the whole update process for the voice.
The generated tables are derivative data of OpenWurli (GPL-3.0-or-later)
and carry the same licence. The generator is in tools/tablegen; the pin is
in its Cargo.toml.
Build and test
Host
# engine test suite (assert-based, no framework) cc -O2 -Iengine/include \ engine/src/wurli_engine.c engine/src/wurli_tables.c \ engine/tests/test_engine.c -lm -o build/test_engine ./build/test_engine # offline renderer cc -O2 -Iengine/include \ engine/src/wurli_engine.c engine/src/wurli_tables.c \ tools/render/render.c -lm -o build/reed ./build/reed note --note 60 --vel 100 --out note.wav ./build/reed midi --in song.mid --out song.wav ./build/reed demo --out demo.wav ./build/reed bench
The tests cover silence, level and velocity, determinism, pitch (+/−25 cents), damper, sustain, polyphony/stealing, 30 s stability and a fixed-point golden hash.
Browser emulator
./web/build.sh python3 -m http.server -d web 8080 # http://localhost:8080/try/
web/build.sh compiles the same C to a standalone wasm32 module with
Emscripten's clang and wasm-ld. The module imports nothing; the worklet
receives the bytes from the page and instantiates them.
MIDI file support
The renderer reads Standard MIDI Files (format 0/1, PPQ timing, tempo changes, note on/off and CC64). It ignores program changes, pitch bend and other controllers. The output is 16-bit stereo at 44.1 kHz, both channels carrying the mono model.
Firmware status
The firmware application is in firmware/ and targets the FM-1 through
felucca-bsp:
- ALNK0 I2S audio, 128-frame half buffers (2.9 ms), the engine rendered in the DMA interrupt.
- 41-key scan (27 notes + 14 buttons), 7 encoders, LCD, LEDs and USB MIDI (TRS MIDI is planned).
- Parameter layer: volume, tremolo, drive, speaker, tune, sustain and panic.
- Flash settings and the Felucca-compatible update loader are planned for the first hardware release.
Alpha. The BSP integration compiles in the project's
host-simulation mode, but no image has been validated on a physical FM-1 yet. No web
installer is published. Do not flash an untested image. Build prerequisites and the
JieLi toolchain setup are in BUILDING.md; recovery is the Felucca/transporter
path described there.
The FM-1 pin map, key ids and board facts are not duplicated here: they are the facts
felucca-bsp/boards/fm1 establishes. This project consumes that layer rather
than re-deriving it.
Emulator
The emulator is not a sample player or a recording: it is the shipping engine compiled to WebAssembly. Controls:
- Click Power, then play the 27 keys (F3–G5).
- Computer keyboard:
A W S E D F T G Y H U J K; octaveZ/X; sustainSpace. - A connected MIDI keyboard is used after power-on (Web MIDI, Chrome/Edge).
- Sliders map to the same engine parameters as the firmware: volume, tremolo, drive, speaker character and tune.
Known limits: the browser audio device may run at 48 kHz and resample; the engine is 44.1 kHz. The monitor speakers/headphones colour the sound, as always. Reverb is not part of the model.
Plan
| Release | Scope |
|---|---|
| v0.1 | Fixed-point voice, behavioural chain, host tests, renderer, WASM emulator, site, docs, firmware skeleton. This tree. |
| v0.2 | Exact preamp/power-amp circuit solver (fixed point), 2× oversampling, A/B regression against OpenWurli renders, 48 kHz. |
| v0.3 | Hardware bring-up: audio, keys/velocity, encoders, LCD, USB MIDI; ROM budget and CPU measurements on a real FM-1; 8–12 voice tuning. |
| v0.4 | Flash settings, presets, update loader, web installer, first signed/validated image. |
| v1.0 | Calibration listening pass, factory presets, recovery docs, release artifacts. |
Lab build and profile selection
REED_LAB=1 ./firmware/build.sh builds a measurement image with
MIDI-CC profile control (CC20..27), an optional USB-CDC telemetry console and optional
USB audio capture at 44.1 kHz. The primary capture is the P2 analog output. It is not a
release. The CC map, serial protocol, safety notes and capture runner are on the
lab page.
The final 8/16/32 profiles will be chosen from the same MIDI song rendered on real hardware, captured as 320 kbps MP3 with CPU/underrun logs. That page is WIP: hardware, song and captures are still pending.
Polyphony and quality
The current budget is 8 full voices. Raising it to 16 or 32 is a CPU/quality
trade, not just a number in a menu: fewer modal modes and a lighter pickup
reduce the real per-voice cost. The full analysis, the proposed presets
(HQ_8, MQ_16, LQ_32), the persistence
plan and the hardware risks are in
docs/POLYPHONY.md.
Licence and credits
Reed is GPL-3.0-only. The generated fixed-point tables are derived
from OpenWurli (GPL-3.0-or-later).
The board layer is felucca-bsp
(MPL-2.0), compatible with GPL. Felucca itself is GPL-3.0. See LICENSING.md,
CREDITS.md and the licence texts in LICENSES/.
OpenWurli's research and calibration (reed physics, pickup field solve, circuit characterisation) are the foundation of the voice. Felucca and felucca-bsp established the FM-1 hardware facts and the update path. Neither project endorses this one.