Creating a Plugin¶
This guide walks you through creating a new TBD plugin from scratch.
Prerequisites¶
Node.js installed (for the code generator).
The TBD repository cloned with submodules (see Plugin Development Setup).
ESP-IDF environment configured.
Step 1: Define the UI¶
Create your MUI file by copying the template:
cp generators/mui-template.json sdcard_image/data/sp/mui-MyPlug.json
Edit mui-MyPlug.json to define your plugin’s parameters:
{
"id": "MyPlug",
"isStereo": false,
"name": "My Plugin",
"hint": "A demo plugin",
"params": [
{
"id": "gain",
"name": "Gain",
"type": "int",
"min": 0,
"max": 4095
},
{
"id": "bypass",
"name": "Bypass",
"type": "bool"
}
]
}
Step 2: Generate Code¶
Run the code generator from the generators/ folder:
cd generators
node generator.js mui-MyPlug.json
This creates three files in the current directory:
ctagSoundProcessorMyPlug.hpp— Header with atomic parameter variables.ctagSoundProcessorMyPlug.cpp— Source with parameter mapping inknowYourself().mp-MyPlug.json— Default preset file.
Step 3: Move Files¶
Move the generated files to the correct directories:
mv ctagSoundProcessorMyPlug.hpp ../components/ctagSoundProcessor/
mv ctagSoundProcessorMyPlug.cpp ../components/ctagSoundProcessor/
mv mp-MyPlug.json ../sdcard_image/data/sp/
Step 4: Implement Process()¶
Open components/ctagSoundProcessor/ctagSoundProcessorMyPlug.cpp and implement
your audio processing:
#include "ctagSoundProcessorMyPlug.hpp"
#include <cmath>
using namespace CTAG::SP;
void ctagSoundProcessorMyPlug::Process(const ProcessData &data) {
// Read parameters (with CV/trigger modulation support)
MK_FLT_PAR_ABS(fGain, gain, 4095.f, 1.f);
MK_BOOL_PAR(bBypass, bypass);
if (bBypass) {
// Output silence
for (int i = 0; i < bufSz; i++) {
data.buf[i * 2 + processCh] = 0.f;
}
return;
}
// Process audio
for (int i = 0; i < bufSz; i++) {
float sample = data.buf[i * 2 + processCh];
data.buf[i * 2 + processCh] = sample * fGain;
}
}
Step 5: Register the Plugin¶
The plugin must be registered in the factory. Reset the plugin cache so the system discovers your new plugin on the next boot:
Open
sdcard_image/data/spm-config.json.Delete the
"availableProcessors": [ ... ],block.Save the file.
Step 6: Build and Test¶
Build the firmware and flash it:
idf.py build
idf.py flash monitor
Or test with the simulator:
cd simulator && mkdir -p build && cd build
cmake .. && make
./tbd-sim
Your plugin should now appear in the Web UI plugin selector.
Modifying Parameters Later¶
If you add or remove parameters from your MUI file after the initial creation, use the in-place update mode of the generator:
cd generators
node generator.js MyPlug -i
Warning
The -i flag overwrites the preset file mp-MyPlug.json.
Back up any custom presets before running this.
Plugin Design Patterns¶
Self-Contained / Monolithic Plugins¶
A plugin can include its own sequencer, arpeggiator, or any internal logic.
The Process() method has full control over the audio buffer. Use internal
state variables to maintain sequencer position, envelope state, etc.
DSP-Only Plugins¶
Simpler plugins focus exclusively on audio processing (filters, effects,
oscillators) and rely on external control from the RP2350 firmware via
MIDI or the controlData pointer in ProcessData.
Using Samples¶
Plugins can access audio samples from the SD card via the ctagSampleRom
helper class. At boot, the system reads WAV files listed in the active sample
and wavetable banks, loads them into PSRAM, and exposes them as indexed slices.
#include "helpers/ctagSampleRom.hpp"
// In your Init() method:
sampleRom = std::make_unique<ctagSampleRom>();
// In your Process() method:
uint32_t nSlices = sampleRom->GetNumberSlices();
uint32_t sliceSize = sampleRom->GetSliceSize(sliceIndex);
sampleRom->ReadSliceAsFloat(buffer, sliceIndex, offset, nSamples);
Key concepts:
Wavetable slices occupy indices 0 to
GetFirstNonWaveTableSlice() - 1. Each wavetable bank file contains 64 wavetables × 256 samples.Sample slices start at
GetFirstNonWaveTableSlice(). Each slice is one WAV file from the active sample bank.Bank switching is done via
SetActiveWaveTableBank()/SetActiveSampleBank()followed byRefreshDataStructure().Sample data is stored as 16-bit integer in PSRAM. Use
ReadSliceAsFloat()to get normalized float output.Total PSRAM allocation for samples is configured at build time (
CONFIG_MAX_ALLOC_BYTES_PSRAM_SAMPLE_DATA).
See ctagSoundProcessorRompler and ctagSoundProcessorWTOsc for
real-world usage examples.
SD Card Sample Layout¶
Samples live in /sdcard/tbdsamples/ on the P4 SD card. The structure is
defined by JSON descriptor files:
sample_rom.json— Master index with bank lists and active bank indicesBank files (e.g.
def_smp.json,def_wt.json) — Arrays of sample entries withfilename,path,nsamples, and optionaloffset
See helpers/ctagSampleRomModel for the full data model implementation.