Werkstatt

A programmable audio effect that lets you write custom DSP code in JavaScript. Define your own signal processing, declare parameters with knobs, and hot-reload changes in real time.


screenshot


0. Overview

Werkstatt is a scriptable audio effect device. You write a Processor class in JavaScript that receives stereo audio buffers and outputs processed audio sample by sample. Parameters declared in the code appear as automatable knobs on the device panel.

Example uses:


1. Editor

Click the Editor button on the device panel to open the full-screen code editor. The editor uses Monaco (the engine behind VS Code) with JavaScript syntax highlighting.

The toolbar at the top provides:

The status bar at the bottom shows the current state:


2. Label

Set the device name using a // @label comment:

// @label My Effect

When the script compiles, the device panel header will display this name. Omitting @label keeps the current name, unless that name came from the previous @label: then the device goes back to its default name. An empty @label (without a name) causes a compile error.


3. Parameters

Declare parameters using // @param comments at the top of your code:

// @param gain 1.0
// @param cutoff 1000 20 20000 exp Hz
// @param mode 0 0 3 int
// @param bypass false

Each @param directive creates an automatable knob on the device panel. The full syntax is:

// @param <name> [default] [min max type [unit]]

Simple (unipolar)

// @param gain           → 0–1, default 0
// @param gain 0.5       → 0–1, default 0.5

The knob value and paramChanged value are in the 0–1 range.

Mapped

// @param cutoff 1000 20 20000 exp Hz    → exponential 20–20000, default 1000
// @param time 500 1 2000 linear ms      → linear 1–2000, default 500
// @param mode 0 0 3 int                 → integer 0–3, default 0

The knob displays the mapped value with the unit. paramChanged receives the mapped value directly — no manual scaling needed.

Boolean

// @param bypass false         → Off/On, default Off
// @param bypass true          → Off/On, default On
// @param bypass bool          → Off/On, default Off

paramChanged receives 0 or 1.

Supported mapping types

Type Description paramChanged receives
(none) Unipolar 0–1 number (0–1)
linear Linear scaling between min and max number (min–max)
exp Exponential scaling (for frequency, time) number (min–max)
int Integer snapping between min and max number (integer)
bool On/Off toggle number (0 or 1)

Parameters are reconciled on each compile: new parameters are added, removed parameters are deleted, and existing parameters keep their current value. Multiple spaces between tokens are allowed for alignment.

Groups

Organize parameters visually using // @group comments:

// @group Envelope green
// @param attack  0.01  0.001  1.0  exp  s
// @param decay   0.2   0      2.0  exp  s
// @param sustain 0.7
// @param release 0.5   0      5.0  exp  s
// @group Filter blue
// @param cutoff  1000  20  20000  exp  Hz
// @param resonance 0.5
// @group <name> [color]

Parameters and samples declared after a @group belong to that group until the next @group or end of declarations. Each group renders as a labeled section on the device panel with a colored header.

Available colors: blue, green, yellow, cream, orange, red, purple, white, gray, dark (default).

Parameters before any @group appear ungrouped.

Prefix stripping

When a parameter's name starts with the group's name (case-insensitive) followed by an uppercase letter, the group prefix is stripped from the displayed control label. The full parameter name is still used for automation, MIDI learn, and tooltips.

// @group Filter blue
// @param filterCutoff    1000  20  20000  exp  Hz   // shown as "Cutoff"
// @param filterResonance 0.5                        // shown as "Resonance"

If the next character is lowercase or the parameter name does not start with the group name, the label is shown unchanged.

Sidechains

Declare any number of sidechain inputs, each with a unique name:

// @sidechain Key
// @sidechain Modulator

Each @sidechain creates a source selector on the device panel. Click it to pick a track, a device or the input of an enclosing composite. Inside process the signal is available as this.sidechains.<name>, a [left, right] pair of Float32Array in the same block coordinates as the src and out buffers. An unconnected sidechain reads silence.

const [keyL, keyR] = this.sidechains.Key

4. Latency

Some processors cannot answer a sample right away. An FFT needs a full window before it can transform it, and a look-ahead limiter looks at audio that has not been played yet. Both output their audio later than it came in, which shifts the track against the rest of the mix.

Declare that delay in milliseconds and the engine compensates for it:

// @latency 21.3

The engine then delays every parallel path by the same amount, so the track stays in time with the rest of the mix. Automation and modulation on devices after Werkstatt follow the delayed audio, and mixdowns and stems start in time as well. The footer shows the total compensation as PDC.

If the delay changes while the script runs, for example when an FFT size parameter changes, set it in frames instead. A positive this.latency overrides the declaration:

class Processor {
    constructor() {
        this.latency = 1024
    }
    paramChanged(label, value) {
        if (label === "size") {this.latency = 256 << Math.round(value * 4)}
    }
    process(io, block) {
        // ...
    }
}

The engine reads this.latency after every block, so a change takes effect immediately.


5. Keyboard Shortcuts

