Adding New Backends to gen_dsp¶
This guide explains how to add support for a new audio platform (e.g., SuperCollider, VCV Rack, LV2) to gen_dsp.
Overview¶
gen_dsp uses a platform registry system that makes adding new backends straightforward. Each platform is a Python class that implements the Platform interface, plus a set of C++ template files for the actual wrapper code.
Required components:
- Platform class (Python) - handles project generation and build orchestration
- C++ wrapper templates - the actual code that wraps gen~ exports
- Build system configuration - Makefile, CMakeLists.txt, or similar
- Registry entry - one line to register the new platform
Step 1: Understand the Platform Interface¶
All platforms extend the Platform abstract base class in src/gen_dsp/platforms/base.py:
class Platform(ABC):
# Platform identifier (e.g., 'pd', 'max', 'supercollider')
name: str = "base"
# File extension for built externals (can be a @property for OS-dependent)
extension: str = ""
# Version string (inherited, typically don't override)
GENEXT_VERSION = "0.8.0"
@abstractmethod
def generate_project(self, export_info, output_dir, lib_name, buffers) -> None:
"""Copy templates and generate config files."""
pass
@abstractmethod
def build(self, project_dir, clean=False, verbose=False) -> BuildResult:
"""Invoke the build system."""
pass
@abstractmethod
def clean(self, project_dir) -> None:
"""Remove build artifacts."""
pass
@abstractmethod
def find_output(self, project_dir) -> Optional[Path]:
"""Locate the compiled external."""
pass
def get_build_instructions(self) -> list[str]:
"""Return CLI commands shown to user after project generation."""
return ["# Build instructions not available"]
The base class also provides utility methods you can use:
generate_buffer_header()- generates gen_buffer.h from templaterun_command()- runs subprocess with optional output streaming
Step 2: Create the Platform Class¶
Create a new file src/gen_dsp/platforms/yourplatform.py:
"""
YourPlatform implementation.
"""
import platform as sys_platform
import shutil
from pathlib import Path
from string import Template
from typing import Optional
from gen_dsp.core.builder import BuildResult
from gen_dsp.core.parser import ExportInfo
from gen_dsp.errors import BuildError, ProjectError
from gen_dsp.platforms.base import Platform
from gen_dsp.templates import get_templates_dir
class YourPlatform(Platform):
"""YourPlatform implementation."""
name = "yourplatform" # Used in CLI: gen-dsp <source> -p yourplatform
@property
def extension(self) -> str:
"""Get the file extension for the current OS."""
system = sys_platform.system().lower()
if system == "darwin":
return ".yourext"
elif system == "linux":
return ".so"
elif system == "windows":
return ".dll"
return ".so"
def get_build_instructions(self) -> list[str]:
"""Return build commands shown to user."""
return [
"make", # or cmake, scons, etc.
]
def generate_project(
self,
export_info: ExportInfo,
output_dir: Path,
lib_name: str,
buffers: list[str],
) -> None:
"""Generate project files."""
templates_dir = get_templates_dir() / "yourplatform"
if not templates_dir.is_dir():
raise ProjectError(f"Templates not found at {templates_dir}")
# Copy static C++ files
static_files = [
"gen_ext_yourplatform.cpp",
"gen_ext_common_yourplatform.h",
# ... other files
]
for filename in static_files:
src = templates_dir / filename
if src.exists():
shutil.copy2(src, output_dir / filename)
# Generate build config (Makefile, CMakeLists.txt, etc.)
self._generate_build_config(
templates_dir / "Makefile.template", # or CMakeLists.txt.template
output_dir / "Makefile",
export_info.name,
lib_name,
)
# Generate gen_buffer.h using base class method
self.generate_buffer_header(
templates_dir / "gen_buffer.h.template",
output_dir / "gen_buffer.h",
buffers,
header_comment="Buffer configuration for YourPlatform wrapper",
)
def _generate_build_config(
self,
template_path: Path,
output_path: Path,
gen_name: str,
lib_name: str,
) -> None:
"""Generate build configuration from template."""
if not template_path.exists():
raise ProjectError(f"Build template not found: {template_path}")
template_content = template_path.read_text()
template = Template(template_content)
content = template.safe_substitute(
gen_name=gen_name,
lib_name=lib_name,
genext_version=self.GENEXT_VERSION,
)
output_path.write_text(content)
def build(
self,
project_dir: Path,
clean: bool = False,
verbose: bool = False,
) -> BuildResult:
"""Build the external."""
# Check for required files
makefile = project_dir / "Makefile"
if not makefile.exists():
raise BuildError(f"Makefile not found in {project_dir}")
# Clean if requested
if clean:
self.run_command(["make", "clean"], project_dir)
# Build using base class run_command helper
result = self.run_command(["make"], project_dir, verbose=verbose)
# Find output
output_file = self.find_output(project_dir)
return BuildResult(
success=result.returncode == 0,
platform=self.name,
output_file=output_file,
stdout=result.stdout,
stderr=result.stderr,
return_code=result.returncode,
)
def clean(self, project_dir: Path) -> None:
"""Clean build artifacts."""
self.run_command(["make", "clean"], project_dir)
def find_output(self, project_dir: Path) -> Optional[Path]:
"""Find the built external."""
for f in project_dir.glob(f"*{self.extension}"):
return f
return None
Step 3: Create C++ Templates¶
Create directory src/gen_dsp/templates/yourplatform/ with:
Required Files¶
The ChucK and AudioUnit backends use a header isolation pattern that prevents genlib headers from conflicting with platform headers. This is the recommended approach. You need these files:
_ext_yourplatform.h- Bridge header declaring wrapper functions via an opaqueGenState*_ext_yourplatform.cpp- Genlib-side implementation (includes genlib headers, NOT platform headers)gen_ext_yourplatform.cpp- Platform-side implementation (includes platform headers, NOT genlib headers)gen_ext_common_yourplatform.h- Shared macros (WRAPPER_NAMESPACE, STR, buffer config)yourplatform_buffer.h- Buffer class extendingDataInterface<t_sample>(genlib-side)gen_buffer.h.template- Buffer configuration template:
// Buffer configuration for gen_dsp wrapper
// Auto-generated by gen-dsp
#define WRAPPER_BUFFER_COUNT $buffer_count
$buffer_definitions
- Build configuration template (e.g.,
Makefile.templateorCMakeLists.txt.template)
Header Isolation Pattern¶
genlib headers (genlib_ops.h in particular) define inline exp2(float) and trunc(float) that conflict with <cmath> on modern compilers. The solution is to split the wrapper into two compilation units:
_ext_yourplatform.cpp(genlib side) -- includes genlib headers, definesWIN32andGENLIB_NO_DENORM_TESTto suppress conflicts, includes the gen~ exported.cpp, implements wrapper functionsgen_ext_yourplatform.cpp(platform side) -- includes only platform headers and_ext_yourplatform.h, calls wrapper functions through the opaqueGenState*interface
The bridge header (_ext_yourplatform.h) declares all wrapper functions in a namespace without including any genlib types:
typedef void GenState; // opaque handle for CommonState
namespace WRAPPER_NAMESPACE {
GenState* wrapper_create(float sr, long bs);
void wrapper_destroy(GenState* state);
void wrapper_reset(GenState* state);
void wrapper_perform(GenState* state, float** ins, long numins,
float** outs, long numouts, long n);
int wrapper_num_inputs();
int wrapper_num_outputs();
int wrapper_num_params();
const char* wrapper_param_name(GenState* state, int index);
void wrapper_set_param(GenState* state, int index, float value);
float wrapper_get_param(GenState* state, int index);
// ... etc
}
See templates/chuck/ or templates/au/ for complete working examples.
Platform-Side Structure¶
Your platform-side file (gen_ext_yourplatform.cpp) uses the wrapper functions:
#include "your_platform_sdk.h" // platform headers only
#include "gen_ext_common_yourplatform.h"
#include "_ext_yourplatform.h"
using namespace WRAPPER_NAMESPACE;
// Platform-specific object structure
struct YourWrapper {
GenState* gen_state;
float** in_buffers;
float** out_buffers;
int num_inputs;
int num_outputs;
};
// Object creation
YourWrapper* yourwrapper_new(float samplerate) {
YourWrapper* x = /* allocate */;
x->num_inputs = wrapper_num_inputs();
x->num_outputs = wrapper_num_outputs();
x->gen_state = wrapper_create(samplerate, /* blocksize */);
// allocate in_buffers, out_buffers ...
return x;
}
// DSP perform routine
void yourwrapper_perform(YourWrapper* x, long nframes) {
// Copy platform audio into x->in_buffers
wrapper_perform(x->gen_state, x->in_buffers, x->num_inputs,
x->out_buffers, x->num_outputs, nframes);
// Copy x->out_buffers back to platform audio
}
// Parameter handling
void yourwrapper_param(YourWrapper* x, const char* name, float value) {
int n = wrapper_num_params();
for (int i = 0; i < n; i++) {
if (strcmp(wrapper_param_name(x->gen_state, i), name) == 0) {
wrapper_set_param(x->gen_state, i, value);
break;
}
}
}
Float32 vs Float64¶
If your platform uses 32-bit float audio (most do), define GENLIB_USE_FLOAT32 so that t_sample = float. This is what ChucK and AudioUnit do. Only Max/MSP uses 64-bit double (no GENLIB_USE_FLOAT32).
Step 4: Add Template Accessor¶
Edit src/gen_dsp/templates/__init__.py to add a function for your templates:
def get_yourplatform_templates_dir() -> Path:
"""Get the path to YourPlatform templates."""
return get_templates_dir() / "yourplatform"
Step 5: Register the Platform¶
Edit src/gen_dsp/platforms/__init__.py:
from gen_dsp.platforms.yourplatform import YourPlatform
PLATFORM_REGISTRY: dict[str, Type[Platform]] = {
"pd": PureDataPlatform,
"max": MaxPlatform,
"chuck": ChuckPlatform,
"au": AudioUnitPlatform,
"clap": ClapPlatform,
"vst3": Vst3Platform,
"yourplatform": YourPlatform, # Add this line
}
__all__ = [
# ...
"YourPlatform", # Add this line
]
That's it! The CLI will automatically pick up the new platform:
gen-dsp <source> -p yourplatformwill workgen-dsp build -p yourplatformwill work- Help text will show the new platform option
Step 6: Add Tests¶
Create tests/test_yourplatform.py:
"""Tests for YourPlatform."""
import pytest
from gen_dsp.platforms import get_platform
from gen_dsp.platforms.yourplatform import YourPlatform
class TestYourPlatform:
def test_platform_registered(self):
"""Test platform is in registry."""
platform = get_platform("yourplatform")
assert isinstance(platform, YourPlatform)
def test_extension(self):
"""Test extension is valid."""
platform = YourPlatform()
assert platform.extension in [".yourext", ".so", ".dll"]
def test_build_instructions(self):
"""Test build instructions are provided."""
platform = YourPlatform()
instructions = platform.get_build_instructions()
assert len(instructions) > 0
Reference: ExportInfo¶
The export_info parameter passed to generate_project() contains:
@dataclass
class ExportInfo:
name: str # Name of the gen~ export (e.g., "gen_exported")
path: Path # Path to the export directory
num_inputs: int # Number of signal inputs
num_outputs: int # Number of signal outputs
num_params: int # Number of parameters
buffers: list[str] # Detected buffer names
has_exp2f_issue: bool # Whether exp2f patch is needed
cpp_path: Path # Path to the .cpp file
h_path: Path # Path to the .h file
Reference: BuildResult¶
The build() method must return a BuildResult:
@dataclass
class BuildResult:
success: bool # True if build succeeded
platform: str # Platform name
output_file: Path|None # Path to built external, or None
stdout: str # Captured stdout
stderr: str # Captured stderr
return_code: int # Process return code
Existing Backends as Reference¶
| Backend | Build system | Signal type | Key pattern |
|---|---|---|---|
| PureData | make (pd-lib-builder) | float32 | Direct genlib include (no isolation) |
| Max/MSP | CMake (max-sdk-base) | float64 | Header isolation, CMake bundle |
| ChucK | make | float32 | Header isolation, frame-by-frame deinterleave |
| AudioUnit | CMake | float32 | Header isolation, AudioComponentPlugInInterface Lookup dispatch |
| CLAP | CMake + FetchContent | float32 | Header isolation, zero-copy process, cross-platform |
| VST3 | CMake + FetchContent | float32 | Header isolation, zero-copy process, SingleComponentEffect, FUID |
The CLAP backend is the recommended reference implementation for new backends. It demonstrates the cleanest version of the header isolation pattern, CMake-based builds with external dependency fetching via FetchContent, and a zero-copy audio process path. Start here when adding a new platform.
CLAP as a Reference Implementation¶
The CLAP backend (platforms/clap.py + templates/clap/) is a good starting point because:
Simple structure -- 7 template files with clear separation of concerns:
| File | Role |
|---|---|
gen_ext_common_clap.h |
Shared macros (WRAPPER_NAMESPACE, STR(), includes gen_buffer.h) |
_ext_clap.h |
Bridge header: opaque GenState*, wrapper function declarations |
_ext_clap.cpp |
Genlib-side: WIN32/GENLIB_NO_DENORM_TEST workaround, buffer instances, wrapper implementations |
clap_buffer.h |
Buffer class extending DataInterface<t_sample> |
gen_buffer.h.template |
Buffer config template ($buffer_count, $buffer_definitions) |
gen_ext_clap.cpp |
Platform-side: plugin state, extensions, process, factory, entry point |
CMakeLists.txt.template |
Build config with FetchContent for external headers |
Zero-copy process -- CLAP's non-interleaved data32[channel][sample] layout matches gen~'s float** exactly. The process function passes CLAP audio buffers directly to wrapper_perform() with no intermediate allocation:
// From gen_ext_clap.cpp -- entire audio path
float** ins = (plug->numInputs > 0 && process->audio_inputs_count > 0)
? process->audio_inputs[0].data32 : nullptr;
float** outs = process->audio_outputs[0].data32;
wrapper_perform(plug->genState, ins, plug->numInputs,
outs, plug->numOutputs, (long)nframes);
If your target platform also uses non-interleaved float**, you can achieve the same zero-copy path. If it uses interleaved audio, see the ChucK backend for a deinterleave approach.
FetchContent for external dependencies -- instead of vendoring headers, the CMake template uses FetchContent_Declare to download them at configure time. This pattern is useful when the target SDK is too large to vendor or has its own CMake build system (e.g., VST3):
FetchContent_Declare(
clap
GIT_REPOSITORY https://github.com/free-audio/clap.git
GIT_TAG 1.2.2
)
FetchContent_MakeAvailable(clap)
target_link_libraries(${PROJECT_NAME} PRIVATE clap)
Auto-detection of plugin type -- uses #if CLAP_NUM_INPUTS > 0 in the C++ template to choose between effect and instrument features at compile time, avoiding runtime branching.
C++ gotcha to be aware of -- static const struct forward declarations are definitions in C++, not just declarations. If you forward-declare static const SomeStruct s_foo; and later define static const SomeStruct s_foo = { ... };, you get a redefinition error. The CLAP backend avoids this by placing struct definitions (e.g., extension vtables) before the functions that reference them, eliminating the need for forward declarations.
Platform class -- ClapPlatform in platforms/clap.py is a minimal ~170-line class. The generate_project() method copies static files, runs template substitution on CMakeLists.txt.template, generates gen_buffer.h, and creates the build directory. Copy this file and adjust for your platform.
Platform-Specific Considerations¶
VST3 (implemented)¶
- Uses CMake with FetchContent for the VST3 SDK (pinned to
v3.7.9_build_61) SingleComponentEffect(combined processor+controller) -- simplest VST3 plugin structuresmtg_add_vst3plugin()macro handles.vst3bundle directory structure- Must compile
vstsinglecomponenteffect.cppand platform entry points explicitly - Deterministic FUID from MD5 of
"com.gen-dsp.vst3.<lib_name>" - Uses
GSTR()macro instead ofSTR()to avoid SDKSTR16collision - GPL3/proprietary dual license -- check compatibility
- See
src/gen_dsp/templates/vst3/andsrc/gen_dsp/platforms/vst3.py
SuperCollider UGens¶
- Uses scons or CMake
- UGen interface differs from genlib - may need adapter layer
- Single-sample or block processing modes
- Buffer access via
SndBuf
VCV Rack¶
- Uses CMake with Rack SDK
- Sample-by-sample processing
- Different parameter model (knobs, CV inputs)
LV2¶
- Uses meson or CMake
- Standardized plugin format with TTL manifests
- Port-based I/O model
JUCE (VST/AU/AAX)¶
- Uses CMake or Projucer
- Most complex but broadest reach
- Consider as a "meta-platform" generating multiple formats
- Note: the AU and CLAP backends already cover AUv2 and CLAP natively without JUCE
Embedded (Bela, Daisy)¶
- Cross-compilation considerations
- Fixed buffer sizes may be required
- Limited memory/CPU constraints
Checklist¶
- [ ] Platform class implements all abstract methods
- [ ] Templates directory created with all required files
- [ ] C++ wrapper compiles and runs
- [ ] Platform registered in
PLATFORM_REGISTRY - [ ] Template accessor added to
templates/__init__.py - [ ] Tests added and passing
- [ ]
get_build_instructions()returns useful commands - [ ] Documentation updated (README, CHANGELOG)