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.


screenshot


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:


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

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 +=.

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.