Apparat
A programmable instrument that lets you write custom synthesizers and samplers in JavaScript. Generate audio from note events, load samples for granular synthesis, declare parameters with knobs, and hot-reload changes in real time.

0. Overview
Apparat is a scriptable instrument device. You write a Processor class in JavaScript that receives note events and generates stereo audio. Parameters and samples declared in the code appear as automatable knobs and file pickers on the device panel.
Example uses:
- Custom synthesizers (additive, subtractive, FM, wavetable)
- Sample playback with pitch tracking
- Granular synthesis
- Algorithmic sound generators
- Prototyping new instrument ideas
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:
- Compile — Compile and load the code into the audio engine
- Community Scripts — Browse and load instruments shared by the community
- From Clipboard — Paste code from the clipboard into the editor and compile it in one step
- Start AI-Prompt — Copy a device-specific AI starter prompt to the clipboard, ready to paste into an AI assistant (e.g. ChatGPT, Claude) for help writing instruments
- Close Editor — Return to the previous view
The status bar at the bottom shows the current state:
- Idle — No compilation attempted yet
- Successfully compiled — Code compiled and loaded into the audio engine
- Error message — Syntax error, runtime error, or validation failure
2. Label
Set the device name using a // @label comment:
// @label My Synth
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 attack 0.01 0.001 1.0 exp s
// @param release 0.3 0.01 2.0 exp s
// @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
Mapped
// @param attack 0.01 0.001 1.0 exp s → exponential 0.001–1.0, default 0.01
// @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.
Boolean
// @param bypass false → Off/On, default Off
// @param bypass true → Off/On, default On
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.
4. Samples
Declare samples using // @sample comments:
// @sample wavetable
// @sample grain
Each @sample creates a file picker on the device panel. Drag an audio file onto it or click to browse. The sample data is available in the processor as this.samples.<name>.
When loaded, this.samples.wavetable returns an AudioData object:
{
sampleRate: number, // original sample rate
numberOfFrames: number, // number of frames
numberOfChannels: number, // 1 (mono) or 2 (stereo)
frames: [Float32Array, Float32Array] // per-channel sample data
}
Returns null before the sample is loaded. Always check:
const data = this.samples.grain
if (data === null) return
When playing back samples, account for sample rate differences between the sample and the project:
const playbackRate = data.sampleRate / sampleRate
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 output buffers. An unconnected sidechain reads silence.
const [keyL, keyR] = this.sidechains.Key
Groups
Organize parameters and samples visually using // @group comments:
// @group Envelope green
// @param attack 0.01 0.001 1.0 exp s
// @param release 0.5 0 5.0 exp s
// @group Samples orange
// @sample wavetable
// @sample grain
// @param volume 0.8
// @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 and samples 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. Sample labels are not affected.
// @group Envelope green
// @param envelopeAttack 0.01 0.001 1.0 exp s // shown as "Attack"
// @param envelopeRelease 0.5 0 5.0 exp s // shown as "Release"
If the next character is lowercase or the parameter name does not start with the group name, the label is shown unchanged.
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:
- NaN detection — If any output sample is NaN, the processor is silenced and the error is reported.
- Overflow protection — If any sample exceeds ~60 dB (amplitude > 1000), the processor is silenced.
- Runtime errors — If
process()throws an exception, the processor is silenced and the error is shown.
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 noteOn, noteOff, reset, and paramChanged.
Globals
| Variable | Type | Description |
|---|---|---|
sampleRate |
number |
Audio sample rate in Hz (e.g. 48000) |
Processor class
class Processor {
noteOn(pitch, velocity, cent, id) { } // note starts (sample-accurate)
noteOff(id) { } // note ends (sample-accurate)
reset() { } // transport stop — fast-release all voices
paramChanged(label, value) { } // parameter knob changed
process(output, block) { } // generate audio
}
Note methods
noteOn and noteOff are called at the exact sample position within the block. The host splits the block at event boundaries and calls process() between them:
[host clears output buffer]
process(output, {s0: 0, s1: 47, ...}) // existing voices render
noteOn(60, 0.8, 0, 42) // note starts at sample 47
process(output, {s0: 47, s1: 128, ...}) // new voice renders
| Parameter | Type | Description |
|---|---|---|
pitch |
number |
MIDI note number (0–127) |
velocity |
number |
Note velocity (0.0–1.0) |
cent |
number |
Fine pitch offset in cents |
id |
number |
Unique note identifier (use for noteOff) |
Always use id to identify notes — not pitch. Multiple notes on the same pitch can be active simultaneously.
reset()
Called on transport stop and position jumps. Put all voices into a fast release (e.g., 5ms fade) to avoid clicks. Do NOT hard-kill voices with this.voices = [].
process(output, block)
The host clears the output buffer before each block. You write to it with = or +=.
output[0]— left channel (Float32Array)output[1]— right channel (Float32Array)
Block properties
| Property | Type | Description |
|---|---|---|
s0 |
number |
First sample index to process (inclusive) |
s1 |
number |
Last sample index to process (exclusive) |
index |
number |
Block counter |
bpm |
number |
Current project tempo |
p0 |
number |
Start position in ppqn |
p1 |
number |
End position in ppqn |
flags |
number |
Bitmask: 1=transporting, 2=discontinuous, 4=playing, 8=bpmChanged |
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.
9. Community Scripts
Select Community Scripts in the code editor toolbar to browse instruments shared by the openDAW community. 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 Apparat instruments. 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.