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

StageFormatNote
Reed state, pickupQ301.0 = 230; 32×32→64 products
FX and gainQ241.0 = 224
Biquad coefficientsQ28DF-II transposed
Public outputQ201.0 = 220, mono
PhaseQ32232 = 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 tables and variation).
  • 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; octave Z/X; sustain Space.
  • 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

ReleaseScope
v0.1Fixed-point voice, behavioural chain, host tests, renderer, WASM emulator, site, docs, firmware skeleton. This tree.
v0.2Exact preamp/power-amp circuit solver (fixed point), 2× oversampling, A/B regression against OpenWurli renders, 48 kHz.
v0.3Hardware bring-up: audio, keys/velocity, encoders, LCD, USB MIDI; ROM budget and CPU measurements on a real FM-1; 8–12 voice tuning.
v0.4Flash settings, presets, update loader, web installer, first signed/validated image.
v1.0Calibration 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.