Orbiters audio architecture
Where the DSP runs, and what a voice costs. Rewritten 2026-08-24 to the dimension-chain model.
6. The model
A dimension is a chain
One serial chain, one to three modules, in a fixed type order — the signal must be created before it can be processed:
- MIDI effect — optional. Operates on notes, so it sits before anything that produces sound.
- Instrument — exactly one, in a head chain: one whose stage receives no signal. A chain that already receives signal (a downstream stage, or any Moon chain — its input is the send) is effects-only and may not hold an instrument.
- Audio effects — optional, up to the chain limit. Downstream of the instrument only; their order among themselves is free and musically meaningful (filter → delay ≠ delay → filter).
{module, parameter, min, equilibrium, max, curveLow, curveHigh} reaching any parameter in the chain. One macro may drive many parameters; a parameter is driven by at most one macro.Routing is between dimensions
Dimensions wire freely as an ordered list of stages: each stage holds one or more dimensions in parallel — they see the same input and sum — and stages run in series. Common shapes: I → II → III, I → II + III, I + II → III, I + II + III. Edit verbs: move earlier / later, parallel with previous, split to its own stage. Inside a dimension the chain is always serial — no wiring choice there.
In the World, dimension 1 carries the generator and is always first — anything upstream of it would receive silence. The Moon’s stages take the send as their input.
I + II → III etc.). The wiring lives in the stored description and is structural — changing it recompiles the voice. Today’s code hardwires serial (AudioEngineAdapter.js:3827-3858, :4318); that becomes the default, not a fact of the engine.Mix — a third entity (to design)
Alongside World and Moon: 1–3 dimensions (author picks), X/Y/Z macros, no instrument. Its mappings target the high level — world level, moon level, the world↔moon mix (a level pair, not a new node), pan — plus body macros, so one Mix macro moves both bodies at once (Mix → macros → parameters keeps “one parameter, one macro” intact). Sends are not a Mix target. Own accent and glyph; the Instrument tab shows only the mapping list.
One panel per Orbiter
The panel — theme preset, colours, radius, font, ring — moves from the dimension to the Orbiter: stored once, applied to every view, edited above the body and dimension selectors. The per-dimension design block (orbiterFallback.js:49-72) goes away with the format change; migration keeps dimension 1’s panel and drops the rest.
Stored description
orbiter
├── panel theme, colours, radius, font, ring — one for everything
├── mix third entity — no instrument; macros → mixer + body macros
│ └── dimensions[1..3] (open: a master effects chain?)
└── bodies planet (world), moon
├── routing dims serial | parallel (+ send pre/post-fader)
└── dimensions[1..3]
├── enabled
├── chain [midiFx?] → instrument → audioFx* (≤3 modules)
│ └── modules[] module id, preset, parameter values
└── macros
├── x → mappings[] { module, parameter, min, equil, max, curveLow, curveHigh }
├── y → mappings[]
└── z → mappings[]
The description is versioned from the first record written. Modules ship parameter tables only; no per-module macros, no factory presets.
1. What a voice costs today
rack.js:619-629), 9 per-dimension (AudioEngineAdapter.js:3809-3821), ~8 outer. Per-sample JS worklets: bitcrusher and both reverbs (both are JCReverb). “N nodes per voice” is the wrong unit — count DSP units and graph nodes separately.2. Target — one compiled unit per voice
All chains, both bodies, one FAUST program per voice, compiled on save. The gain rail stays outside it.
IN trim before the unit, OUT fader after it, both metered via createLevelTap (AudioEngineAdapter.js:2286) — fold them in and the tap points vanish.3. Two runtimes, one DSP source
| Edit runtime | Play runtime | |
|---|---|---|
| Shape | one node per module, hot-swappable | one compiled unit per voice, built on save |
| For | authoring — instant chain edits | listeners and rooms — one process() per voice |
| Source | the same per-module .dsp, compiled standalone (edit) or composed via component() (play). Smoothing lives in the .dsp, never in the host. |
4. What triggers a recompile
FAUST compiles every branch in the program: a module bypassed at runtime still costs full price; only a module absent at compile time is free. A playing voice is never switched — a recompile applies to the next voice.
| Change | Cost | Declared as |
|---|---|---|
| Any macro or parameter move, the send level | a parameter write | reactivity: 'live' |
Filter type / model / slope, granular anchored | recompile | reactivity: 'structural' |
| Chain edits, dimension routing, macro re-mapping | recompile | the stored description |
5. What stays outside the unit
| Stays outside | Why |
|---|---|
| Decode, streaming, Signalsmith stretch | Not FAUST; already WASM where it matters. The instrument module wraps them. |
The clock — Tone.now() via toneClockBackend.js | Already the audio hardware clock; rooms are live. Out of scope. |
| The sync layer | Sockets, async. FAUST has no I/O. |
| Cosmic LFO and every axis control value | Arbitrated in PRIORITY_MAP, room-replicated, drives the knob. Correct on rAF. |
The shared master — MultiOrbiterAudioHost | One bus and limiter across N voices. |
| The gain rail — norm / IN / OUT | Metering taps and persisted MIDI-mapped ids. See §2. |
What does go inside, beyond the DSP: a module’s own internal modulators, driven by a phase anchor + rate handed in by the JS transport. The unit gets no clock.