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:

  1. MIDI effect — optional. Operates on notes, so it sits before anything that produces sound.
  2. 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.
  3. 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).
MIDI effectoptional · notesINSTRUMENTrequired · exactly oneaudio effectaudio effectorder free among effects X · Y · Z macros each macro → any parameters, anywhere in this dimension’s chain
The chain is owned by the dimension. Macros are decoupled: X, Y and Z each carry an ordered mapping list {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.

SERIALdim 1dim 2dim 3each processes the previous one’s output PARALLELdim 1dim 2dim 3side by side from the same input, merged
Serial and parallel are the two poles; the stage list expresses everything between (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

playerEW::I racksEW::II racksEW::III racksgain raillimiter → outevery rack builds 3 nodes even with no effect loaded — ~44 graph nodes before any module DSP
One Web Audio node per module, plus plumbing: ~27 nodes from the nine racks alone (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.

ONE compiled FAUST unit normINPLANET — dimension chains[midi?] → instrument → fx* · dims serial or parallelMOON — parallel bus, same shapesend → chains → returnOUTmaster−0.5 dB → limitersend = travel to the Moonreturn≤3 modules per dimension · 6 macros (3 planet + 3 moon) · gain rail unchanged
The instrument is a chain module (the track player module wraps the existing player — decode, streaming and Signalsmith stretch stay their own nodes, not FAUST). The two user gain stages stay where they are: 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 runtimePlay runtime
Shapeone node per module, hot-swappableone compiled unit per voice, built on save
Forauthoring — instant chain editslisteners and rooms — one process() per voice
Sourcethe 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.

ChangeCostDeclared as
Any macro or parameter move, the send levela parameter writereactivity: 'live'
Filter type / model / slope, granular anchoredrecompilereactivity: 'structural'
Chain edits, dimension routing, macro re-mappingrecompilethe stored description

5. What stays outside the unit

Stays outsideWhy
Decode, streaming, Signalsmith stretchNot FAUST; already WASM where it matters. The instrument module wraps them.
The clock — Tone.now() via toneClockBackend.jsAlready the audio hardware clock; rooms are live. Out of scope.
The sync layerSockets, async. FAUST has no I/O.
Cosmic LFO and every axis control valueArbitrated in PRIORITY_MAP, room-replicated, drives the knob. Correct on rAF.
The shared master — MultiOrbiterAudioHostOne bus and limiter across N voices.
The gain rail — norm / IN / OUTMetering 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.