Multichannel audio bundles
This document answers how an audio cable carries 1 to 32 lanes, how widths are inferred through nested definitions, which routers exist, and how each host maps logical lanes to rea
This document answers how an audio cable carries 1 to 32 lanes, how widths are inferred through nested definitions, which routers exist, and how each host maps logical lanes to real buses.
Status: bounded 1–32-channel source inference, shared Rust processing, bundle routing, observations and host adapters are implemented; every DSP processor builds on them. Surround speaker layouts, ambisonic processing, hardware configurations and DAWs are not qualified.
Portable contracts and inference#
An audio port has a fixed channels count from 1 to 32 or "auto", plus optional ordered labels. Labels constrain the width to their length and describe lanes; they do not imply a speaker layout. Logical channels and device assignments stay separate, and no device identifier is stored in a Toy or session.
Primitive version 0.1.0 retains the four fixed-stereo foundation contracts and algorithms. Version 0.2.0 supports automatic widths through the same gain, low-pass, delay and oscillator algorithms. Legacy Toys are not rewritten on load; changing a legacy node's channel fields in the inspector upgrades that node to 0.2.0, keeping its two channels, in one operation with Undo.
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#FAFAF7', 'primaryTextColor': '#1C1C1B', 'primaryBorderColor': '#1C1C1B', 'lineColor': '#64645F', 'fontFamily': 'DM Sans, Inter, system-ui, sans-serif'}}}%%
flowchart LR
IN["$in<br/>channels: 2"] --> G["Group instance A<br/>ports: auto"]
IN2["Oscillator<br/>channels: 6"] --> G2["Group instance B<br/>same definition, own variables"]
G --> OUT["$out<br/>channels: auto → 2"]
G2 --> OUT2["$out<br/>channels: auto → 6"]
SOLVE["Solver<br/>fixed/equality · concatenation · max-width<br/>runs before preparation"]
SOLVE -. "resolves" .-> G
SOLVE -. "resolves" .-> G2
classDef source fill:#F0442E,stroke:#1C1C1B,color:#FAFAF7
classDef shared fill:#DCEBDD,stroke:#1C1C1B,color:#1C1C1B
classDef host fill:#DED8F0,stroke:#1C1C1B,color:#1C1C1B
class IN,IN2 source
class G,G2,SOLVE shared
class OUT,OUT2 hostpackages/node-catalog/src/channels.ts and crates/compiler/src/channels.rs resolve fixed/equality, concatenation and maximum-width constraints before preparation. Each nested definition instance receives separate variables, so one definition can run at different widths. Fixed ports constrain inferred ports in either direction. Derived widths resolve before otherwise unconstrained lanes default to mono. Sources default to mono in version 0.2; a declared count prepares independent generator states. Public numeric controls currently apply to every lane; per-channel automation arrays remain planned. The same solver resolves video streams and control lanes, with shared SELECTORS and COMBINERS per type (signal types and domains); control and MIDI cables are described in MIDI, control and mixing.
| Input adaptation policy | Behavior |
|---|---|
strict (default) | Requires matching widths |
broadcast | Expands only mono inputs |
pad | Fills missing lanes with silence |
repeat | Cycles narrower bundles |
Primitive audio configuration declares an optional output width and one of these policies. No policy reduces a wider input; reduction or selection requires an explicit router. Connections retain one driver per inlet; the Mixer exposes intentional fan-in.
Inference and schema checks cover unused definitions as well as active instances. A malformed label count, incompatible fixed constraint, unsupported legacy configuration, out-of-range selector or matrix route is rejected. The existing source byte/depth/expansion budgets still apply. The 32-channel limit is an execution-profile admission bound, not a special stereo/surround format.
Mixing and routing#
All four routers use version 0.2.0:
| Primitive | Behavior |
|---|---|
std.mixer — Mixer | Up to 32 channels, each with an ID-scoped audio inlet, dB gain, pan, mute and solo. Broadcast adaptation infers the widest input unless an output width is configured. Reorder is presentation only. |
std.combine — Combine channels | Concatenates inputs A and B in that order. No summing. The output count is their combined width. |
std.select — Select channels | Extracts a contiguous range beginning at the one-based audio.firstChannel, with audio.channels outputs (default one). Multiple selectors split a bundle. |
std.matrix — Channel matrix | A fixed output width and up to 256 explicit audio.routes, each with one-based from, to and gain −8…8. Routes add without normalization; omitted destinations are silent. An empty route list is silence. |
The Channels section of a node's Properties edits output counts, adaptation policies, selection ranges, matrix and Delay feedback routes, and, on Audio input and Audio output, port widths and channel names. It appears only on nodes whose width can be configured: combiners derive theirs, and converters, MIDI, data and bus nodes have none. Each change is one Undo step; a combination the solver rejects stays as a draft beside its error until another field makes it valid or it is reverted. Ports and cables show inferred widths for the selected nested instance. Changes use the existing source validator, authoring history and renderer; no second writable graph exists. Node discovery and auditions use the same contracts, and temporary audition input adapters are real admitted matrix nodes.
Prepared engine and ABI#
Compiled Program 2 carries explicit input/output counts, operator widths, input bus widths, adaptation policies and prepared routes. Program 1 remains accepted with stereo defaults for older serialized programs. Input arity and numeric vectors are sized per operator during preparation. Fixed members and canonical ID-ordered repeat members share a generated maximum of 72 input slots and 288 parameter slots. Shared generated DSP contracts provide discrete selectors and control metadata.
The engine allocates channel-sized scratch, filter history, delay history, generator phases and observation storage during preparation and checks channel-weighted buffers against the 128 MiB profile. Processing uses prepared lane maps, bounded loops and fixed stack scratch; it does not infer widths, allocate, lock or perform I/O. A changed topology is a replacement prepared generation, and existing host candidate failure handling retains the active program.
ABI 1 gains additive tt_input_channels, tt_output_channels, tt_signal_channels and tt_meter_channel queries. Buffer views contain interleaved max_frames × channels floats. Legacy meter fields keep their original meanings; the new channel queries expose every lane. Frame meters render the supplied lane arrays through the shared renderer, with an expandable, bounded list for bundles wider than stereo. Tail preparation includes mixer/matrix gain bounds and all relevant input paths.
Host mappings#
| Host | Input | Output / monitoring |
|---|---|---|
| Browser worklet | Discrete source channels; Audio settings choose ordered zero-padding or repetition | The worklet preserves logical lanes. A browser-local device mixer applies explicit discrete routing, alternating-lane stereo fold, first pair or mono average through native Web Audio nodes; per-device strips add gain/mute/solo/polarity and lane overrides. Offline WAV rendering keeps the logical output count |
| JUCE adapter | 0–32 bus channels; missing lanes are zero; a mono logical input averages supplied host channels | 1–32 bus channels; matching widths map directly; differing counts fold lanes cyclically, averaging contributors per destination; mono duplicates for two-channel monitoring |
The browser preserves the engine's logical bundle. Legacy programs keep the former mono-input repetition default. Routing policies, device bindings and strip identities/levels are browser-local host preferences, not Toy fields. See the device mixer. Native snapshot metadata describes the mappings and actual host counts and is explicitly excluded from serialized DAW state. The default product bus remains stereo. Fixtures qualify software behavior; physical devices, channel-layout conventions and actual DAW configurations require separate qualification.
Evidence and references#
The shared channel corpus (tests/conformance/channel-cases.mjs) checks browser, native and WASM admission, including repeated generic definitions at different widths. Runtime fixtures (packages/runtime-web/test/multichannel.test.mjs) exercise 1/2/3/5/8/16/32 lanes, per-lane telemetry, independent instances, explicit adaptation and routing, memory rejection, native/WASM PCM comparisons and block partitioning at 44.1/48/96 kHz. Rust and native host allocation fixtures include multichannel callbacks. Browser checks cover channel editing, inference, Undo, matrix configuration and monitor controls. Root npm run check remains required.
The design uses Cycling '74's documented MC bundles and inlet summing, wrapper expansion, replication policies, combining and mixdown as behavioral references. Implementation and fixtures are original TuneToys code; no Max, Hotwire or synth.ist source or sibling build dependency was added.
Explicit speaker-layout matrices, multichannel spatial reverbs, per-lane automation/voice allocation, floating channel counts inside an active generation, external audio buses (side-chains, media and recorder audio) and MIDI 2.0 remain planned.
Implementation#
packages/node-catalog—src/channels.tscrates/compiler—src/channels.rs;crates/engine;crates/engine-ffipackages/runtime-webandnative/plugin-shell— host mappings