Shortcut Action
Alt+Enter Compile and run
Ctrl+S / Cmd+S Compile, run, and save to project

6. Safety

The engine validates your output on every audio block:

When silenced, the device outputs silence until the next successful compile.


7. API Reference

Your code must define a class Processor with a process method. Optionally implement paramChanged to receive parameter updates and reset to clear state on transport stop.

Globals

Variable Type Description
sampleRate number Audio sample rate in Hz (e.g. 44100, 48000)

Processor class

class Processor {
    process(io, block) {
        // io.src[0], io.src[1] — input Float32Arrays (left, right)
        // io.out[0], io.out[1] — output Float32Arrays (left, right)
        //
        // block.s0    — first sample index to process (inclusive)
        // block.s1    — last sample index to process (exclusive)
        // block.index — block counter (increments each audio callback)
        // block.bpm   — current project tempo in beats per minute
        // block.p0    — start position in ppqn (pulses per quarter note, 960 ppqn)
        // block.p1    — end position in ppqn
        // block.flags — bitmask:
        //   1 (transporting) — transport is active
        //   2 (discontinuous) — position jumped (seek, loop restart)
        //   4 (playing)       — playback is active
        //   8 (bpmChanged)    — tempo changed this block
    }
    paramChanged(label, value) {
        // label — string matching the @param name
        // value — number between 0.0 and 1.0
    }
    reset() {
        // optional, called when the transport stops: clear buffers and tails
    }
    // this.latency: optional, the output delay in frames (overrides // @latency, see Latency)
}

Pressing Stop silences the device. If your script has no reset, the Processor is rebuilt on stop and all state in class fields starts fresh.


8. Custom Views

A script can place its own editor element into the device panel, store custom data and show live telemetry. Everything stays in the one script: the directive header, class Processor and the view classes.

// @view 2 1 8 3                   class View at column 2, row 1, 8 columns wide, 3 rows high
// @window Editor 900 520          a resizable floating window for the class Editor
// @collection steps 16            16 records ...
// @field steps.velocity 0.8 0 1 linear
// @buffer wave 256                256 floats
// @stream spectrum 512 fft        processor -> view telemetry
Directive What it does
@view [Class] <column> <row> <columns> <rows> Places a view into the 3-row panel grid, the knobs fill the free cells. Overlapping views are marked red.
@window [Class] <width> <height> A floating window, opened from the device header or host.openWindow().
@collection / @field / @record Records the view edits and the processor reads (this.data.<name>). Saved with the project, undoable.
@buffer <name> <length> Floats the view writes and the processor reads (this.data.<name>, a Float32Array).
@stream <name> <length> [value|ring|fft] Live values from the processor to the view (this.streams.<name>). Never saved.

Every view runs in its own sandboxed frame: it cannot reach the studio, the network or your files, and it shares no variables with the Processor. A script with a view therefore keeps its top level free of state: only class, function and const declarations, every top-level value is frozen. The code editor marks violations as you type.

In the view, the class receives host: host.params, host.data, host.streams, host.edit and host.gesture (one undo step), host.preview (play notes), host.send (reaches message(data) of the Processor) and host.color (the studio and display colours). Hover any of them in the code editor for details.

The mouse wheel over the view scrolls the device panel. A view that uses the wheel itself calls event.preventDefault() in its wheel listener to keep it.

A parameter the view controls itself does not need a knob. Mark it with [omit-default-ui] and attach it to the element that controls it, so a right click there opens the regular parameter menu (automation, modulation, MIDI learn, reset):

// @param cutoff 1000 20 20000 exp Hz [omit-default-ui]
host.attachParameter(cutoffHandle, "cutoff", {enterValue: true})

{enterValue: true} adds a double click that opens the same text field as on a regular knob, to type a value with its unit. Use it for knobs and sliders. Leave it out for checkboxes and buttons, where a double click should only toggle.

The parameter stays automatable, modulatable and MIDI learnable. host.params.cutoff.sources tells the view whether it is automated or modulated, value already includes both.

Community scripts with views exist for Apparat (Envelope Synth, Harmonic Synth, Wave Draw) and Spielwerk (Step Arp), the API is the same here. The AI starter prompt describes all of it.


9. Community Scripts

Select Community Scripts in the code editor toolbar to browse processors shared by the openDAW community, for example Hard Clipper, Ring Modulator, Simple Delay and Biquad Lowpass. Search by name, author or tag, preview the code and click Open to load one into the device. Paste a GitHub link to a script, a folder or a pull request into the field at the bottom to load a script straight from GitHub, so you can try it before it is published. How to contribute? explains how to share your own.


10. AI Assistance

Click Start AI-Prompt in the editor toolbar to copy a detailed starter prompt to your clipboard. Paste it into any AI assistant to get help writing Werkstatt processors. Once the AI generates code, copy it and click From Clipboard to load and compile it directly.

The prompt also covers custom views and carries a design guide derived from the stock devices, so a view the AI writes looks like part of openDAW.