Hello, Machines — your first rack voice¶
Note
This is the end-to-end walk-through for adding a new Machine (a voice that
lives inside the TBD-16’s GrooveBoxRack — sometimes called a rack plugin in
the codebase). In ~15 minutes you’ll add a new drum Machine called my2, hear it
from the desktop simulator (no hardware needed), and have it routed through the
WebUI’s machine tabs.
New to the project? Read Quickstart first — it covers the one-time setup (clone, OS-specific dependency install, building the simulator) and the legacy-plugin vs Machine distinction. Then come back here. Already built the simulator? Skip to Step 1 below.
Other references: Writing a Machine for the API
reference (every macro, every helper), Machines page for the
catalogue of every Machine that ships today, Desktop Simulator
for the simulator’s flags and /ctrl page details.
What we’re building¶
A drop-in replacement for the Analog Bass Drum voice on Kick 2 (track 1, the second drum strip). It will:
Live on MIDI channel 10 (= channel index 9), note 37 — same as the existing
fmbvoice on that track.Have two parameters —
freq(pitch) anddecay(envelope length) — exposed as MIDI CCs and as WebUI sliders.Render a simple sine-burst envelope (we’re not chasing audio quality here — we’re proving the toolchain).
By the end you’ll have written about 15 lines of DSP. The wiring is one command.
Prerequisites¶
You need:
A C++17 compiler (Apple
clang/ GCC / MSYS2mingw-w64-x86_64-gcc)CMake 3.16 or newer
Boost —
filesystem,thread,program_optionsmacOS:
brew install cmake boostDebian / Ubuntu:
sudo apt install build-essential cmake libboost-all-dev libasound2-devArch:
sudo pacman -S cmake boostWindows: see Quickstart for the MSYS2 setup.
You don’t need ESP-IDF, a TBD-16 board, or an audio interface. Everything in this walk-through runs on your laptop’s default sound device.
Now clone the repo and build the simulator:
git clone --recurse-submodules https://github.com/dadamachines/ctag-tbd
cd ctag-tbd
cd simulator && mkdir -p build && cd build && cmake .. && make tbd-sim load-test routing-test
./tbd-sim -o
(Already cloned without --recurse-submodules? Run
git submodule update --init --recursive from the repo root, then redo the
cmake .. && make step.)
Confirm the baseline works: open http://localhost:8080/, load GrooveBoxRack,
then open http://localhost:8080/ctrl → GrooveBoxRack (MIDI) and play the
drum pads. You should hear the default kit. Now Ctrl-C the simulator and proceed.
If the build fails or the simulator crashes on launch, see the Desktop Simulator page — it has per-OS dependency notes, audio-device gotchas, and the most common fixes.
Step 1 — Write a 12-line descriptor¶
The descriptor is the single source of truth — rackgen.js consumes it and edits
everything else. Create generators/rack-my2.json:
{
"id": "my2",
"className": "RackMy2",
"name": "My BD 2",
"type": "drum",
"track": 1,
"params": [
{ "id": "freq", "name": "Freq", "ctrl": 8, "def": 32 },
{ "id": "decay", "name": "Decay", "ctrl": 9, "def": 24 }
]
}
The cross-references this enforces:
track: 1is the Kick 2 track — its MIDI channel and note number come fromsdcard_image/data/synthdefinitions.json. (Drum track 1 is channel-index 9, note 37 — but you don’t need to write that anywhere; the registry registration will read it from the descriptor automatically.)type: "drum"means you implementtrigger()(notnoteOn/noteOff); the descriptor must match the target track’s type orrackgen.jsrefuses.ctrl: 8, 9are the per-machine MIDI CC numbers. Convention: machine params start at 8 (the channel mixer occupies 1..7). Eachctrlmust be unique within this machine.
Step 2 — Dry-run rackgen.js (sanity check)¶
Before changing anything in your tree, preview every patch:
cd generators
node rackgen.js rack-my2.json
You’ll see:
generated RackMy2.hpp + .cpp in .../generators— the class skeleton, written tocwdso you can eyeball it. (In dry-run mode nothing else is touched.)Three JSON patch previews for
synthdefinitions.json,mui-GrooveBoxRack.json,mp-GrooveBoxRack.json.Four
ctagSoundProcessorGrooveBoxRack.{hpp,cpp}wiring snippets — the#include, the member, theInit()line, theProcess()block, and thebuildVoiceRegistry()line.A footer:
(track 1 = WebUI CH02 "Kick2", MIDI ch 10 note 37).
Confirm the values look sane, then delete the temporary files in generators/:
rm RackMy2.hpp RackMy2.cpp
Step 3 — Apply with -i (the one command)¶
node rackgen.js rack-my2.json -i
Output ends with:
patched: ✓ synthdefinitions.json ✓ mui-GrooveBoxRack.json ✓ mp-GrooveBoxRack.json
wired: ✓ GrooveBoxRack.hpp member ✓ Init() ✓ Process() ✓ buildVoiceRegistry()
(.bak files kept for every file we touched — diff if anything looks off.)
Next steps:
1. cd simulator/build && cmake . && make
2. Open RackMy2.cpp and fill in the DSP body inside Process().
Every ✓ means an automatic insertion succeeded. If any line is ✗, the report
also prints the raw snippet you can paste manually (almost never needed).
The new files live at:
components/ctagSoundProcessor/rack/RackMy2.{hpp,cpp}— your class.Five edits in
components/ctagSoundProcessor/ctagSoundProcessorGrooveBoxRack.{hpp,cpp}— wiring.Updated
sdcard_image/data/synthdefinitions.jsonandmui-/mp-GrooveBoxRack.json.
Step 4 — Build the simulator (CMake re-config required)¶
The new rack/RackMy2.cpp is picked up by a file(GLOB ...) in CMake that does
not auto-refresh. You must re-run cmake . once after adding a file:
cd ../simulator/build
cmake . && make
If you skip cmake . you’ll get a linker error about RackMy2::trigger() not
being found — the .cpp wasn’t added to the build. That’s the symptom; re-run cmake.
Step 5 — Verify the baseline still works¶
Before touching the DSP, make sure the wiring is healthy. Run the two headless harnesses:
./load-test GrooveBoxRack
./routing-test
Both should report PASS. load-test confirms the rack constructs, fires
notes and isn’t silent; routing-test confirms the new voice didn’t break
any existing routing
(it shouldn’t — the new entry is added to the registry but only fires when
ch2_my2.enabled = true, which only happens when you select "my2" from the
machine dropdown).
Step 6 — Write the DSP (the only manual part)¶
Open components/ctagSoundProcessor/rack/RackMy2.cpp. The template gave you:
An
Init()that registers both parameters withregisterParamAndCC.A
trigger()stub that setsmidi_trig = true.A
Process()skeleton that reads the params viaMK_FLT_PAR_*macros and fillsout[BUF_SZ]with zeros.
Replace the body of Process() with a sine-burst:
void RackMy2::Process(const GrooveBoxRackProcessData &data) {
bool _trig = false;
if (midi_trig) { _trig = true; midi_trig = false; }
if (!this->enabled) return;
MK_FLT_PAR_ABS_NOCV(fFreq, freq, 4095.f, 1.f) // 0..1
MK_FLT_PAR_ABS_NOCV(fDecay, decay, 4095.f, 1.f) // 0..1
// Map fFreq 0..1 to 50..200 Hz; fDecay 0..1 to 50..1000 ms (samples at 44.1 kHz).
const float freqHz = 50.f + fFreq * 150.f;
const int decaySps = static_cast<int>((50.f + fDecay * 950.f) * 44.1f);
if (_trig) { phase_ = 0.f; envSamples_ = decaySps; }
const float dPhase = freqHz / 44100.f;
for (int i = 0; i < BUF_SZ; i++) {
float env = envSamples_ > 0 ? (envSamples_ / static_cast<float>(decaySps)) : 0.f;
out[i] = std::sin(2.0f * 3.14159265f * phase_) * env * 0.8f;
phase_ += dPhase;
if (phase_ > 1.f) phase_ -= 1.f;
if (envSamples_ > 0) envSamples_--;
}
}
You also need two private members in RackMy2.hpp (alongside the atomic<int16_t>
parameter members the template generated):
private:
float phase_ { 0.f };
int envSamples_ { 0 };
Add #include <cmath> at the top of RackMy2.cpp for std::sin.
Step 7 — Build and hear it¶
Rebuild — no cmake . re-run needed this time (we only edited existing files,
no new file was added to the GLOB):
make
./tbd-sim -o
Open http://localhost:8080/, load GrooveBoxRack, then in the CH02 (Kick2)
machine tabs, pick My BD 2. Open http://localhost:8080/ctrl → GrooveBoxRack (MIDI)
and hit the Kick 2 pad. You’ll hear a short sine-burst kick. Move the Freq /
Decay sliders in the WebUI — the sound changes in real time.
If the sliders aren’t there, force-refresh the WebUI (Cmd-Shift-R / Ctrl-F5) — Shoelace caches param specs aggressively. If you hear nothing, check:
./load-test --machine my2— does the isolated-voice test produce a non-zero peak?The machine dropdown in CH02 — is My BD 2 actually selected?
The mute switch on the CH02 strip in the WebUI.
Step 8 — Iterate fast¶
The single-voice harness is the tightest loop while you’re tuning DSP — it
constructs the rack, picks my2, fires a kick, and reports the peak:
./load-test --machine my2
Re-run after every edit. When the peak / shape looks right, jump to the simulator for the actual audible test.
When you’re satisfied, build the firmware too:
cd ../..
. ~/esp/esp-idf/export.sh
idf.py build
If the firmware build is OK, your voice is real — flashing the TBD-16 will give you the same sound from the device.
Step 9 — Commit (or roll back)¶
What changed (after Step 3 + the DSP edits):
components/ctagSoundProcessor/rack/RackMy2.hpp (new)
components/ctagSoundProcessor/rack/RackMy2.cpp (new)
components/ctagSoundProcessor/ctagSoundProcessorGrooveBoxRack.hpp (+2 lines)
components/ctagSoundProcessor/ctagSoundProcessorGrooveBoxRack.cpp (+5 lines)
sdcard_image/data/synthdefinitions.json (+1 machine entry, +id in track 1's machines list)
sdcard_image/data/sp/mui-GrooveBoxRack.json (+1 param group)
sdcard_image/data/sp/mp-GrooveBoxRack.json (+2 preset defaults)
generators/rack-my2.json (new — keep this for re-running rackgen)
If you want to roll back, every file rackgen.js -i touched has a .bak
next to it. Restore them and drop the new files:
for f in $(git status --porcelain | awk '/^\s*M / {print $2}'); do
[ -f "$f.bak" ] && mv "$f.bak" "$f"
done
rm components/ctagSoundProcessor/rack/RackMy2.{hpp,cpp}
Then git status -s should print nothing.
See also¶
Writing a Machine — the reference: every parameter macro, the voice-registry helpers, the channel-mixer surface, every trigger / noteOn / noteOff contract detail.
Desktop Simulator —
-o/--sromflags, the/ctrlpage, per-OS dependency installs, troubleshooting common issues.Machines catalogue — the 17 Machines that ship today, with their internal id, the track they live on, and a link to the user-facing page.
Quickstart — the one-page intro to the whole TBD project, including the legacy-plugin vs Machine distinction and the firmware build path.