Percussa SSP Backend Assessment¶
Assessment of the Percussa SSP SDK as a gen-dsp backend. Mirrors the structure of move-everything.md.
What is the Percussa SSP?¶
The Percussa SSP (Super Signal Processor) is a high-end, ARM-Linux-based Eurorack module / standalone: a programmable multi-channel DSP host with a colour display, four encoders, and eight soft keys. The SDK lets third parties build native modules that run on the device as shared libraries.
Plugin API (Percussa.h, API 3.5)¶
SSP modules are .so shared libraries that export three C functions and
implement a small C++ interface in namespace Percussa::SSP:
- C exports:
createDescriptor() -> PluginDescriptor*,createInstance() -> PluginInterface*,getApiVersion(unsigned& major, unsigned& minor). PluginInterface(audio processor):prepare(double sampleRate, int samplesPerBlock)-- init (~ gen~create).process(float** channelData, int numChannels, int numSamples)-- the DSP callback. Samples are float32, planar (float**).buttonPressed(int n, bool)-- soft keys (0-7), navigation (8-11), shift (12-13).encoderPressed(int n, bool)/encoderTurned(int n, int)-- encoders 0-3.inputEnabled(int n, bool)/outputEnabled(int n, bool)-- per-channel enable.getState(void**, size_t*)/setState(void*, size_t)-- preset save/load.getEditor() -> PluginEditorInterface*.PluginEditorInterface(display):renderToImage(unsigned char*, w, h)(BGRA pixel buffer),draw(w, h)(OpenGL ES),frameStart(),visibilityChanged(bool).PluginDescriptor(metadata):name,descriptiveName,manufacturerName,version,uid(matches the VST uid for preset compatibility),inputChannelNames/outputChannelNames(vector<string>),colour(BGRA).
Example module (examples/api/test): a TestPluginInterface : Percussa::SSP::PluginInterface
declaring 2 in / 2 out ("In 1/2", "Out 1/2"), uid 0x54455354.
Channel layout (resolved). channelData is a JUCE-style in-place buffer:
TheTechnobear's bridge wraps it directly as
AudioSampleBuffer buffer(channelData, numChannels, numSamples) and calls
processBlock with no copy. So the raw float** pointers are usable as-is; gen-dsp
would map them straight to genlib's t_sample** ins/outs. The exact input/output
split (separate numIn + numOut pointers vs. an in-place max(in,out) buffer) is
the one detail to confirm at runtime on the device.
Build & toolchain¶
- CMake with a cross-compilation toolchain file
xcSSP.cmake. The SSP runs Rockchip ARM 32-bit hard-float Linux (Buildroot); target triplearm-rockchip-linux-gnueabihf. Output is a.so. - The toolchain + sysroot is a free, public tarball (a Buildroot SDK), not
device- or account-tied:
https://sw13072022.s3.us-west-1.amazonaws.com/arm-rockchip-linux-gnueabihf_sdk-buildroot.tar.gz. It is extracted under~/buildroot/...and the build points at it viaexport BUILDROOT=.... Because the URL is public, gen-dsp could auto-fetch and cache it the way it already does for libDaisy / Circle / the Rack SDK. - The official SDK build also references Steinberg's VST3 SDK (
export VSTSDK=...), but that is for theexamples/vst/wrappers; the nativeexamples/api/path -- the one gen-dsp would target -- should not need it (confirm). - Dev hosts: macOS / Linux.
- Build:
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=../xcSSP.cmake ..thencmake --build . -- -j4.
What xcSSP.cmake actually sets¶
CMAKE_SYSTEM_NAME Linux,CMAKE_SYSTEM_PROCESSOR armv7l, triplearm-linux-gnueabihf.- Compiler is host Clang, not the Buildroot GCC.
CMAKE_C/CXX_COMPILER=${TOOLSROOT}/clang(++)withCMAKE_C/CXX_COMPILER_TARGET=arm-linux-gnueabihf.TOOLSROOTdefaults to Homebrew LLVM on macOS (/opt/homebrew/opt/llvm/binor/usr/local/opt/llvm/bin) and/usr/binon Linux. So the host needs LLVM/Clang; the Buildroot tree supplies only the sysroot + GCC 8.4.0 libstdc++ (clang is pointed at its C++ headers via explicit-I${BUILDROOT}/.../include/c++/8.4.0[...]). CMAKE_SYSROOT = ${BUILDROOT}/arm-rockchip-linux-gnueabihf/sysroot;GCCROOT = ${BUILDROOT}/lib/gcc/arm-rockchip-linux-gnueabihf/8.4.0.- CPU flags:
-mcpu=cortex-a17 -mfloat-abi=hard -mfpu=neon-vfpv4-- the SSP is a Cortex-A17 SoC (Rockchip RK3288-class), NEON / hard-float. - Link flags add
-L/-B/-Wl,-rpath-linkfor${CMAKE_SYSROOT}/liband${GCCROOT}, applied to exe/module/shared linker flags. - Env overrides:
BUILDROOT(sysroot root) andTOOLSROOT(clang location).
This is reproducible by gen-dsp directly: emit a toolchain file that runs the host Clang
with --target=arm-linux-gnueabihf, the cached Buildroot sysroot, the libstdc++ 8.4.0
include paths, and the cortex-a17 NEON flags -- no Percussa xcSSP.cmake needed at
generation time. A doctor check would verify Clang and the Buildroot sysroot are
present.
Constraints vs. gen-dsp typical backends¶
- float32 planar matches gen-dsp's
GENLIB_USE_FLOAT32(t_sample = float) and gen~'sfloat**perform signature -- essentially a direct mapping, no int16 or interleave conversion (unlike Move). - No float-parameter API. Control is via 4 encoders + 8 soft keys, not a CLAP/VST
parameter list. gen~ params must be mapped onto the encoders with paging (as the
Daisy/Circle backends map params to hardware knobs), with
getState/setStateserializing the current values for presets. - Cross-compile sysroot -- but auto-fetchable. Unlike a device-tied SDK, the
arm-rockchip-linux-gnueabihfBuildroot toolchain is a public tarball, so gen-dsp can download and cache it automatically (as it does libDaisy / Circle / Rack SDK) instead of requiring a user-set path. ABUILDROOToverride (cf.RACK_DIR) can still point at an existing install. - AGPL-3.0 SDK.
Percussa.his AGPL-3.0. Generated glue that includes it is AGPL-encumbered, and the resulting module is AGPL. gen-dsp itself is MIT; the SSP-specific template/glue files would carry the AGPL implications (the genlib and user DSP are separate TUs under the header-isolation pattern). Worth flagging to users, not a blocker. - Optional display.
renderToImage/drawcan be stubbed or used to render a minimal parameter readout; not required for audio to work.
Feasibility: Yes -- a good fit¶
The shared-library-plus-descriptor/instance model is very close to gen-dsp's existing
CLAP / LV2 backends, and prepare/process map almost 1:1 onto gen~'s
create/perform. float32 planar audio removes the format-conversion friction that
complicates the Move backend. The user owns the hardware, so the build and audio path
can be verified on-device.
Implementation requirements¶
- Platform key
"ssp";platforms/ssp.pyplus atemplates/ssp/set. - Header-isolation bridge
_ext_ssp.cpp/_ext_ssp.h: the SSP-side.cppincludes onlyPercussa.h; the genlib-side_ext*.cppincludes only genlib; they meet at an opaqueGenState*C interface (the established gen-dsp pattern). process()bridge: slicechannelDatainto gen~ inputs ([0 .. numIn)) and outputs ([numIn .. numIn+numOut)) -- exact layout to confirm on device -- and call the genlib perform.- Parameter -> encoder mapping with paging, plus
getState/setState(de)serialization of param values; mapPluginDescriptor.inputChannelNames/outputChannelNamesfrom the manifest I/O. - CMakeLists wired to a
xcSSP.cmake-style toolchain. gen-dsp can auto-download and cache the public Buildroot toolchain tarball (like libDaisy / Circle), with aBUILDROOTenv override for an existing install. - Optional minimal
PluginEditorInterfacerendering param names/values.
Reference implementation (TheTechnobear)¶
TheTechnobear/SSP is a mature, ~39-module
collection that proves the path end to end. Its technobear/common/ provides a
reusable framework:
SSPApiProcessor.{h,cpp}-- bridgesPercussa::SSP::PluginInterfaceto a JUCEAudioProcessor(AudioSampleBuffer buffer(channelData, numChannels, numSamples); processor_->processBlock(buffer, midi)).SSPExApi.h(SSPExtendedApi) -- an extended parameter model layered on the raw SDK:numberOfParameters(),parameterDesc(idx, ...),parameterValue(idx), withbuttonPressed/encoderTurnedtranslated to parameter updates and paged across the four encoders. This is the cleanest reference for gen-dsp's param->encoder mapping.- Editor base (
SSPApiEditor.{h,cpp}) for the display.
Two implementation paths for gen-dsp:
- Native (recommended) -- generate a module that implements
Percussa::SSP::PluginInterfacedirectly against the officialPercussa.h, bridgingprocess()to genlib with no JUCE. Matches gen-dsp's other backends (minimal wrapper, no heavy framework dependency) and the officialexamples/api/test. - JUCE-based -- reuse TheTechnobear's
SSPApiProcessor/SSPExtendedApi. More machinery (JUCE + madronalib submodules) but a ready-made parameter/encoder/editor layer. Heavier; only worth it if the display/param UX is a priority.
Either way, TheTechnobear's SSPExtendedApi parameter abstraction is the model to copy
for mapping gen~ params onto the encoders.
Implementation plan¶
A concrete, phased plan to add an "ssp" backend, native (no JUCE), following the
existing gen-dsp backend anatomy (header isolation + a per-platform bridge over the
shared wrapper_* C interface, as in the Web Audio backend).
Files to add¶
src/gen_dsp/platforms/ssp.py--SspPlatform(Platform); register"ssp"inplatforms/__init__.pyPLATFORM_REGISTRY. Implementsgenerate_project,build,clean,find_output,get_build_instructions,extension(.so),description,build_system("CMake (clang cross)"), andlist_boards()->[].src/gen_dsp/templates/ssp/:_ext_ssp.cpp(static) -- genlib side: includes genlib + the exported gen~ code, defines thewrapper_*functions inWRAPPER_NAMESPACE. Mirrors_ext_webaudio.cpp(drop the buffer section unless/until SSP buffers are wired).gen_ext_common_ssp.h(static) -- theWRAPPER_NAMESPACEmacros._ext_ssp.h-- generated from the sharedgen_ext_h.template(opaqueGenState*C interface). Reused unchanged.ssp_plugin.cpp.template-- the only genuinely new code: implementsPercussa::SSP::PluginInterface,createDescriptor/createInstance/getApiVersion, theprocess()bridge, the param->encoder layer, andgetState/setState.CMakeLists.txt.template-- builds the.so; pulls in the generated toolchain.ssp_toolchain.cmake.template-- emitted per project (see below).src/gen_dsp/core/doctor.py-- add ansspentry checking forclang/clang++and the Buildroot sysroot.
Build & toolchain strategy¶
- Fetch the SDK like Daisy/Circle fetch theirs, but as a tarball, not a clone: a
helper (cf.
daisy._resolve_libdaisy_dir) downloadsarm-rockchip-linux-gnueabihf_sdk-buildroot.tar.gzinto the shared cache (core/cache.get_cache_dir()), extracts once, and returns the root. ABUILDROOTenv var overrides to an existing install. Pin the URL + a version/sha constant. - Emit our own toolchain file rather than depend on Percussa's
xcSSP.cmake:ssp_toolchain.cmakesetsCMAKE_SYSTEM_NAME Linux,SYSTEM_PROCESSOR armv7l,CMAKE_C/CXX_COMPILER= hostclang/clang++with--target=arm-linux-gnueabihf,CMAKE_SYSROOT= cached Buildroot sysroot, the libstdc++ 8.4.0 include paths,-mcpu=cortex-a17 -mfloat-abi=hard -mfpu=neon-vfpv4, and the sysroot/GCCROOT link flags. (ReproducesxcSSP.cmakefrom values we now know.) build()resolves the SDK, writes the toolchain into the project, then runscmake -DCMAKE_TOOLCHAIN_FILE=ssp_toolchain.cmake -B build+cmake --build build. This makessspthe first CMake cross-compile backend (existing cross-compile backends -- Daisy, Circle -- are Make-based).
process() bridge¶
The SSP gives process(float** channelData, int numChannels, int numSamples). genlib's
perform(state, t_sample** ins, long nins, t_sample** outs, long nouts, long n) takes the
same planar float**. Bridge: pass channelData[0 .. num_inputs) as ins and
channelData[num_inputs .. num_inputs+num_outputs) as outs (the inputs-then-outputs
assumption). Guard with a one-time check of numChannels vs num_inputs+num_outputs;
keep an in-place fallback (outs overlap ins) behind a clearly marked switch, to flip
after the on-device confirmation. genlib already tolerates t_sample = float.
Parameter -> encoder layer (model: SSPExtendedApi)¶
- The manifest's
ParamInfo[](name/min/max/default) drives a fixed param table. - Four encoders address a page of four params;
encoderTurned(n, delta)updates parampage*4 + nbydelta * (max-min) * step, clamped;encoderPressedresets to default. - Soft keys / navigation buttons change page; the editor's
renderToImagedraws the current page's fourname: valuerows (BGRA). The editor can be a stub initially. getState/setStateserialize the param-value array (+ current page) to a blob for presets. (gen~ MIDI-to-CV is N/A for SSP -- no MIDI in the callback; treat all I/O as audio/CV and disable the MIDI path for this backend.)
Descriptor¶
createDescriptor() fills name/manufacturerName/version from the project config,
a deterministic uid (e.g. hashed from the name), and
inputChannelNames/outputChannelNames sized from manifest.num_inputs/num_outputs
(generic "In N"/"Out N", or gen~ I/O names when present).
Phasing¶
- Scaffold + generation (verifiable here, in CI): platform + templates + registry;
generate_projectemits all files. Always-run generation tests (cf. the webaudio tests): files present, descriptor channel counts match the manifest, toolchain carries the cortex-a17/clang flags, param-table code present. No cross-build needed. - Toolchain + build (verified on the user's device): SDK auto-fetch, toolchain emit,
build()->.so;doctorentry. Build/integration tests gated onclang+BUILDROOT(skipped in CI, like theemccgate). - Param/encoder + display: the
SSPExtendedApi-style layer, paging,getState/setState,renderToImageparam readout. - Polish: confirm the channel split on device, presets, docs + an example, and a
backend page under
docs/backends/.
Decisions / risks¶
- Licensing.
ssp_plugin.cppincludes the AGPL-3.0Percussa.h, so the generated SSP project is AGPL-3.0 (genlib + user DSP remain separate TUs). gen-dsp's generator code stays MIT (it only emits text). Mark the SSP template/bridge files and generated project with an AGPL notice and document it. Not a code blocker. - On-device confirmations before calling it done: the exact in/out channel split;
whether the native
api/path needs the VST3 SDK (thevst/examples do); the module install path on the device. - What can be validated in CI vs. on device. Generation (phase 1) runs everywhere; the cross-build + audio (phases 2-4) require the Cortex-A17 sysroot + clang target and the hardware, so they live behind a tool gate and are validated by the device owner.
Closest analog¶
- Code shape: CLAP / LV2 (a
.soplugin: descriptor + instance factory,prepare/process). - Build & control: Circle / Daisy / VCV Rack (cross-compile against an externally installed SDK; map params onto fixed hardware controls).
Open questions (confirm on device)¶
- The input/output split within
channelData: separatenumIn+numOutpointers, or an in-placemax(numIn, numOut)JUCE buffer? (Wrapping is confirmed in-place; only the bus interpretation remains.) - The SSP's fixed I/O width / maximum channel count.
- Module install path on the device (SD card folder?).
- Whether the native
api/build path needs the VST3 SDK or only thevst/wrappers do. - AGPL-3.0 implications for distributing generated modules.