Getting Started

Requirements

  • macOS 15 (Sequoia) or later
  • Apple Silicon (M-series)
  • Any AUv3-compatible host (Logic Pro, GarageBand, Ableton Live, and more)

Installation

  1. Download the latest DMG
  2. Open the .dmg and drag ConjureDSP into your Applications folder
  3. 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

FieldPythonRust
Input audioctx.inputs (2D ndarray)ctx.input(channel, frame)
Output audioctx.outputs (2D ndarray)ctx.set_output(channel, frame, value)
Channel countctx.inputs.shape[0]ctx.channels()
Frame countctx.frame_countctx.frames()
Sample ratectx.sample_ratectx.sample_rate()
Parametersctx.params["name"] or ctx.params.namectx.param(NAME)
Sidechainctx.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 keyRust indexDescription
bpmT_TEMPOTempo in BPM
beatT_BEATCurrent beat position
is_playingT_PLAYINGTrue/1.0 if playing, False/0.0 if stopped
time_sig_numeratorT_TIME_SIG_NUMTime signature numerator
time_sig_denominatorT_TIME_SIG_DENTime signature denominator
sample_positionT_SAMPLE_POSSample 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.outputs are 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 uv installer 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

ConceptPythonRust
Input audioctx.inputsctx.input(c, i)
Output audioctx.outputsctx.set_output(c, i, v)
Channelsctx.inputs.shape[0]ctx.channels()
Framesctx.frame_countctx.frames()
Sample ratectx.sample_ratectx.sample_rate()
Parametersctx.params["name"] / ctx.params.namectx.param(NAME)
Sidechain audioctx.sidechainctx.sidechain(c, i)
Sidechain connected?always present (zero-filled)ctx.sidechain_connected()
Transportctx.transport[key]unsafe { TRANSPORT_BUF[INDEX] }
Telemetry write (scalar)ctx.telemetry["slot"] = vctx.set_telemetry_scalar(SLOT, v)
Telemetry write (vector)ctx.telemetry["slot"][:n] = arrctx.set_telemetry_vector(SLOT, &slice)
State readctx.state["key"]via state!() macro

Transport

Python keyRust index constantDescription
bpmT_TEMPOTempo in BPM
beatT_BEATCurrent beat position
is_playingT_PLAYINGTrue/1.0 if playing
time_sig_numeratorT_TIME_SIG_NUMTime signature numerator
time_sig_denominatorT_TIME_SIG_DENTime signature denominator
sample_positionT_SAMPLE_POSSample 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
BuilderRangeUnitCurveDefault
freq()20 – 20000Hzlog1000
db()-60 – +12dBlinear0
time_ms()0.1 – 1000mslog100
pct()0 – 100%linear50
mix()0.0 – 1.0linear0.5
toggle()0 – 1linear0
ratio()1 – 20:1linear4
lfo_rate()0.1 – 20Hzlog1
choice(...)0 – N-1linear0
integer(min, max)customlinearmin
param(min, max)customlinearmin

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:

MethodParameters
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.

MethodDescription
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.

MethodDescription
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.

MethodDescription
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).

DescriptionPythonRust
Decibels to linear gain (0 dB = 1.0)db_to_gain(db)db_to_gain(db)
Linear gain to decibelsgain_to_db(gain)gain_to_db(gain)
Milliseconds to sample countms_to_samples(ms, sample_rate)ms_to_samples(ms, sample_rate)
Sample count to millisecondssamples_to_ms(samples, sample_rate)samples_to_ms(samples, sample_rate)
Frequency to period in samplesfreq_to_period(freq, sample_rate)freq_to_period(freq, sample_rate)
One-pole smoothing coefficientsmooth_coeff(time_ms, sample_rate)smooth_coeff(time_ms, sample_rate)
Linear crossfadecrossfade(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 interpolationlerp(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/sigmoid over 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 — accel won’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

OperationPythonRust
Matrix multiply, out = a @ bmatmul(a, b, out)matmul(a, b, out, m, k, n)
Matrix multiply-accumulate, c += a @ bmatmul_acc(a, b, c, m, k, n) (Rust-only)
Elementwise addvec_add(a, b, out)vec_add(a, b, out)
Elementwise multiplyvec_mul(a, b, out)vec_mul(a, b, out)
tanhvec_tanh(x, out)vec_tanh(input, output)
sigmoidvec_sigmoid(x, out)vec_sigmoid(input, output)
Add scalarvec_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:

MethodDescription
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 / functionDescription
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_model call 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!()

PythonRust
Importfrom conjuredsp.nam import load_modeluse conjuredsp::*;
Load (single)model = load_model(path)nam!(path) (module-level macro)
Load (multi)call load_model multiple timesnams! { SLOT = "path", … }
Processmodel.process(buffer, channel)nam_process(input, output, channel) -> bool
Process (slot)nam_process_slot(slot, input, output, channel) -> bool
Reset statemodel.reset()

load_model(path) — loads a .nam file. Supported path formats:

FormatExample
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>
ComponentUse 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:

APIDescription
ConjureDSP.apiVersionInteger; 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' eventLight/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 orphan ui/ files with no manifest hookup, unresolved param= 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 for ConjureDSP.ready, and reports any JS errors, exceptions thrown inside ready callbacks, and components whose param= 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 session
  • conjure-use-gemini — auto-launch Gemini CLI next session
  • conjure-use-codex — auto-launch Codex CLI next session
  • conjure-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

  1. Load or save the preset you want to export.
  2. Click the Export button (the share icon) in the plugin toolbar. A popover titled Export as Standalone AU opens.
  3. Enter an Effect name — this is what your DAW will display.
  4. 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), with repo scope.
  • 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:

  1. Quit your DAW.
  2. Open Terminal and run:
    killall -9 AudioComponentRegistrar
  3. 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:

  1. 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.
  2. If that doesn’t work, quit and reload the plugin in your DAW. A fresh instance retries the auto-launch from scratch.
  3. 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_bundle to catch static issues — orphan UI files, unresolved param= references, blocked external assets, low-contrast text.
  • Run smoke_test_ui to load the UI in an offscreen webview and surface JS errors, exceptions thrown inside ConjureDSP.ready callbacks, and components whose param= 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.