Thanks for your interest in improving Spore! This guide covers how the project is laid out, the conventions to follow, and how to get a change merged.
git clone --recurse-submodules https://github.com/rainybit-code/spore.git
cd spore
scripts/setup.sh # init submodules + build libDaisy & DaisySP (Windows: scripts\setup.ps1)
scripts/build.sh # -> build/daisy_synth.binSee README.md for the toolchain install and flashing steps.
You can build the firmware without any hardware; you only need a Daisy Seed + Hothouse to
run it.
src/
main.cpp wiring: mode dispatch, controls, MIDI, audio callback
hothouse.h/.cpp Hothouse hardware proxy (vendored, GPL-3.0)
config/ params.h (all tunables) + per-mode parameter structs
modes/ one class per mode, all implementing IMode (mode.h)
mod/ modulation engine (LFOs / S&H / chaos / RNG)
fx/ global FX (delay + reverb) and the master output stage
io/ controls, knobs, MIDI, clock, sensors
dsp/ shared voice DSP
- Language/style. C++14 (
-std=gnu++14). Formatting is enforced by.clang-format(Google C++, 4-space indent, 100-col). Runclang-format -ion any file you touch before committing — CI rejects unformatted code. - Header-only modules. Everything under
modes/,io/,mod/,fx/, anddsp/is header-only and pulled in throughmain.cpp; onlymain.cpp,hothouse.cpp, andusb_identity.ccompile as translation units (seeMakefile'sCPP_SOURCES). - Namespace. Project code lives in
namespace synthbox. - License header. Start every source file with the two-line SPDX + copyright header used throughout the tree.
- Tunable values go in
config/params.h. Ranges, rates, sizes, and thresholds belong there with a unit comment — not as literals inside DSP code. - Comments explain why the current code is the way it is when that isn't obvious from reading it. Keep them concise; don't narrate change history.
Implement the IMode interface in a new src/modes/<name>_mode.h,
add the matching tunables to config/params.h, then register an instance in the
g_modes[] array in main.cpp and bump MODE_COUNT in src/io/controls.h.
The firmware and the Propagator browser
editor share a CC/SysEx protocol. If you add or change a control that the editor should
see, update both src/config/params.h (the params::midi map) and
docs/MIDI_PROTOCOL.md.
Spore runs two contexts: the audio ISR (AudioCallback) and the main loop. State
shared between them lives in globals in main.cpp:
- The audio ISR owns the per-block DSP and writes the mode/FX selection and the CPU watchdog state.
- The main loop owns USB MIDI, the tempo clock, and the LEDs, and reads/writes the same selection state in response to MIDI.
- Globals touched by both contexts are
volatile(e.g.g_active,g_overload). Keep cross-context shared state to single-word reads/writes; there is no locking. - The
g_*Paramsstructs (config/*_params.h) are written by MIDI in the main loop and read by the ISR; they are plain value writes, safe to tear at the field level.
If you introduce new shared state, document which context owns it and prefer a single writer.
The audio callback has a hard deadline (one block, kBlockSize/48 kHz). A CpuLoadMeter
(g_cpu) tracks average/peak callback load and reports it over SysEx (cmd 0x02); a
watchdog (params::watchdog) sheds the global FX and halves Synth polyphony after sustained
overload. When you touch the DSP or the voice count, verify the worst case still has
headroom — and watch the peak, not just the average, since a single over-deadline block
crackles even when the average looks fine.
The heaviest configuration the engine can produce:
- Mode = Synth, FX = Reverb (
ReverbScis the costly one), master filter on with high resonance. - Synth params (CC 40+): voices = max (6), unison = max (4), engine = analog (4 PolyBLEP osc + sub per voice), filter = Moog (4-pole), drive up.
- LFO→cutoff depth up and chaos speed (CC 18) maxed so modulation churns every block.
- MIDI-flood: hold all 6 voices and retrigger fast with a short attack/decay, so every
voice's filter envelope stays in motion — that is what exercises the filter-coefficient
path on all voices at once (see the control-rate
SetFreqindsp/voice.h).
This pins 6 voices × 5 oscillators + 6 Moog filters + ReverbSc + master filter + limiter
simultaneously. A "pass" is: no audible crackle in the ~150 ms before the watchdog trips, and
the watchdog trips and then recovers cleanly (LED returns to heartbeat, full polyphony) once
the flood stops. Granular at 12 grains / max density + reverb is a lighter, separate path
worth a second check.
Each script exists as a .sh/.ps1 pair (scripts/build.{sh,ps1}, etc.). They are thin
wrappers over the Makefile — keep the actual logic in the Makefile where possible, and
update both files in the pair when you change one.
- Branch off
main. - Make your change. Running
clang-format -iandscripts/build.shlocally is the fast way to catch problems, but CI enforces both either way. - Add a line under
## [Unreleased]inCHANGELOG.md. - Open a PR against
main. On every PR, CI builds the firmware, checks formatting, and requires aCHANGELOG.mdentry (label the PRskip-changelogfor changes that don't warrant one, e.g. CI-only tweaks).
Releases are cut from version tags — see the Releases & versioning section of the README.