From 9f1a0ac49542ffe2cee37289cbd2c7adb645a2f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Michelle?= Date: Thu, 19 Mar 2026 15:04:52 +0100 Subject: [PATCH] adds manual --- .../manuals/devices/instruments/apparat.md | 402 ++++++++++++++++++ .../manuals/devices/instruments/apparat.webp | Bin 0 -> 11476 bytes packages/app/studio/src/ui/pages/Manuals.ts | 6 + .../studio/adapters/src/DeviceManualUrls.ts | 1 + .../instruments/ApparatDeviceBoxAdapter.ts | 2 +- .../src/factories/InstrumentFactories.ts | 2 +- 6 files changed, 411 insertions(+), 2 deletions(-) create mode 100644 packages/app/studio/public/manuals/devices/instruments/apparat.md create mode 100644 packages/app/studio/public/manuals/devices/instruments/apparat.webp diff --git a/packages/app/studio/public/manuals/devices/instruments/apparat.md b/packages/app/studio/public/manuals/devices/instruments/apparat.md new file mode 100644 index 00000000..29b66c31 --- /dev/null +++ b/packages/app/studio/public/manuals/devices/instruments/apparat.md @@ -0,0 +1,402 @@ +# 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](apparat.webp) + +--- + +## 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 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. Parameters + +Declare parameters using `// @param` comments at the top of your code: + +```javascript +// @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 [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. + +--- + +## 3. Samples + +Declare samples using `// @sample` comments: + +```javascript +// @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.`. + +When loaded, `this.samples.wavetable` returns an `AudioData` object: + +```javascript +{ + 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: + +```javascript +const data = this.samples.grain +if (data === null) return +``` + +When playing back samples, account for sample rate differences between the sample and the project: + +```javascript +const playbackRate = data.sampleRate / sampleRate +``` + +--- + +## 4. Keyboard Shortcuts + +| Shortcut | Action | +|---------------------|-------------------------------------| +| `Alt+Enter` | Compile and run | +| `Ctrl+S` / `Cmd+S` | Compile, run, and save to project | + +--- + +## 5. 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. + +--- + +## 6. 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 + +```javascript +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 | + +--- + +## 7. Examples + +Select **Examples** in the code editor toolbar to load ready-made instruments (Simple Sine Synth, Grain Synthesizer). + +--- + +## 8. AI Prompt + +Copy the following prompt into an AI assistant to get help writing Apparat instruments: + +``` +You are helping the user write an instrument processor for the openDAW Apparat device. +The user writes plain JavaScript (no imports, no modules). The code runs inside an AudioWorklet. + +The code MUST define a class called `Processor` with the following interface: + +class Processor { + noteOn(pitch, velocity, cent, id) { } // called at exact sample position + noteOff(id) { } // called at exact sample position + reset() { } // called on transport stop — fast-release all voices + paramChanged(label, value) { } // optional + process(output, block) { } // generate audio +} + +## noteOn(pitch, velocity, cent, id) +Called when a note starts, at the exact sample position within the audio block. + +- pitch: number (0–127) — MIDI note number +- velocity: number (0.0–1.0) — note velocity +- cent: number — fine pitch offset in cents +- id: number — unique identifier for this note instance + +Convert pitch to frequency: 440 * Math.pow(2, (pitch - 69 + cent / 100) / 12) + +IMPORTANT: Always use `id` to identify notes, not `pitch`. Multiple notes on the same +pitch can be active simultaneously (e.g. overlapping regions, multiple MIDI sources). + +## noteOff(id) +Called when a note ends. Find the voice by `id` and release it. + +## reset() +Called on transport stop and position jumps. Put all voices into a fast release +state (e.g., set a short fade rate like 0.05) to avoid clicks. Do NOT hard-kill +voices with `this.voices = []` — that causes clicks. + +Example: +reset() { + for (const voice of this.voices) { + voice.gate = false + voice.fadeRate = 0.05 // fast ~5ms fade + } +} + +## process(output, block) +Called between note events. The host clears the output buffer before each block. +You write audio samples to the output buffers. + +- output[0]: Float32Array — left channel (write to this) +- output[1]: Float32Array — right channel (write to this) + +block (timing and transport): +- block.s0 — first sample index to process (inclusive) +- block.s1 — last sample index to process (exclusive) +- block.index — block counter +- block.bpm — current project tempo in BPM +- block.p0 — start position in ppqn (480 ppqn per quarter note) +- block.p1 — end position in ppqn +- block.flags — bitmask: 1=transporting, 2=discontinuous, 4=playing, 8=bpmChanged + +The host interleaves noteOn/noteOff calls with process() calls at exact sample +positions. Your process() may be called multiple times per block: + + process(output, {s0: 0, s1: 47}) // render existing voices + noteOn(60, 0.8, 0, 42) // note starts at sample 47 + process(output, {s0: 47, s1: 128}) // render with new voice + +## paramChanged(label, value) +Called when a parameter knob changes value. +- label — string, matches the name from the @param comment +- value — the mapped value (number). For unipolar: 0.0–1.0. For linear/exp: min–max. + For int: integer in min–max. For bool: 0 or 1. + +## Declaring parameters +Parameters are declared as comments at the top of the file: + +// @param [default] [min max type [unit]] + +Supported types: linear, exp, int, bool. +If no type is given, the parameter is unipolar (0–1). +If the default is "true" or "false", the type is bool. +Multiple spaces between tokens are allowed for alignment. + +Examples: +// @param attack 0.01 0.001 1.0 exp s +// @param mode 0 0 3 int +// @param bypass false + +## Declaring samples +Samples are declared as comments: + +// @sample + +Each @sample creates a file picker on the device panel. Access the loaded audio via +this.samples.. Returns null before loaded. + +When loaded, this.samples. is an AudioData object: +- sampleRate: number — the sample's own sample rate +- numberOfFrames: number — total frames +- numberOfChannels: number — 1 (mono) or 2 (stereo) +- frames: [Float32Array, ...] — per-channel audio data + +IMPORTANT: The sample's sampleRate may differ from the project sampleRate. +When playing back samples, always account for this: + const playbackRate = data.sampleRate / sampleRate +Advance your read position by playbackRate per output sample. + +## Globals +- sampleRate — number, the project audio sample rate in Hz (e.g. 48000). + Always use this instead of hardcoding a sample rate. + +## Constraints +- NEVER allocate memory inside process(). No `new`, no array literals, no object + literals, no string concatenation, no closures. Any allocation in the audio hot path + causes GC pauses and audio glitches. Pre-allocate all buffers and state as class fields. +- Output is validated every block. NaN or amplitudes > 1000 will silence the processor. +- Do not use import/export/require. No access to DOM or fetch. +- The code runs in an AudioWorklet thread. Only AudioWorklet-safe APIs are available + (Math, typed arrays, basic JS). No console, no setTimeout, no DOM. +- You can define and use helper classes alongside the Processor class. + +## Template + +// @param attack 0.01 0.001 1.0 exp s +// @param release 0.3 0.01 2.0 exp s + +class Processor { + voices = [] + attack = 0.01 + release = 0.3 + paramChanged(name, value) { + if (name === "attack") this.attack = value + if (name === "release") this.release = value + } + noteOn(pitch, velocity, cent, id) { + this.voices.push({ + id, velocity, + freq: 440 * Math.pow(2, (pitch - 69 + cent / 100) / 12), + phase: 0, gain: 0, gate: true, releaseTime: this.release + }) + } + noteOff(id) { + const voice = this.voices.find(v => v.id === id) + if (voice) voice.gate = false + } + reset() { + for (const voice of this.voices) { + voice.gate = false + voice.releaseTime = 0.005 + } + } + process(output, block) { + const [outL, outR] = output + const attackRate = 1 / (this.attack * sampleRate) + for (let i = this.voices.length - 1; i >= 0; i--) { + const voice = this.voices[i] + const releaseRate = 1 / (voice.releaseTime * sampleRate) + for (let s = block.s0; s < block.s1; s++) { + if (voice.gate) { + voice.gain += (voice.velocity - voice.gain) * attackRate + } else { + voice.gain -= voice.gain * releaseRate + if (voice.gain < 0.001) { + this.voices.splice(i, 1) + break + } + } + const sample = Math.sin(voice.phase * Math.PI * 2) * voice.gain * 0.3 + outL[s] += sample + outR[s] += sample + voice.phase += voice.freq / sampleRate + } + } + } +} +``` diff --git a/packages/app/studio/public/manuals/devices/instruments/apparat.webp b/packages/app/studio/public/manuals/devices/instruments/apparat.webp new file mode 100644 index 0000000000000000000000000000000000000000..7035c526a6a6e8388e550be4f04d21972b04abd8 GIT binary patch literal 11476 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 = [ type: "folder", label: "Instruments", files: [ + { + type: "page", + label: "Apparat", + path: "/manuals/devices/instruments/apparat", + icon: InstrumentFactories.Apparat.defaultIcon + }, { type: "page", label: "MIDIOutput", diff --git a/packages/studio/adapters/src/DeviceManualUrls.ts b/packages/studio/adapters/src/DeviceManualUrls.ts index c54c5f40..7e8c1105 100644 --- a/packages/studio/adapters/src/DeviceManualUrls.ts +++ b/packages/studio/adapters/src/DeviceManualUrls.ts @@ -27,6 +27,7 @@ export namespace DeviceManualUrls { export const Werkstatt = "manuals/devices/audio/werkstatt" // Instruments + export const Apparat = "manuals/devices/instruments/apparat" export const Tape = "manuals/devices/instruments/tape" export const Nano = "manuals/devices/instruments/nano" export const Playfield = "manuals/devices/instruments/playfield" diff --git a/packages/studio/adapters/src/devices/instruments/ApparatDeviceBoxAdapter.ts b/packages/studio/adapters/src/devices/instruments/ApparatDeviceBoxAdapter.ts index 388d6f16..8fc34174 100644 --- a/packages/studio/adapters/src/devices/instruments/ApparatDeviceBoxAdapter.ts +++ b/packages/studio/adapters/src/devices/instruments/ApparatDeviceBoxAdapter.ts @@ -16,7 +16,7 @@ export class ApparatDeviceBoxAdapter implements InstrumentDeviceBoxAdapter { readonly type = "instrument" readonly accepts = "midi" - readonly manualUrl = "" + readonly manualUrl = DeviceManualUrls.Apparat readonly #context: BoxAdaptersContext readonly #box: ApparatDeviceBox diff --git a/packages/studio/adapters/src/factories/InstrumentFactories.ts b/packages/studio/adapters/src/factories/InstrumentFactories.ts index cf97da11..ef24e9fb 100644 --- a/packages/studio/adapters/src/factories/InstrumentFactories.ts +++ b/packages/studio/adapters/src/factories/InstrumentFactories.ts @@ -191,7 +191,7 @@ export namespace InstrumentFactories { defaultName: "Apparat", defaultIcon: IconSymbol.Code, description: "User-scripted instrument", - manualPage: "", + manualPage: DeviceManualUrls.Apparat, trackType: TrackType.Notes, create: (boxGraph: BoxGraph, host: Field,