Compiled Faust devices
MAGDA has two kinds of Faust device:
- Compiled (this skill): the
.dsp is compiled to C++ at build time and statically linked. Fast, ships in the binary. Lives in magda/daw/audio/faust_dsp/*.dsp + magda/daw/audio/plugins/compiled/Magda*CompiledPlugin.{hpp,cpp}.
- Interpreted/JIT (FaustPlugin / FaustInstrumentPlugin): loads
.dsp at runtime. Not this skill.
The pipeline (how a .dsp becomes a device)
faust_dsp/magda_x.dsp
--(CMake: magda_compile_faust_dsp, runs `faust -lang cpp`)-->
cmake-build-debug/compiled_dsps/magda_x.generated.cpp (NOT checked in)
--(#include'd by)-->
plugins/compiled/MagdaXCompiledPlugin.cpp (the host wrapper)
--(custom UI)-->
ui/components/chain/custom_ui/XUI.cpp
- The generated
.cpp is produced by the build — there is no checked-in generated file to hand-edit. Just edit the .dsp and rebuild (make debug); CMake reruns faust. Confirm the result in cmake-build-debug/compiled_dsps/<name>.generated.cpp.
- Registration lives in
magda/daw/audio/CMakeLists.txt via magda_compile_faust_dsp(<dsp> <ClassName> <OUT_VAR>); the generated path is added to target_sources and the wrapper #includes <name>.generated.cpp.
- Validate DSP before building with the
faust-mcp-magda MCP tools (compile_faust, search_faust_libraries, get_faust_library).
Host-slot contract (THE thing to get right)
Each control is a Faust hslider/nentry tagged [idx:N]. N is the host slot index — the stable contract between dsp, wrapper, and UI. The wrapper harvests zones by reading the [idx:N] metadata.
Three places must agree on slot indices:
.dsp — [idx:N] on each param.
MagdaXCompiledPlugin.hpp — kFooSlot = N constants + kHostSlotCount.
MagdaXCompiledPlugin.cpp — hostSlotInfo_[kFooSlot] = {.name, .scale, .minValue, .maxValue, .defaultValue, .choices}.
XUI.{hpp,cpp} — its own kFooSlot constants + kNumParams, label, layout.
Adding a parameter (common case)
Append at the end (idx = old kHostSlotCount) so existing slot indices stay stable — never insert in the middle and renumber.
.dsp: add the hslider/nentry with the next free [idx:N]; wire it into the DSP graph. Reuse-by-copy from a sibling .dsp when matching behaviour (e.g. the drive lerp (1-d)*x + d*(tanh(4x)/tanh(4)) from magda_filter_svf.dsp).
- Wrapper
.hpp: add kFooSlot = N, bump kHostSlotCount. The std::array<…, kHostSlotCount> members grow automatically.
- Wrapper
.cpp: add hostSlotInfo_[kFooSlot] = {…}. Use ParameterScale::Discrete + .choices = {...} for menus, Linear/Logarithmic/FaderDB otherwise.
- UI: add
kFooSlot, bump kNumParams, add a label, place it in resized().
make debug (regenerates the dsp), confirm the param appears in the generated .cpp.
[style:menu{'A':0;'B':1}] in the dsp = a discrete choice param; mirror with .choices in slot info.
Custom UI + linking
- The UI exposes
std::vector<LinkableTextSlider*> getLinkableSliders(); each slider carries its slot via setParamIndex(). DeviceSlotComponent::setupCustomUILinking() wires mod/macro/automation/MIDI-Learn off that list. Any control that must be linkable has to be a LinkableTextSlider in that list.
- To present a slot as something else (e.g. segmented buttons for a menu): keep the hidden
LinkableTextSlider in controls_/getLinkableSliders() (carries value + linking) and drive it from the custom widget. Set selection explicitly for segmented buttons (setToggleState(i==sel)), not via JUCE radio groups (radio exclusivity left two segments lit).
- Theme-font buttons:
setLookAndFeel(&FlatTabButtonLookAndFeel::getInstance()) (flat tab) or SmallButtonLookAndFeel (rounded), both in themes/SmallButtonLookAndFeel.hpp; clear with setLookAndFeel(nullptr) in the destructor.
- Labels/fonts:
FontManager::getInstance().getUIFont(...), colours via DarkTheme::getColour(...).
Reusing a curve view across devices
Curve/response views (e.g. CompiledFilterCurveView) are normally bound to one plugin's slots via a DeviceInfo snapshot. To reuse one in a different device, add a raw-value entry point (setRawState(...)) that sets the target values directly and bypasses the device-snapshot path, plus optional per-instance theming (e.g. setCurveColour). Drive it from the host UI on every relevant param change.
Gotchas
- Generated
.cpp is build output — don't look for it in git; rebuild to refresh.
- Never reorder/insert
[idx:N]; append. (Unreleased features: no migration needed, but stable indices keep diffs sane.)
ba.selectn(n, sel, ...) is strict — every branch runs, so cascading/parallel filter modes is cheap and safe.
- Polyphonic synths use Faust's
mydsp_poly (group=false) — per-voice zones are harvested into voiceZonesBySlot_.
- Build with the
building skill (make debug), never raw faust/cmake.
1---2name: faust-device3description: Work on MAGDA's compiled Faust devices (synths/FX whose DSP is a .dsp file compiled to C++ at build time). Use when adding or changing a parameter on a compiled Faust plugin (e.g. Poly Synth, the filter engines, compiled FX), creating a new compiled Faust device, or wiring its custom UI. Covers the .dsp -> generated.cpp pipeline, the host-slot contract, and UI/linking.4---56# Compiled Faust devices78MAGDA has two kinds of Faust device:9- **Compiled** (this skill): the `.dsp` is compiled to C++ **at build time** and statically linked. Fast, ships in the binary. Lives in `magda/daw/audio/faust_dsp/*.dsp` + `magda/daw/audio/plugins/compiled/Magda*CompiledPlugin.{hpp,cpp}`.10- Interpreted/JIT (FaustPlugin / FaustInstrumentPlugin): loads `.dsp` at runtime. Not this skill.1112## The pipeline (how a .dsp becomes a device)1314```15faust_dsp/magda_x.dsp16 --(CMake: magda_compile_faust_dsp, runs `faust -lang cpp`)-->17cmake-build-debug/compiled_dsps/magda_x.generated.cpp (NOT checked in)18 --(#include'd by)-->19plugins/compiled/MagdaXCompiledPlugin.cpp (the host wrapper)20 --(custom UI)-->21ui/components/chain/custom_ui/XUI.cpp22```2324- The generated `.cpp` is produced by the build — there is **no checked-in generated file** to hand-edit. Just edit the `.dsp` and rebuild (`make debug`); CMake reruns faust. Confirm the result in `cmake-build-debug/compiled_dsps/<name>.generated.cpp`.25- Registration lives in `magda/daw/audio/CMakeLists.txt` via `magda_compile_faust_dsp(<dsp> <ClassName> <OUT_VAR>)`; the generated path is added to `target_sources` and the wrapper `#include`s `<name>.generated.cpp`.26- Validate DSP before building with the `faust-mcp-magda` MCP tools (`compile_faust`, `search_faust_libraries`, `get_faust_library`).2728## Host-slot contract (THE thing to get right)2930Each control is a Faust `hslider`/`nentry` tagged `[idx:N]`. `N` is the **host slot index** — the stable contract between dsp, wrapper, and UI. The wrapper harvests zones by reading the `[idx:N]` metadata.3132Three places must agree on slot indices:331. **`.dsp`** — `[idx:N]` on each param.342. **`MagdaXCompiledPlugin.hpp`** — `kFooSlot = N` constants + `kHostSlotCount`.353. **`MagdaXCompiledPlugin.cpp`** — `hostSlotInfo_[kFooSlot] = {.name, .scale, .minValue, .maxValue, .defaultValue, .choices}`.364. **`XUI.{hpp,cpp}`** — its own `kFooSlot` constants + `kNumParams`, label, layout.3738### Adding a parameter (common case)3940**Append at the end** (`idx = old kHostSlotCount`) so existing slot indices stay stable — never insert in the middle and renumber.41421. `.dsp`: add the `hslider`/`nentry` with the next free `[idx:N]`; wire it into the DSP graph. Reuse-by-copy from a sibling `.dsp` when matching behaviour (e.g. the drive lerp `(1-d)*x + d*(tanh(4x)/tanh(4))` from `magda_filter_svf.dsp`).432. Wrapper `.hpp`: add `kFooSlot = N`, bump `kHostSlotCount`. The `std::array<…, kHostSlotCount>` members grow automatically.443. Wrapper `.cpp`: add `hostSlotInfo_[kFooSlot] = {…}`. Use `ParameterScale::Discrete` + `.choices = {...}` for menus, `Linear`/`Logarithmic`/`FaderDB` otherwise.454. UI: add `kFooSlot`, bump `kNumParams`, add a label, place it in `resized()`.465. `make debug` (regenerates the dsp), confirm the param appears in the generated `.cpp`.4748`[style:menu{'A':0;'B':1}]` in the dsp = a discrete choice param; mirror with `.choices` in slot info.4950## Custom UI + linking5152- The UI exposes `std::vector<LinkableTextSlider*> getLinkableSliders()`; each slider carries its slot via `setParamIndex()`. `DeviceSlotComponent::setupCustomUILinking()` wires mod/macro/automation/MIDI-Learn off that list. Any control that must be linkable has to be a `LinkableTextSlider` in that list.53- To present a slot as something else (e.g. segmented buttons for a menu): keep the hidden `LinkableTextSlider` in `controls_`/`getLinkableSliders()` (carries value + linking) and drive it from the custom widget. Set selection **explicitly** for segmented buttons (`setToggleState(i==sel)`), not via JUCE radio groups (radio exclusivity left two segments lit).54- Theme-font buttons: `setLookAndFeel(&FlatTabButtonLookAndFeel::getInstance())` (flat tab) or `SmallButtonLookAndFeel` (rounded), both in `themes/SmallButtonLookAndFeel.hpp`; clear with `setLookAndFeel(nullptr)` in the destructor.55- Labels/fonts: `FontManager::getInstance().getUIFont(...)`, colours via `DarkTheme::getColour(...)`.5657## Reusing a curve view across devices5859Curve/response views (e.g. `CompiledFilterCurveView`) are normally bound to one plugin's slots via a `DeviceInfo` snapshot. To reuse one in a different device, add a **raw-value entry point** (`setRawState(...)`) that sets the target values directly and bypasses the device-snapshot path, plus optional per-instance theming (e.g. `setCurveColour`). Drive it from the host UI on every relevant param change.6061## Gotchas6263- Generated `.cpp` is build output — don't look for it in git; rebuild to refresh.64- Never reorder/insert `[idx:N]`; append. (Unreleased features: no migration needed, but stable indices keep diffs sane.)65- `ba.selectn(n, sel, ...)` is strict — every branch runs, so cascading/parallel filter modes is cheap and safe.66- Polyphonic synths use Faust's `mydsp_poly` (group=false) — per-voice zones are harvested into `voiceZonesBySlot_`.67- Build with the `building` skill (`make debug`), never raw faust/cmake.