Braided Audio
WAVE Braided Audio publishes N synchronized audio channels — stems, languages, camera-angle commentary, a multichannel bed — as one MoQ track of interleaved multi-channel PCM, instead of N independent tracks. A subscriber reconstructs a single shared clock from that one track and applies a per-channel gain (a fader). It is the structural cure for a whole class of stem-silence / drift bugs: N independent MoQ tracks force a player to re-derive timing from N separately-arriving, independently backlogged streams; one interleaved track has one arrival timeline, so channels are phase-locked by construction.
Use cases: stem players and remix decks, karaoke, multi-language commentary, immersive/spatial beds, and multichannel audio-over-IP delivered over the web.
Braided Audio is live today as the WAVE Listen stem mixer at listen.wave.online/s/chillwave-stems (four lofi instrument faders — drums, bass, keys, guitar). The customer-callablePOST /v1/braid/publishcontrol route is live — call it with@wave-av/sdkor raw HTTP, below. The@wave-av/braid-audioclient decode package described further down is coming and is not yet published to npm.
Wire contract (frozen)
Every published MoQ object is:
[ 16-byte sync header ][ interleaved S16LE payload ]
Sync header (16 bytes, big-endian)
| offset | field | type | meaning |
|---|---|---|---|
| 0 | magic | u16 BE | 0x5742 ("WB", WaveBraid). Non-braid objects are ignored, never misread. |
| 2 | version | u8 | 1 |
| 3 | channels | u8 | interleaved channel count, 1–255 |
| 4 | epoch | u32 BE | publisher-run id; a restart bumps it so the subscriber re-primes deterministically |
| 8 | sampleIndex | u64 BE | per-channel frame index of this window's first frame (monotonic within an epoch) |
The subscriber times playback from {epoch, sampleIndex} — never the MoQ object id — so a relay backlog burst or a publisher restart is a deterministic resync, not silent drift. A gap or backward step in sampleIndex, or a change in epoch, signals a discontinuity and triggers a re-prime.
Payload — interleaved S16LE
channels mono streams interleaved per frame, little-endian signed 16-bit. Frame k, channel c sits at byte 16 + (kchannels + c)2. S16LE (not S32LE) halves bandwidth.
Object-size envelope
Every object must stay at or below ~38400 bytes so it survives as a single unfragmented browser delivery. Object bytes = 16 + windowFrames channels 2, so the publish window shrinks as channel count grows:
| channels | max windowFrames | max window @ 48kHz |
|---|---|---|
| 4 | 4800 | 100 ms |
| 5 | 3840 | ~79 ms |
| 6 | 3200 | ~66 ms |
| 8 | 2400 | ~49 ms |
Manifest
A reserved wave-manifest control track carries the JSON manifest the subscriber reads first:
{
"v": 2,
"mode": "braid",
"track": "stems", // the single interleaved audio track to subscribe to
"format": "s16le",
"sampleRate": 48000,
"channels": 4,
"windowFrames": 4800,
"stems": [ // channel index i == stems[i]; drives the faders
{ "index": 0, "track": "drums", "label": "Drums" },
{ "index": 1, "track": "bass", "label": "Bass" },
{ "index": 2, "track": "keys", "label": "Keys" },
{ "index": 3, "track": "guitar", "label": "Guitar" }
]
}
Every channel set is validated sample-identical before it goes live — a set that would drift never publishes.
Publish API
POST /v1/braid/publish
Requires gateway auth scoped moq:write (agent callers: this route is x402-payable — an unauthenticated call answers 402 with a payment offer, not a stub). Orchestrates one braid publisher — merges the given sources into interleaved S16LE, sync-headers each object, and writes the audio track plus the wave-manifest control track.
Using @wave-av/sdk (npm i @wave-av/sdk — resolves; npm view @wave-av/sdk version → current latest):
import { WaveClient } from "@wave-av/sdk";
const client = new WaveClient({ apiKey: process.env.WAVE_API_KEY });
const result = await client.post("/v1/braid/publish", {
ns: "chillwave-stems",
sources: [
{ label: "Drums", track: "drums", url: "https://storage.example.com/chillwave/drums.wav" },
{ label: "Bass", track: "bass", url: "https://storage.example.com/chillwave/bass.wav" },
],
windowMs: 100,
});
// result: { ns, track, channels, windowMs, objectBytes, machineId, status: "starting" }
WaveClient doesn't ship a dedicated Braided Audio module yet — client.post/client.get/client.delete are the same low-level HTTP methods every typed SDK module (client.transcribe, client.pricing, …) is built on, so calling the route directly is a normal, supported use of the SDK, not a workaround.
Raw HTTP
curl -X POST https://api.wave.online/v1/braid/publish \
-H "Authorization: Bearer $WAVE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"ns": "chillwave-stems",
"sources": [
{ "label": "Drums", "track": "drums", "url": "https://storage.example.com/chillwave/drums.wav" },
{ "label": "Bass", "track": "bass", "url": "https://storage.example.com/chillwave/bass.wav" }
],
"windowMs": 100
}'
ns— the MoQ namespace to publish under. Republishing the samensreplaces the prior machine.sources— 2 or more channel sources, each alabel+trackand either aurlor apath.windowMs— optional; auto-clamped to the object-size envelope for the resulting channel count.
Response (201)
{
"ns": "chillwave-stems",
"track": "stems",
"channels": 2,
"windowMs": 100,
"objectBytes": 9616,
"machineId": "braid-...",
"status": "starting"
}
To stop early: DELETE /v1/braid/publish/{ns} (also moq:write) tears the machine down and returns { ns, machineId, status: "stopped" }. 501 on either route means the braid worker isn't provisioned in that environment.
Subscribing reuses the standard MoQ subscribe/token-mint path — a braid subscriber is an ordinary MoQ subscriber that also reads the wave-manifest track.
Client SDK — @wave-av/braid-audio (coming)
Not yet published to npm —npm view @wave-av/braid-audio404s. Do notnpm installit yet; the shape below is the planned API, not something you can run today.
A framework-agnostic core (DataView-only — runs unchanged in a browser, a Web Worker, or Node) for decoding the wire format on the receive side:
import { decodeBraidObject } from "@wave-av/braid-audio"; // planned — not yet on npm
// obj is one MoQ object payload (header + interleaved PCM)
const { channels, epoch, sampleIndex, channelsData } = decodeBraidObject(obj);
// channelsData[i] is a Float32Array in [-1, 1) for channel i — feed each to a GainNode fader.
On top of the core, a documented browser layer wires each decoded channel to a ChannelSplitter → N GainNode faders → a shared master gain, reusing a bounded play-head (drop-on-overrun, rebuild-on-underrun, re-prime on epoch change) so the browser stays on the live edge. Until this ships, decode the wire format (above) directly against the frozen contract.
Constraints
- 48kHz fixed sample rate.
- 2 or more channel sources per braid.
- Per-org concurrency and fleet-wide braid caps are operational limits enforced at the gateway; exact
values are set per environment.
See What WAVE speaks for the transports Braided Audio rides on, and the API reference for the full contract as it ships.