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 in knowYourself().

  • 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:

  1. Open sdcard_image/data/spm-config.json.

  2. Delete the "availableProcessors": [ ... ], block.

  3. 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 by RefreshDataStructure().

  • 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 indices

  • Bank files (e.g. def_smp.json, def_wt.json) — Arrays of sample entries with filename, path, nsamples, and optional offset

See helpers/ctagSampleRomModel for the full data model implementation.