Media

Money

Dispatch

Company

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-callable POST /v1/braid/publish control route is live — call it with @wave-av/sdk or raw HTTP, below. The @wave-av/braid-audio client 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)

offsetfieldtypemeaning
0magicu16 BE0x5742 ("WB", WaveBraid). Non-braid objects are ignored, never misread.
2versionu81
3channelsu8interleaved channel count, 1–255
4epochu32 BEpublisher-run id; a restart bumps it so the subscriber re-primes deterministically
8sampleIndexu64 BEper-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:

channelsmax windowFramesmax window @ 48kHz
44800100 ms
53840~79 ms
63200~66 ms
82400~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 same ns replaces the prior machine.
  • sources — 2 or more channel sources, each a label + track and either a url or a path.
  • 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-audio 404s. Do not npm install it 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.