Getting Started
Requirements
- macOS 15 (Sequoia) or later
- Apple Silicon (M-series)
- Any AUv3-compatible host (Logic Pro, GarageBand, Ableton Live, and more)
Installation
- Download the latest DMG
- Open the
.dmgand drag ConjureDSP into your Applications folder - Launch your DAW and load ConjureDSP as an Audio Unit effect on any track
Your First Effect
ConjureDSP comes with a built-in code editor. When you load the plugin, you’ll see the editor with a default passthrough script.
Try replacing it with a simple gain effect:
from conjuredsp import db
from conjuredsp.dsp import db_to_gain
PARAMS = {
"gain": db(-24, 12, default=0),
}
def process(ctx):
gain = db_to_gain(ctx.params["gain"])
ctx.outputs[:] = ctx.inputs * gain process is called for every audio buffer. In Python it receives a single ctx object — ctx.inputs and ctx.outputs are 2D NumPy arrays of shape (channels, frame_count), ctx.params["gain"] reads the current parameter value in dB, and ctx.sample_rate is the host sample rate. In Rust the process! { ctx => … } macro generates the same context.
The conjuredsp library provides parameter builders (freq, db, time_ms, mix, etc.), filters, delay lines, oscillators, and DSP utilities. See Writing Effects and API Reference for details.
Factory Presets
ConjureDSP ships with factory presets covering common effects — gain, delay, chorus, compressor, EQ, and more. Many include a custom UI authored in HTML and JavaScript instead of the stock slider strip; use them as working examples or starting points for your own effects.
Writing Effects
Presets are Bundles
Every preset in ConjureDSP is a .cdp directory bundle: a manifest.json plus an entry script (process.py or process.rs) and an optional ui/ subtree if you want a custom interface. ConjureDSP creates and manages bundles for you — you write code in the editor and hit Save — but it’s worth knowing the shape because every preset you save, share, or sync is one of these on disk.
The process() Function
Every effect defines a process function that the host calls for every audio buffer. It reads input samples and writes output samples through a ctx object.
import numpy as np
def process(ctx):
np.copyto(ctx.outputs, ctx.inputs) In Python, ctx.inputs and ctx.outputs are 2D NumPy float32 arrays of shape (channels, frame_count), pre-sliced to the current block — vectorized assignments work directly. In Rust, the process! { ctx => … } macro emits the exports the host needs and binds ctx for sample-by-sample access.
Context
| Field | Python | Rust |
|---|---|---|
| Input audio | ctx.inputs (2D ndarray) | ctx.input(channel, frame) |
| Output audio | ctx.outputs (2D ndarray) | ctx.set_output(channel, frame, value) |
| Channel count | ctx.inputs.shape[0] | ctx.channels() |
| Frame count | ctx.frame_count | ctx.frames() |
| Sample rate | ctx.sample_rate | ctx.sample_rate() |
| Parameters | ctx.params["name"] or ctx.params.name | ctx.param(NAME) |
| Sidechain | ctx.sidechain (2D ndarray) | ctx.sidechain(channel, frame) |
Transport
DAW transport state — tempo, beat position, play state — is always available.
def process(ctx):
bpm = ctx.transport["bpm"]
playing = ctx.transport["is_playing"]
beat = ctx.transport["beat"] | Python key | Rust index | Description |
|---|---|---|
bpm | T_TEMPO | Tempo in BPM |
beat | T_BEAT | Current beat position |
is_playing | T_PLAYING | True/1.0 if playing, False/0.0 if stopped |
time_sig_numerator | T_TIME_SIG_NUM | Time signature numerator |
time_sig_denominator | T_TIME_SIG_DEN | Time signature denominator |
sample_position | T_SAMPLE_POS | Sample position in the timeline |
Sidechain
Every ConjureDSP instance exposes a second input bus labeled Sidechain that DAWs route from another track. Use it to drive ducking, gating, or any effect that follows an external signal.
Sidechain frames are always accessible — when the DAW hasn’t routed anything, the buffer is zero-filled in both languages, so reads are safe without a guard. The example below computes the per-block sidechain peak, which you’d typically feed into an envelope follower to drive a duck or gate:
def process(ctx):
# ctx.sidechain is a 2D ndarray of shape (channels, frame_count),
# mirroring ctx.inputs. Use numpy for vectorised access.
sc_peak = float(np.max(np.abs(ctx.sidechain))) if ctx.sidechain.size else 0.0
# ... use sc_peak to drive ducking, gating, etc. ... Important Notes
ctx.inputs/ctx.outputsare pre-allocated and pre-sliced — write into them directly, don’t create new buffers.process()runs on the real-time audio thread — avoid allocations and I/O.
Defining Parameters
Declare automatable parameters with the builder functions from conjuredsp:
from conjuredsp import freq, db, time_ms, mix, pct, toggle, ratio, choice, integer, param
PARAMS = {
"cutoff": freq(),
"gain": db(),
"time": time_ms(),
"mix": mix(),
"depth": pct(),
"bypass": toggle(),
"ratio": ratio(),
"mode": choice("Low", "Mid", "High"),
"bit_depth": integer(2, 16, unit="bits", default=8),
"custom": param(0.5, 20, unit="Hz", default=5, curve="log"),
}
def process(ctx):
cutoff = ctx.params["cutoff"]
# ... Each entry becomes a named parameter the DAW can automate. Values are denormalized to the real range before the script sees them — Python reads via ctx.params["cutoff"] (or ctx.params.cutoff), Rust via ctx.param(CUTOFF).
The full set of builders, their default ranges, and modifier methods (min, max, default, unit, curve) live in the API Reference.
Persistent State
DSP state that survives across process() calls — filter histories, delay-line buffers, envelope-follower levels — needs somewhere to live. The basic shape is a module-level binding read and written by process():
_envelope = 0.0
def process(ctx):
global _envelope
# ... update _envelope using ctx.inputs ...
_envelope = new_env persist! expands to a static NAME: Persist<T> = …;, so it has to sit at module scope — not inside process! { ctx => … }. Use persist! for Copy values you read or replace wholesale (envelope levels, write counters, coefficient structs).
Mutable in-place state
For DSP blocks whose &mut self methods are the natural usage shape — Biquad::process_sample, Lfo::tick, DelayLine::write — or raw buffers written linearly per block, use persist_mut! in Rust. Python just mutates the object in place:
_filter = Biquad()
def process(ctx):
# _filter.process_sample mutates _filter in place — no `global` needed.
y = _filter.process_sample(x) Biquad, Lfo, DelayLine, and the other stateful DSP blocks don’t implement Copy, so they have to live behind persist_mut! rather than a bare static mut.
State sized from ctx
When the shape of the state depends on something you only learn at runtime — channel count, a sample-rate-dependent buffer length — Python uses a lazy-init guard on the first process() call. Rust pre-allocates a fixed-size array sized to the worst case (AUv3 hosts top out at 2 channels):
_sc_filters = None
def process(ctx):
global _sc_filters
n_ch = ctx.inputs.shape[0]
if _sc_filters is None or len(_sc_filters) != n_ch:
_sc_filters = [Biquad() for _ in range(n_ch)]
# ... use _sc_filters[ch] ... For UI-driven state that survives DAW sessions, see State on the Custom UIs page.
Compilation
Python scripts run immediately when you save. Rust code is compiled on save by a bundled standalone Rust compiler that ships inside the plugin — no cargo install step, no system Rust toolchain, and no network round-trip. The first compile of a new script takes a couple of seconds; after that, ConjureDSP caches the compiled binary by the SHA256 of your source, so re-running the same script is instant. Editing the script triggers a recompile only for the changed version, leaving the cache for previous versions intact.
The conjuredsp Rust library is pre-compiled and bundled with the plugin, so use conjuredsp::*; works out of the box with no Cargo.toml and no dependency resolution.
Installing Packages and Crates
ConjureDSP has a built-in package manager for both Python packages and Rust crates. Open the package pane from the plugin toolbar to:
- Browse and install any package from PyPI for use in your Python scripts
- Browse and install any crate from crates.io for use in your Rust scripts
- See what’s already installed and remove things you don’t need
Installed packages are available immediately to all your scripts — numpy, scipy, librosa, and the rest of the SciPy ecosystem on the Python side; any crate you can use on the Rust side. The package manager handles fetching, building, and signing on your behalf so the new code can run inside the plugin sandbox.
When to Use Rust
- CPU-intensive algorithms (convolution, physical modeling)
- Effects that need deterministic performance
- When Python’s overhead is noticeable at low buffer sizes
For most effects (gain, tremolo, filters, delay), Python is fast enough and much easier to iterate with.
Installing Packages
ConjureDSP bundles NumPy, SciPy, and the Python standard library for Python scripts, and a curated DSP prelude for Rust. When you want to reach beyond that — a favorite Python library for FFT analysis, or a Rust crate with a filter primitive you like — ConjureDSP can install it for you.
Opening the Package Manager
Click the Packages button (the shipping-box icon) in the preset toolbar. A popover opens with two tabs: Python and Rust.
Installing a Python Package
In the Python tab, type a package spec — for example pedalboard==0.9.16 — and click Install. Live search against PyPI suggests completions once you’ve typed a couple of characters.
Installed packages are available to every preset:
import pedalboard
from scipy import signal
Built-in packages (the standard library, NumPy, SciPy) are listed under Built-in; packages you’ve installed appear under User-installed.
Installing a Rust Crate
The Rust tab works the same way. Enter a crate spec like dasp = "0.11" and click Install. Live search queries crates.io.
First install takes a little longer — crates are compiled from source so they can run inside the plugin — but subsequent installs reuse the cached toolchain.
Use the crate in your scripts with a normal use statement:
use dasp::signal;
use conjuredsp::*;
Where Packages Live
Installed packages are shared across all presets, not isolated per-preset. They live inside the ConjureDSP application container and survive app updates.
Right-click any user-installed package and choose Remove to uninstall it. If an install fails, a View Log button appears next to the error — tap it to see the installer output.
Restrictions
- Python: must be compatible with the bundled Python runtime. Pure-Python and most common binary wheels work; the bundled
uvinstaller handles the resolution. - Rust: crates must compile without platform-specific C dependencies. Most DSP and math crates work out of the box.
API Reference
process()
Called for every audio buffer.
def process(ctx):
# ctx.inputs / ctx.outputs are 2D ndarrays of shape (channels, frame_count)
ctx.outputs[:] = ctx.inputs process! { ctx => … } (Rust) is a single macro that emits everything the host needs — input/output/transport/sidechain buffers, the C-ABI entry point, and the ctx binding — in one shot.
Context fields
| Concept | Python | Rust |
|---|---|---|
| Input audio | ctx.inputs | ctx.input(c, i) |
| Output audio | ctx.outputs | ctx.set_output(c, i, v) |
| Channels | ctx.inputs.shape[0] | ctx.channels() |
| Frames | ctx.frame_count | ctx.frames() |
| Sample rate | ctx.sample_rate | ctx.sample_rate() |
| Parameters | ctx.params["name"] / ctx.params.name | ctx.param(NAME) |
| Sidechain audio | ctx.sidechain | ctx.sidechain(c, i) |
| Sidechain connected? | always present (zero-filled) | ctx.sidechain_connected() |
| Transport | ctx.transport[key] | unsafe { TRANSPORT_BUF[INDEX] } |
| Telemetry write (scalar) | ctx.telemetry["slot"] = v | ctx.set_telemetry_scalar(SLOT, v) |
| Telemetry write (vector) | ctx.telemetry["slot"][:n] = arr | ctx.set_telemetry_vector(SLOT, &slice) |
| State read | ctx.state["key"] | via state!() macro |
Transport
| Python key | Rust index constant | Description |
|---|---|---|
bpm | T_TEMPO | Tempo in BPM |
beat | T_BEAT | Current beat position |
is_playing | T_PLAYING | True/1.0 if playing |
time_sig_numerator | T_TIME_SIG_NUM | Time signature numerator |
time_sig_denominator | T_TIME_SIG_DEN | Time signature denominator |
sample_position | T_SAMPLE_POS | Sample position in the timeline |
The Rust constants are emitted by process!. Read them through the TRANSPORT_BUF static (also emitted): unsafe { TRANSPORT_BUF[T_TEMPO] }.
Parameter Builders
from conjuredsp import freq, db, time_ms, mix, pct, toggle, ratio, choice, integer, lfo_rate, param | Builder | Range | Unit | Curve | Default |
|---|---|---|---|---|
freq() | 20 – 20000 | Hz | log | 1000 |
db() | -60 – +12 | dB | linear | 0 |
time_ms() | 0.1 – 1000 | ms | log | 100 |
pct() | 0 – 100 | % | linear | 50 |
mix() | 0.0 – 1.0 | linear | 0.5 | |
toggle() | 0 – 1 | linear | 0 | |
ratio() | 1 – 20 | :1 | linear | 4 |
lfo_rate() | 0.1 – 20 | Hz | log | 1 |
choice(...) | 0 – N-1 | linear | 0 | |
integer(min, max) | custom | linear | min | |
param(min, max) | custom | linear | min |
Every Rust builder accepts the same five chained modifiers: .min(f), .max(f), .default(f), .unit("…"), .curve("linear" | "log"). Python builders take the equivalents as keyword args (freq(min=100, default=440)), but the named ones — freq, lfo_rate, db, time_ms, pct, mix, toggle, ratio — bake in their unit and curve and will raise a TypeError if you pass unit= or curve=. For a custom unit or curve in Python, drop down to param(min, max, unit=..., curve=...).
choice takes positional labels in Python (choice("A", "B", "C")) and a slice in Rust (choice(&["A", "B", "C"])). integer(min, max) is stepped — automation snaps to whole numbers and the script receives an integer-valued float.
Filters
from conjuredsp.filters import Biquad, BiquadCoeffs BiquadCoeffs — static methods returning filter coefficients:
| Method | Parameters |
|---|---|
lowpass(freq, q, sample_rate) | |
highpass(freq, q, sample_rate) | |
bandpass(freq, q, sample_rate) | |
notch(freq, q, sample_rate) | |
peak(freq, q, gain_db, sample_rate) | |
lowshelf(freq, q, gain_db, sample_rate) | |
highshelf(freq, q, gain_db, sample_rate) | |
allpass(freq, q, sample_rate) | |
identity() | passthrough; useful as an array initializer |
BiquadCoeffs also implements Default (delegates to identity()), so [BiquadCoeffs::default(); N] works for fixed-size coefficient arrays. Only BiquadCoeffs::identity() is const fn; the design-time builders (lowpass, highpass, etc.) call trig and have to run inside process!.
Biquad — stateful filter (direct form II transposed). Create one per channel.
| Method | Description |
|---|---|
Biquad() / Biquad::new() | Create a filter (passthrough by default) |
set_coeffs(coeffs) | Update coefficients (preserves state) |
process_sample(x) | Filter one sample, returns output |
reset() | Zero filter state |
In Rust, Biquad::new() is const fn so filters can be created in static initializers. Biquad doesn’t implement Copy, so hold it across blocks with persist_mut!:
persist_mut!(FILTERS: [Biquad; 2] = [Biquad::new(), Biquad::new()]);
process! { ctx =>
FILTERS.with_mut(|fs| {
for c in 0..ctx.channels().min(2) {
for i in 0..ctx.frames() {
let y = fs[c].process_sample(ctx.input(c, i) as f64) as f32;
ctx.set_output(c, i, y);
}
}
});
}
Delay Lines
from conjuredsp.buffers import DelayLine
dl = DelayLine(48000) # max delay in samples In Rust, DelayLine uses const generics for the buffer size and new() is const fn.
| Method | Description |
|---|---|
write(sample) | Write a sample and advance the write head |
read(delay_samples) | Read with linear interpolation |
read_cubic(delay_samples) | Read with Hermite cubic interpolation |
tap(delay_samples) | Read at integer delay (no interpolation) |
clear() | Zero the buffer and reset position |
Oscillators
from conjuredsp.osc import LFO, sine, triangle, saw, advance_phase LFO (Python) / Lfo (Rust) — stateful oscillator. Waveforms: sine, triangle, saw, square.
| Method | Description |
|---|---|
set_freq(freq) | Update frequency |
set_waveform(waveform) | Change waveform |
tick() | Advance one sample, returns value in [-1, 1] |
tick_n(n) | Advance n samples, returns array (Python: numpy) |
reset() | Reset phase to zero |
Python’s LFO needs the sample rate and is typically built lazily; Rust’s Lfo::new() is const fn and lives in a persist_mut!:
_lfo = None
def process(ctx):
global _lfo
if _lfo is None:
_lfo = LFO(ctx.sample_rate)
_lfo.set_freq(ctx.params["rate"]) In Python, pass waveform names as strings: LFO(sample_rate, waveform="triangle") or lfo.set_waveform("saw"). In Rust, use the Waveform enum.
Stateless functions — take phase in [0, 1), return value in [-1, 1]:
sine(phase), triangle(phase), saw(phase), advance_phase(phase, freq, sample_rate)
DSP Utilities
from conjuredsp.dsp import db_to_gain, smooth_coeff, crossfade, dbfs_to_vu The unit-conversion helpers are scalar in both languages. The two crossfades operate on buffers in Python (NumPy fused-multiply-add) and per-sample in Rust (the compiler auto-vectorises the surrounding loop, and per-sample composes naturally with the per-sample DSP idiom).
| Description | Python | Rust |
|---|---|---|
| Decibels to linear gain (0 dB = 1.0) | db_to_gain(db) | db_to_gain(db) |
| Linear gain to decibels | gain_to_db(gain) | gain_to_db(gain) |
| Milliseconds to sample count | ms_to_samples(ms, sample_rate) | ms_to_samples(ms, sample_rate) |
| Sample count to milliseconds | samples_to_ms(samples, sample_rate) | samples_to_ms(samples, sample_rate) |
| Frequency to period in samples | freq_to_period(freq, sample_rate) | freq_to_period(freq, sample_rate) |
| One-pole smoothing coefficient | smooth_coeff(time_ms, sample_rate) | smooth_coeff(time_ms, sample_rate) |
| Linear crossfade | crossfade(dry, wet, mix, out, n) | crossfade(dry, wet, mix) -> f32 |
| Equal-power crossfade (constant energy at the midpoint) | equal_power_crossfade(dry, wet, mix, out, n) | equal_power_crossfade(dry, wet, mix) -> f32 |
| Soft clipper (tanh saturation) | soft_clip(x, drive=1.0) | soft_clip(x, drive) |
| Linear interpolation | lerp(a, b, t) | lerp(a, b, t) |
| Convert dBFS to VU using EBU R68 (0 VU = -18 dBFS) | dbfs_to_vu(dbfs) | dbfs_to_vu(dbfs) |
VU_REF_DBFS (-18) is exposed as a constant for callers that want to do the conversion themselves.
Vectorized Math (accel)
The accel module is the bridge from preset code to Apple’s Accelerate framework — vDSP, cblas, and the vectorised libm transcendentals. Rust presets are compiled to WASM, which cannot link Accelerate directly; accel is the only path from a Rust preset to those routines, via host imports the runtime fulfils. On the Python side, accel is a thin layer over NumPy, whose BLAS backend on macOS is itself Accelerate — so both languages land on the same underlying SIMD/AMX code. The two surfaces match function-for-function, with one exception: matmul_acc (BLAS-style accumulating sgemm) is Rust-only.
When to use it
- Reach for it for small dense matmuls (mixing matrices, tiny neural-net layers), batch elementwise ops over a full block, or
tanh/sigmoidover long buffers. - Skip it in Rust for trivial per-sample loops — the Rust compiler auto-vectorises those, and the FFI hop costs more than it saves.
- Skip it in Python when your NumPy code is already vectorised and the out-buffer discipline buys nothing —
accelwon’t be faster.
The out buffer rule
Every function takes a pre-allocated out array. The point is real-time safety: allocating inside process() grows the host’s memory footprint over time (the macOS magazine allocator retains pages it has handed out), and the allocation itself can miss the audio deadline on hot enough call paths. Allocate scratch once at module scope and slice into it per block.
API
| Operation | Python | Rust |
|---|---|---|
Matrix multiply, out = a @ b | matmul(a, b, out) | matmul(a, b, out, m, k, n) |
Matrix multiply-accumulate, c += a @ b | — | matmul_acc(a, b, c, m, k, n) (Rust-only) |
| Elementwise add | vec_add(a, b, out) | vec_add(a, b, out) |
| Elementwise multiply | vec_mul(a, b, out) | vec_mul(a, b, out) |
tanh | vec_tanh(x, out) | vec_tanh(input, output) |
sigmoid | vec_sigmoid(x, out) | vec_sigmoid(input, output) |
| Add scalar | vec_add_scalar(x, scalar, out) | vec_add_scalar(input, scalar, output) |
Rust slices are flat row-major f32; matmul and matmul_acc take explicit m, k, n dims (a is m × k, b is k × n, out / c is m × n). Python infers shapes from the ndarrays.
Examples
A 2×2 mixing matrix applied per block in Rust:
use conjuredsp::accel;
// Mild crossfeed, row-major.
const MIX: [f32; 4] = [
0.9, 0.1,
0.1, 0.9,
];
process! { ctx =>
let n = ctx.frames();
// `input_block` and `output_block` are 2-channel × n-frame row-major
// slices materialised from ctx in your preset.
accel::matmul(&MIX, &input_block, &mut output_block, 2, 2, n);
}
A tanh waveshaper in Python with pre-allocated scratch:
import numpy as np
from conjuredsp.accel import vec_tanh
from conjuredsp.params import db
PARAMS = {"drive": db(min=0, max=24, default=6)}
# Allocated once at module load — never inside process().
SCRATCH_IN = np.empty(8192, dtype=np.float32)
SCRATCH_OUT = np.empty(8192, dtype=np.float32)
def process(ctx):
n = ctx.frame_count
gain = 10 ** (ctx.params["drive"] / 20.0)
for ch in range(ctx.outputs.shape[0]):
np.multiply(ctx.inputs[ch], gain, out=SCRATCH_IN[:n])
vec_tanh(SCRATCH_IN[:n], SCRATCH_OUT[:n])
ctx.outputs[ch][:n] = SCRATCH_OUT[:n]
The bundled NAM presets (preset_nam.cdp, preset_nam_rust.cdp) are the live consumer: the conjuredsp.nam / conjuredsp::nam modules dispatch their sgemm and tanh inner loops through accel.
Reporting Algorithmic Latency
Effects that introduce algorithmic latency (lookahead limiting, FFT windowing, oversampling) should declare it so the DAW can compensate and keep everything in sync.
Only report latency, not delay. The creative delay time in a delay effect is the point of the effect and should not be reported — the DAW would pull the whole track earlier to compensate, defeating the purpose. A delay effect can still declare latency for any lookahead or windowing it does internally; just don’t roll the delay time itself into that number.
LATENCY = 256 # samples of lookahead Python declares a module-level LATENCY constant in samples. Rust uses the latency!() macro. ConjureDSP reads the value at script-load time and forwards it to the DAW via AUAudioUnit.latency for automatic delay compensation.
Persistent State (Rust)
Rust effects use macros to declare state that persists across process() calls.
// Single Copy values — read or replace wholesale.
persist!(WRITE_HEAD: usize = 0);
// Mutable DSP blocks — call methods through .with_mut().
persist_mut!(FILTER: Biquad = Biquad::new());
// Fixed-size arrays of Copy types work too.
persist!(ENV: [f32; 4] = [0.0; 4]);
process! { ctx =>
let w = WRITE_HEAD.get();
WRITE_HEAD.set(w + 1);
FILTER.with_mut(|f| {
let y = f.process_sample(x as f64);
});
}
Use persist! for Copy values (scalars, coefficient structs like BiquadCoeffs). Use persist_mut! for stateful blocks that need &mut self access — Biquad, Lfo, DelayLine, raw buffers. These DSP blocks don’t implement Copy, so persist_mut! is the only way to hold them in a static.
Available Libraries
The Python runtime includes numpy and scipy in addition to the conjuredsp package. The accel module (above) is the cross-language hardware-accelerated path for batch math.
Neural Amp Modeling
from conjuredsp.nam import load_model
model = load_model("tone3000://TONE_ID/MODEL_ID") Load a .nam tone model. Accepts tone3000://tone_id/model_id, ~/relative, or absolute paths. See Neural Amp Modeling for full usage and examples.
Python model methods:
| Method | Description |
|---|---|
model.process(buffer, channel) | Process a float32 NumPy array for the given channel index. Returns a float32 array. |
model.reset() | Clear per-channel hidden state (LSTM models). |
Rust:
| Macro / function | Description |
|---|---|
nam!("path") | Single-slot model. Generates nam_process(input, output, channel) -> bool. |
nams! { SLOT = "path", … } | Multi-slot models. Generates nam_process_slot(slot, input, output, channel) -> bool. |
Both Rust calls return false if the slot didn’t load (file missing, format mismatch); nam_process is a wrapper around slot 0 of nams!.
Telemetry, State, and UI Hooks
DSP scripts can publish per-block telemetry that custom HTML/JS UIs read in real time, and can declare bundle-private persistent state that survives DAW sessions. The author surface for both lives on the Custom UIs page — including the telemetry! and state! macros for Rust, the TELEMETRY and STATE dicts for Python, and the window.ConjureDSP.{audio, state} JS bridge.
Neural Amp Modeling
Neural Amp Modeling (NAM) lets you load .nam tone models — deep-learning captures of amps, pedals, and full rigs — and run them as part of your ConjureDSP effects. ConjureDSP includes a built-in tone browser connected to tone3000.com, a community library where you can discover, download, and use thousands of tones without leaving your DAW.
The Tone Browser
Open the tone browser from the ConjureDSP plugin UI. From there you can:
- Search thousands of community tones by name, gear type (Amp, Pedal, Full Rig, Outboard, IR), or sort by trending, newest, or most downloaded.
- Browse your tones — see tones you’ve created or marked as favorites on tone3000.com.
- Download models in multiple sizes: Standard, Lite, Feather, or Nano — trading off quality for CPU load.
- Insert code — tap “Use” on any downloaded model and ConjureDSP inserts the
load_modelcall directly into your editor.
Sign in with your tone3000.com account to access your favorites and created tones. Authentication uses OAuth — no password is stored by ConjureDSP.
Loading a Model
from conjuredsp.nam import load_model
model = load_model("tone3000://TONE_ID/MODEL_ID") TONE_ID and MODEL_ID are filled in automatically when you tap “Use” in the tone browser. You can also pass a local file path:
model = load_model("~/my-tones/jcm800.nam") Processing Audio
Call model.process() (Python) or nam_process() (Rust) inside your process function. The model is stateful — pass the channel index so each channel gets independent hidden state.
from conjuredsp.nam import load_model
from conjuredsp import db, mix
from conjuredsp.dsp import db_to_gain
model = load_model("tone3000://TONE_ID/MODEL_ID")
PARAMS = {
"input_gain": db(min=-60, max=12, default=0),
"mix": mix(),
}
def process(ctx):
gain = db_to_gain(ctx.params["input_gain"])
mix_val = ctx.params["mix"]
n_ch = ctx.inputs.shape[0]
for ch in range(n_ch):
dry = ctx.inputs[ch]
wet = model.process(dry * gain, ch)
ctx.outputs[ch] = dry * (1.0 - mix_val) + wet * mix_val Multi-slot Models
A preset can load multiple NAM models — say, an amp into a cab — and route them in series. In Rust, declare them with nams! and call nam_process_slot:
use conjuredsp::*;
nams! {
AMP = "tone3000://AMP_TONE/MODEL",
CAB = "tone3000://CAB_TONE/MODEL",
}
process! { ctx =>
let mut a = [0.0_f32; MAX_FR];
let mut b = [0.0_f32; MAX_FR];
let n = ctx.frames();
for c in 0..ctx.channels() {
for i in 0..n { a[i] = ctx.input(c, i); }
unsafe {
nam_process_slot(AMP, &a[..n], &mut b[..n], c);
nam_process_slot(CAB, &b[..n], &mut a[..n], c);
}
for i in 0..n { ctx.set_output(c, i, a[i]); }
}
}
In Python, just call load_model() multiple times and chain the calls.
API Reference
load_model() / nam!()
| Python | Rust | |
|---|---|---|
| Import | from conjuredsp.nam import load_model | use conjuredsp::*; |
| Load (single) | model = load_model(path) | nam!(path) (module-level macro) |
| Load (multi) | call load_model multiple times | nams! { SLOT = "path", … } |
| Process | model.process(buffer, channel) | nam_process(input, output, channel) -> bool |
| Process (slot) | — | nam_process_slot(slot, input, output, channel) -> bool |
| Reset state | model.reset() | — |
load_model(path) — loads a .nam file. Supported path formats:
| Format | Example |
|---|---|
| tone3000 URL | "tone3000://abc123/def456" |
| Home-relative | "~/my-tones/jcm800.nam" |
| Absolute path | "/Users/me/tones/jcm800.nam" |
model.process(buffer, channel) (Python) — processes a NumPy float32 array for the given channel index. Returns a float32 array of the same length.
model.reset() (Python) — clears per-channel state: WaveNet history buffers and LSTM hidden cells. Call this when playback restarts to avoid artifacts from stale state.
nam!(path) / nams! { … } (Rust) — module-level macros. Both load their models at startup. nam! generates nam_process(input: &[f32], output: &mut [f32], channel: usize) -> bool (a wrapper around slot 0). nams! generates the index constants (AMP, CAB, …) and nam_process_slot(slot, input, output, channel) -> bool. Both Rust calls return false if the slot didn’t load successfully.
Supported Architectures
ConjureDSP supports both WaveNet and LSTM architectures.
tone3000:// URL Scheme
Downloaded tones are stored locally in the ConjureDSP app container. The tone3000://TONE_ID/MODEL_ID path is resolved automatically — no manual file management needed. Tones remain available offline after download.
Custom UIs
Every preset is a .cdp bundle directory. By default the bundle is just an entry script — Python or Rust — and ConjureDSP renders a generic strip of sliders for whatever parameters you declare. Drop a ui/index.html file into the bundle and ConjureDSP renders that instead, in a webview embedded in the plugin window.
Bundle Layout
MyEffect.cdp/
manifest.json # required — declares the entry script + UI
process.py # or process.rs
ui/
index.html # custom UI markup
assets/ # CSS, JS, images, fonts (optional)
A minimal manifest.json for a bundle with a custom UI:
{
"schemaVersion": 2,
"entry": "process.py",
"language": "python",
"params": [
{ "name": "drive", "min": 1.0, "max": 10.0, "default": 1.0, "unit": "x" }
],
"ui": {
"entryHTML": "ui/index.html",
"width": 420,
"height": 220,
"fps": 30,
"audioFrames": true
}
}
The params array populates the parameter tree before the script compiles, so the UI renders with correct defaults during a slow Rust compile. ui.fps caps the rate of audio-frame callbacks (see Audio frames below). Set audioFrames: false if your UI doesn’t need per-block telemetry — it skips the bridge work entirely.
Components
ConjureDSP injects a small library of web components into every custom-UI webview. Drop any of them into your HTML and bind to a parameter with the param= attribute:
<cdp-slider param="drive"></cdp-slider>
<cdp-knob param="cutoff"></cdp-knob>
<cdp-toggle param="bypass"></cdp-toggle>
| Component | Use for |
|---|---|
<cdp-slider> | Linear horizontal slider with label + value readout |
<cdp-knob> | Rotary knob; supports custom SVG via slot |
<cdp-toggle> | On/off switch (binds to toggle() parameters) |
<cdp-choice> | Dropdown menu (binds to choice() parameters) |
<cdp-xy> | Two-axis pad bound to two parameters |
<cdp-meter> | Vertical/horizontal level meter, peak-hold optional |
<cdp-scope> | Oscilloscope; renders vector telemetry as a waveform |
<cdp-bargraph> | Bar-per-element view of vector telemetry |
<cdp-panel> | Layout container for grouping controls |
Parameter names resolve loosely — case, underscores, and spaces all collapse, so param="cutoff_hz" in HTML will bind to a parameter declared as Cutoff Hz in the manifest. This lets the same ui/index.html serve both the Python and Rust variant of a preset.
Theming uses CSS custom properties and ::part() hooks. Override the accent color globally with :root { --cdp-accent: hotpink; } or per-element with inline styles. For fully custom geometry — your own knob graphic, say — slot an SVG into <cdp-knob> and react to the --cdp-knob-norm CSS variable that the component writes on every value change.
JavaScript Bridge
ConjureDSP exposes a small global, window.ConjureDSP, that’s available the moment your script runs. The components above use it under the hood; you can use it directly when you need to drive a custom widget.
ConjureDSP.ready(() => {
// Read a parameter value
const drive = ConjureDSP.parameters.get(0);
// Write a parameter value (drives DAW automation as if a knob moved)
ConjureDSP.parameters.set(0, 4.5);
// React to changes from anywhere — slider drag, DAW automation, MIDI learn
ConjureDSP.parameters.onAnyChange((index, value) => {
console.log(`param ${index} → ${value}`);
});
});
Always wait for ConjureDSP.ready(cb) before reading state — until the initial state arrives, parameters.get() and parameters.metadata() return undefined. The parameters.set() call fires onChange and onAnyChange handlers synchronously, so the same redraw runs whether the user dragged your widget or the DAW automated the parameter.
The full surface:
| API | Description |
|---|---|
ConjureDSP.apiVersion | Integer; bumps on breaking changes |
ConjureDSP.ready(cb) | Fires once when initial state arrives |
ConjureDSP.parameters.{count, get, set, metadata} | Parameter read/write |
ConjureDSP.parameters.{onChange, onAnyChange} | Change subscriptions |
ConjureDSP.state.{get, set, reset, resetAll} | Persistent JSON state (see below) |
ConjureDSP.state.{onChange, onAnyChange} | State change subscriptions |
ConjureDSP.audio.{onFrame, offFrame} | Per-block audio telemetry |
ConjureDSP.transport.onChange(cb) | DAW transport updates ({bpm, isPlaying, beat, …}) |
ConjureDSP.theme (getter) + 'themechange' event | Light/dark theme tracking |
ConjureDSP.log(...) | Forward to the host log |
Audio Frames and Telemetry
ConjureDSP.audio.onFrame(cb) fires at the rate set by manifest.ui.fps, with a small payload containing per-block RMS, peak, and (opt-in) FFT bins. Use it to drive meters, scopes, and visualizers without round-tripping through parameters.
For DSP-internal state that the audio payload can’t see — envelope follower levels, gain reduction, FFT spectra you computed yourself — publish it through the telemetry channel. Declare slots in your script and write to them each block:
TELEMETRY = {
"peak_db": {"unit": "dB"},
"envelope_db": {"unit": "dB"},
}
def process(ctx):
# ... DSP ...
ctx.telemetry["peak_db"] = peak_db
ctx.telemetry["envelope_db"] = env_db Telemetry values arrive on the JS side inside frame.telemetry, keyed by the slot name your script wrote.
Vector slots
Scalar slots cover meters and readouts. For per-sample shapes — a gain-reduction curve under <cdp-scope>, an FFT spectrum under <cdp-bargraph> — declare a vector slot and write an array each block. The host harvests the first ctx.frame_count values; anything past that is ignored.
TELEMETRY = {
"gr_curve": {"unit": "dB", "shape": "vector"},
}
def process(ctx):
n = ctx.frame_count
# ctx.telemetry["gr_curve"] is a pre-allocated numpy array of length
# MAX_FRAMES — write the first `n` samples in place:
ctx.telemetry["gr_curve"][:n] = gr_per_sample[:n] Vector slots arrive on the JS side as a Float32Array under frame.telemetry[slot]. Bind a <cdp-scope> or <cdp-bargraph> to the slot and the component handles the draw, or read the array directly inside onFrame for a custom visualisation.
Persistent State
DSP scripts can declare a small JSON state object that persists across DAW sessions and travels with the project file. Both the script and the UI can read and write it.
STATE = {
"preset_label": "Default",
"history": [],
}
def process(ctx):
label = ctx.state["preset_label"] # read-only mapping
# ... DSP ... From the UI side, ConjureDSP.state.set(key, value) writes; onChange lets you redraw when the script (or another UI surface) updates a key. Writes that would push the serialized state past the 64 KB cap return false and don’t commit. Keys not in STATE’s defaults log a one-shot warning.
State is bundle-private — different presets can use the same key names without colliding.
Hot Reload, Validation, Sandboxing
Edit ui/index.html, manifest.json, or anything under ui/assets/ from any editor — the in-plugin Monaco editor, VS Code, or via the write_bundle_file MCP tool. The plugin watches the bundle directory and reloads the webview within a few hundred milliseconds.
Two MCP tools catch the common breakage classes before you ship:
validate_bundle— static lint sweep. Reports orphanui/files with no manifest hookup, unresolvedparam=references (with “did you mean” suggestions), externally-blocked assets, low-contrast text, and a few other authoring traps. The exact rule list is what the tool prints; treat its output as the source of truth.smoke_test_ui— loads the UI in an offscreen webview, waits forConjureDSP.ready, and reports any JS errors, exceptions thrown insidereadycallbacks, and components whoseparam=attribute didn’t bind.
UI assets load from a custom URL scheme served by the plugin. A Content-Security-Policy header on every response blocks fetch, XMLHttpRequest, and WebSocket, so UI code can’t make network calls. Inline styles and scripts are allowed; loading anything from outside the bundle is not.
Worked Example
A complete bundle that scales the input by a drive parameter, soft-clips, and publishes peak + envelope telemetry to two bar meters in the UI.
MyEffect.cdp/manifest.json:
{
"schemaVersion": 2,
"entry": "process.py",
"language": "python",
"params": [
{ "name": "drive", "min": 1.0, "max": 10.0, "default": 1.0, "unit": "x" }
],
"ui": {
"entryHTML": "ui/index.html",
"width": 420,
"height": 220,
"fps": 30,
"audioFrames": true
}
}
MyEffect.cdp/process.py:
import numpy as np
PARAMS = {
"drive": {"min": 1.0, "max": 10.0, "default": 1.0, "unit": "x"},
}
TELEMETRY = {
"peak_db": {"unit": "dB"},
"envelope_db": {"unit": "dB"},
}
_envelope = 0.0
def process(ctx):
global _envelope
drive = max(1.0, ctx.params["drive"])
n_ch, frame_count = ctx.inputs.shape
attack = float(np.exp(-1.0 / (0.050 * ctx.sample_rate)))
release = float(np.exp(-1.0 / (0.200 * ctx.sample_rate)))
block_peak = float(np.max(np.abs(ctx.inputs))) if frame_count > 0 else 0.0
last_ch = ctx.inputs[n_ch - 1]
env = _envelope
for i in range(frame_count):
target = abs(float(last_ch[i])) * drive
coeff = attack if target > env else release
env = target + coeff * (env - target)
_envelope = env
for ch in range(n_ch):
ctx.outputs[ch] = np.tanh(ctx.inputs[ch] * drive)
def lin_to_db(x):
return -120.0 if x <= 1e-6 else float(20.0 * np.log10(x))
ctx.telemetry["peak_db"] = lin_to_db(block_peak)
ctx.telemetry["envelope_db"] = lin_to_db(env)
MyEffect.cdp/ui/index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
:root { color-scheme: light dark; }
body {
margin: 0; padding: 18px 22px;
display: flex; flex-direction: column; gap: 14px;
font: 13px -apple-system, system-ui, sans-serif;
background: Canvas; color: CanvasText;
}
.row {
display: grid; grid-template-columns: 96px 1fr 64px;
align-items: center; gap: 10px;
}
.bar { height: 10px; background: color-mix(in srgb, CanvasText 14%, transparent); border-radius: 5px; overflow: hidden; }
.fill { height: 100%; width: 0%;
background: color-mix(in srgb, CanvasText 65%, transparent);
transition: width 60ms linear; }
.value { text-align: right; font-variant-numeric: tabular-nums; }
</style>
</head>
<body>
<cdp-slider param="drive"></cdp-slider>
<div class="row">
<span>Peak</span>
<div class="bar"><div class="fill" id="peakFill"></div></div>
<span class="value" id="peakValue">— dB</span>
</div>
<div class="row">
<span>Envelope</span>
<div class="bar"><div class="fill" id="envFill"></div></div>
<span class="value" id="envValue">— dB</span>
</div>
<script>
const dbToFill = (db) => {
const t = Math.max(0, Math.min(1, (db + 60) / 72));
return (t * 100).toFixed(1) + '%';
};
const fmtDb = (db) => db <= -120 ? '-∞ dB' : db.toFixed(1) + ' dB';
const peakFill = document.getElementById('peakFill');
const peakValue = document.getElementById('peakValue');
const envFill = document.getElementById('envFill');
const envValue = document.getElementById('envValue');
ConjureDSP.ready(() => {
ConjureDSP.audio.onFrame((frame) => {
if (!frame.telemetry) return;
const peak = frame.telemetry['peak_db'];
const env = frame.telemetry['envelope_db'];
if (peak !== undefined) {
peakFill.style.width = dbToFill(peak);
peakValue.textContent = fmtDb(peak);
}
if (env !== undefined) {
envFill.style.width = dbToFill(env);
envValue.textContent = fmtDb(env);
}
});
});
</script>
</body>
</html>
Save the bundle, load the preset in ConjureDSP, and play audio through it — the slider drives drive and both meters react in real time. Run validate_bundle and smoke_test_ui on the bundle path to confirm nothing’s wrong before sharing it.
AI-Assisted Coding
ConjureDSP is built around the idea that you should be able to describe an audio effect in plain English and hear it on your track seconds later. The AI does the typing; you do the listening.
The important part: ConjureDSP itself doesn’t host or route any AI. There’s no ConjureDSP-branded mode. You select your preferred coding assistant, and ConjureDSP plugs into it.
There are two ways to do this.
Option 1: Integrated AI connection
ConjureDSP ships with a full terminal pane inside the plugin window. You can run any terminal-based AI coding agent in it. ConjureDSP has first-class auto-launch support for Claude Code, Codex CLI, and Gemini CLI, and any other tool (Aider, a plain shell, etc.) works via the manual picker option. The agent reads your current script, writes new DSP code, hot-reloads it, and you hear the change on the track. It can also tweak parameters, save presets, and check the audio state.
Picking an agent
On first launch, ConjureDSP looks for supported agents on your PATH. If it finds exactly one, it auto-launches that agent. If it finds more than one, you get a numbered picker to choose — the choice is remembered for next time.
You can switch agents from the terminal at any time with:
conjure-use-claude— auto-launch Claude Code next sessionconjure-use-gemini— auto-launch Gemini CLI next sessionconjure-use-codex— auto-launch Codex CLI next sessionconjure-use-manual— skip auto-launch; drop to a plain shell so you can start whatever tool you want
The same preference can be edited in Settings → Terminal, which also has a Relaunch terminal button to apply the change immediately.
MCP, automatically
Under the hood, ConjureDSP exposes itself as an MCP server (Model Context Protocol) with 19 tools, grouped by what they do:
- DSP scripting:
compile_and_run,get_script,get_error,get_docs,list_packages,dsp_probe - Parameters and audio state:
set_parameter,get_parameters,get_audio_state,toggle_bypass - Presets and tones:
list_presets,save_preset,duplicate_bundle,list_tones - Custom UI authoring:
get_bundle_info,read_bundle_file,write_bundle_file,validate_bundle,smoke_test_ui
The bundle-editing tools let an agent author a custom HTML/JS UI alongside the DSP — write ui/index.html, validate it, and smoke-test it without leaving the chat.
Whichever agent you pick, ConjureDSP wires up the MCP connection automatically before launching it — no manual config. The Terminal settings tab shows each agent’s MCP status so you can confirm it’s connected. Any other MCP-compatible client can connect the same way; the connection is local, over a loopback socket on your machine.
Option 2: Copy-paste prompt helper
If you’d rather not run an agent at all, ConjureDSP has a built-in prompt helper. You type what you want to build, and ConjureDSP assembles a self-contained prompt that bundles:
- The relevant API documentation
- Your current script (optional)
- Your description and target language
Copy it, paste it into Claude.ai, ChatGPT, Gemini, or any chatbot you already use, then paste the response back into the editor and hit Run. Same loop, no subscription required.
Exporting as a Standalone AU
Once you’ve dialed in a preset you’re happy with, you can export it as its own AUv3 plugin. The result is a self-contained Audio Unit that shows up in your DAW’s plugin list like any other effect.
Exporting a Preset
- Load or save the preset you want to export.
- Click the Export button (the share icon) in the plugin toolbar. A popover titled Export as Standalone AU opens.
- Enter an Effect name — this is what your DAW will display.
- Click Export.
ConjureDSP builds the plugin in the background and installs it to ~/Library/Application Support/ConjureDSP/Exports/ when it’s done. Your DAW will pick it up on the next plugin rescan.
NAM Tones in Exported Presets
If your preset loads a NAM tone model (load_model in Python, nam! in Rust), the tone file is bundled into the exported plugin so it keeps working on machines that don’t have ConjureDSP installed.
Because you’re redistributing that tone file, the exporter asks you to certify one of:
- I have permission from the tone creator to redistribute this file.
- I am exporting for personal use only.
Pick whichever applies before the Export button becomes available.
What Ships in the Plugin
Exported plugins are fully self-contained:
- Python presets ship their source plus the Python runtime needed to execute it.
- Rust presets ship a pre-compiled binary — nothing is compiled at load time.
- Custom UIs travel with the export. If your bundle has a
ui/subtree, the exported AU renders the same HTML/JS/CSS interface in any DAW. - Sidechain bus is preserved — the standalone AU advertises the same second input bus that ConjureDSP does, so existing sidechain routings keep working.
Either way, exported plugins load instantly in your DAW and run independently of ConjureDSP.
Updating an Exported Plugin
Each exported AU is a snapshot of the preset at export time. Editing the preset inside ConjureDSP doesn’t update plugins you’ve already exported — re-export to pick up changes. The new build overwrites the previous one under the same effect name.
Syncing Presets with GitHub
Every preset you save in ConjureDSP is already a git commit under the hood. If you want to back those commits up — or share presets with another machine or another person — point ConjureDSP at a GitHub repository and it’ll push automatically on every save.
Setting Up Sync
Open the preset toolbar, click the Settings (gear) icon, and select the Sync tab.
1. Personal Access Token
Paste a GitHub personal access token and click Save. The token is stored in your macOS Keychain and is only used when pushing to a remote — local commits don’t require one.
Two token types work:
- Classic PAT (starts with
ghp_) — create at GitHub → Settings → Developer settings → Personal access tokens → Tokens (classic), withreposcope. - Fine-grained PAT (starts with
github_pat_) — create at GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens, scoped to your presets repository with Contents: Read and write.
2. Commit Messages
Choose how commits are named on save:
- Always prompt for message on save — ConjureDSP pops a dialog on every save.
- Always use timestamp — commits are named with an ISO timestamp. No prompt.
3. Remote
Enter your repository URL (e.g., https://github.com/you/my-conjuredsp-presets.git) and click Set remote. After that, every saved preset pushes automatically with a short debounce, so rapid saves batch into a single push.
Use Push now to force a push immediately, or Clear remote to stop auto-push without deleting any commits.
Repository Format
Each preset is a .cdp directory bundle. Repositories are a flat list of bundles at the root:
my-conjuredsp-presets/
Tremolo.cdp/
manifest.json
process.rs
Tin Can Telephone.cdp/
manifest.json
process.py
Acid Sermon.cdp/
manifest.json
process.rs
ui/
index.html
assets/
style.css
You can edit files directly on GitHub or on another machine; ConjureDSP picks up changes on its next pull. Editing a bundle’s ui/index.html shows up as its own per-file diff in git log, alongside changes to the entry script.
What Gets Synced
- User presets — yes. Anything you save yourself.
- Factory presets — no. These ship with the app.
- Package manager state — no. Install packages separately on each machine.
Where Presets Live Locally
User presets live in ConjureDSP’s container at ~/Library/Group Containers/group.com.MichaelJancsy.ConjureDSP/Presets/. That directory is itself a git repository, so you can inspect history from the command line if you want:
cd ~/Library/Group\ Containers/group.com.MichaelJancsy.ConjureDSP/Presets
git log --oneline
Push Status
The Sync tab shows the last push result at a glance: Never pushed, Last push: N minutes ago, or Push failed: <error>. If a push fails — bad token, network drop, rejected branch — the commit stays local. Fix the problem and hit Push now to retry.
Troubleshooting
If something isn’t behaving the way you expect, this page is the first place to look. If you don’t see your issue here, email conjuredsp@gmail.com.
ConjureDSP doesn’t appear in my DAW’s plugin list
macOS keeps an Audio Unit cache that occasionally gets out of sync after an install. The fix:
- Quit your DAW.
- Open Terminal and run:
killall -9 AudioComponentRegistrar - Reopen your DAW. Most DAWs will trigger a fresh AU scan automatically. In Logic Pro you can force it from Logic Pro → Settings → Plug-In Manager → Reset & Rescan Selection.
If it still doesn’t show up, make sure ConjureDSP is in /Applications (not your Downloads folder).
I loaded the plugin but I hear silence
The most common reason: you’re running the Demo and its audition budget has elapsed. The Demo gives you about a minute of active audio playback per plugin instance — silent passages don’t count against it, so the clock only ticks while something is actually coming out of the plugin. Once the budget is used up, output is muted until you reset it.
To get more demo time, click Restart Demo in the plugin’s subscription settings pane. This zeroes the counter and gives you another minute of active playback. Or grab a license to remove the limit entirely.
If you’re running the Licensed version and still hearing silence, check:
- The bypass toggle in the plugin header isn’t on
- Your script’s
process()function actually writes to the output buffers - The track itself has signal coming in (try the built-in spectrogram — if input is moving but output isn’t, the script is the issue)
My Rust code takes a long time to compile the first time
That’s expected. ConjureDSP ships its own standalone Rust compiler and builds your code the first time it sees a particular script. After that, the compiled binary is cached by the SHA256 of your source, so subsequent runs of the same code are instant. Editing the script triggers a recompile only for the changed version.
The integrated terminal won’t connect
The terminal runs in a small companion app called ConjureDSPTerminal that ships inside the main app bundle. The plugin tries to auto-launch it when you open the terminal pane, but if it doesn’t connect:
- Look for a Launch Terminal button in the terminal pane — this re-triggers the auto-launch. You should see ConjureDSPTerminal appear in your Dock a moment later.
- If that doesn’t work, quit and reload the plugin in your DAW. A fresh instance retries the auto-launch from scratch.
- As a manual fallback, you can open the companion app directly at
/Applications/ConjureDSP.app/Contents/Library/ConjureDSPTerminal.app.
Why a companion app? AU plugins run inside a sandbox managed by your DAW, and that sandbox can’t fork the Claude Code CLI directly. The companion app sits outside the sandbox and relays input and output to the terminal pane in the plugin window.
My custom UI looks broken
Two MCP tools cover most authoring problems:
- Run
validate_bundleto catch static issues — orphan UI files, unresolvedparam=references, blocked external assets, low-contrast text. - Run
smoke_test_uito load the UI in an offscreen webview and surface JS errors, exceptions thrown insideConjureDSP.readycallbacks, and components whoseparam=attribute didn’t bind.
Both run from any agent connected to the integrated terminal, or any external MCP client. See Custom UIs for the bridge surface and component reference.
Monaco editor / code editor doesn’t load
This usually means a network call to the bundled editor assets failed at install time. Reinstalling from the latest DMG fixes it. If that doesn’t work, email us with your macOS version and DAW name.
NAM / tone3000 questions
See the Neural Amp Modeling page for which model architectures are supported (WaveNet and LSTM at the time of writing) and how the in-plugin tone browser works.
My preset works in ConjureDSP but the exported standalone plugin sounds different
Exported plugins use the same Python or Rust runtime as ConjureDSP, but the parameter ranges and defaults are baked in at export time. If you’re getting different output, double-check that the parameter values in the standalone plugin match the preset you exported from. If the difference is in the audio itself (not parameters), email us with both files attached so we can reproduce.