Skip to content

How it works

  • UI thread (Slint). Every edit goes through AppState, the single source of truth. Structural changes (add, remove, move a block, load a preset) produce a ChainSpec; live changes (a knob, a bypass, an A/B or mix change) produce a ChainCommand.
  • Chain builder thread. It instantiates the plugins and models of a ChainSpec, off the audio thread, and hands the finished chain over, replaying the live commands received meanwhile.
  • JACK callback. Tuner, then effects, then looper, with preallocated buffers. A new chain is crossfaded in over 20 ms; the old one is dropped off the audio thread.

The audio callback never allocates memory and never waits on a lock: it only uses try_lock and atomics. Model loading, file reading and plugin instantiation all happen on the builder thread. Long impulse responses are split: the first 170 ms are convolved in the callback, the rest on a worker thread with a deadline, so the callback’s cost stays flat.

Scenes, knobs and bypasses are commands applied to the running chain: no rebuild, so delays and reverbs ring on. Bypass uses a 10 ms ramp; delays and reverbs fade their input and let their output ring out.

A Buildroot image with BusyBox init, no systemd and no desktop:

  • a PREEMPT_RT kernel (Raspberry Pi 6.12, or mainline 6.12 on x86_64);
  • jackd at real-time priority 80 on the audio interface, waiting for one if none is plugged;
  • the UI full screen on DRM/KMS, without X11 or Wayland, restarted with the audio;
  • NetworkManager, chrony and dropbear, and nothing else.
Partition Mounted
FAT PEDALBOARD /boot, read-only pedalboard.txt, authorized_keys
System A and B /, read-only Never written while running, so a power cut cannot damage it
Data /data Presets, models, plugins, networks; created on first boot, checked at every boot

Updates are RAUC bundles signed with the release key.

Each image records OS_ID, a fingerprint of everything in the system but the app. A release whose OS_ID matches the running system is installed as an app update: the app alone, extracted to /data/app, tried at its next start and kept once it has run 30 seconds. Otherwise it is a system update: written to the other slot and tried once with the firmware’s tryboot flag on the Pi, or GRUB’s environment on x86_64, then kept once the UI has run 30 seconds.

The app runs as an unprivileged user. It downloads and checks the bundle, then asks a small root service to install it; RAUC checks the signature.

src/
├── audio/ JACK callback, chain switcher, tuner, looper
├── plugins/ lilv host, LV2 features, NAM, impulse responses, small DSP blocks
├── state/ AppState: chain, presets, scenes, setlists, session
├── presets/ Preset and setlist storage
├── tone3000/ TONE3000 account, library, pictures
├── midi.rs MIDI parsing, the Helix-style map, clock
├── update.rs Release check, download, request to the OS updater
└── …_ui.rs Glue between each Settings card or page and the UI
ui/ Slint UI: pages, components, theme
native/ C wrapper around NeuralAudio
os/ Buildroot tree: boards, packages, init scripts, RAUC