--- status: draft date: 2026-05-20 definition: technical --- # Audio ABR Bandwidth-driven adaptive bitrate selection for audio. Wires `createTrackedFetch` into audio segment loading so audio fetches emit per-chunk bandwidth samples; adds a `switchAudioQuality` behavior parallel to `switchVideoQuality` that selects audio renditions based on bandwidth + constraints. The audio sibling of [video-abr](./video-abr.md) — same sampling-baked-into-loading pattern, same EWMA + safety-margin algorithm, same constraint-slot manual- override shape — applied within a single audio rendition group (typically a set of bitrate-varied AAC stereo renditions in one language). A **Media-src feature** in the framing from [clusters.md § Feature classification axes](./clusters.md#feature-classification-axes): audio renditions with multiple bitrates won't *adaptively* select without it (they'll play at the default-picked bitrate regardless of bandwidth conditions). ## Status - **Composition:** not implemented in `createSimpleHlsEngine`. Today [`audio-playback`](./audio-playback.md) is the single-rendition baseline; `setupAudioBufferActors` (`packages/spf/src/playback/behaviors/dom/setup-buffer-actors.ts`) uses plain `fetchStream` (no bandwidth sampling) and there is no audio-side equivalent of `switchVideoQuality`. The in-code JSDoc on `setupAudioBufferActors` (lines 249-255) explicitly documents the audio-ABR wiring change as "a localized change to this setup body (swap `fetchStream` for a `createTrackedFetch` call + declare `bandwidthState` writable here)." - **Definition depth:** technical — scope and constraints articulated against [video-abr](./video-abr.md)'s sketched precedent; algorithm reuses existing primitives (`createTrackedFetch`, `selectQuality`, `BandwidthState` EWMA accumulator); audio-specific design decisions open. Source material: [SPF Epics Working Doc — epic #8 Audio ABR / Multi-bitrate](https://www.notion.so/35f97a7f89d08123a13fecab1ca1cac4) (cluster C, Media-src, eng M-L, validation M; "Depends on #7" — the multi-language-audio rendition-group machinery). - **Prerequisite chain:** Notion #8 lists this as depending on #7 (multi-language-audio) for the rendition-group machinery. In practice, single-rendition audio-playback is the only hard prerequisite for the *mechanism*; multi-language-audio enables the *interesting case* (multiple audio renditions to switch between within a language). ## Phases of complexity Scope slices mirroring [video-abr.md](./video-abr.md)'s structure. Phases here are aspirational (technical-depth doc); they describe what would land, not what exists. | Phase | What | Notes | |---|---|---| | Bandwidth sampling on audio loading | Swap plain `fetchStream` for `createTrackedFetch` in `setupAudioBufferActors`. Audio segment fetches emit per-chunk bandwidth samples via EWMA accumulator, same as video does today | One-line swap per the in-code JSDoc instruction. Affects state-key writability for the setup behavior (declares `bandwidthState` writable). Reuses `createTrackedFetch` + `sampleBandwidth` primitives unchanged | | Bandwidth state — shared or separate | Decide whether audio samples feed the existing single `state.bandwidthState` slot (multi-writer, shared estimate) or a new `state.audioBandwidthState` slot (per-type independent estimate). Affects EWMA accumulator design and the slot-multi-writer characterization | Open question; the choice constrains the sampling-phase wiring above and the switching-phase reads below. See Open questions | | `switchAudioQuality` behavior | New behavior parallel to `switchVideoQuality` (`packages/spf/src/playback/behaviors/quality-switching.ts`). Reads `presentation`, `selectedAudioTrackId`, the relevant bandwidth slot; writes `selectedAudioTrackId`. Reuses the pure `selectQuality` algorithm from `media/abr/quality-selection.ts` unchanged | Same EWMA + safety-margin + upgrade-margin algorithm as video. Mirror of video-abr's "Dynamic bandwidth-driven adjustment" phase | | Manual override via `userAudioTrackSelection` | New constraint slot analog of `userVideoTrackSelection`. Consumer-driven narrowing of the audio candidate set; when only one survives, `switchAudioQuality` short-circuits — same pattern as video-abr's manual override | Open: whether this is a *separate* slot from multi-language-audio's Tier 2 programmatic write or *shares* it. video-abr uses a single slot (`userVideoTrackSelection`) for both pin-to-quality and manual-track-override; the analogous decision for audio is whether one slot handles both bitrate-pinning and language-pinning | | Multi-writer coordination on `selectedAudioTrackId` | Slot becomes triple-writer: `selectAudioTrack` default + `switchAudioQuality` ABR + (future) `multi-language-audio` Tier 2 programmatic. Same shape as video's `selectedVideoTrackId` triple-writer (default + ABR + external manual override) | Per the multi-writer characterization in [clusters.md § Multi-writer state slots](./clusters.md#multi-writer-state-slots): three writers from different decision domains (config vs derived vs intent) but all writing the same value type (track id). Coordination shape is well-precedented by video | | Cross-language / cross-channel boundary handling | Audio-ABR operates within a single rendition group (same language, same channel count, same codec). Cross-language switching consumes `multi-language-audio`'s primitive; cross-channel-count switching consumes `5.1-surround-selection`'s `changeType()` primitive | Defines the boundary of "within ABR scope" vs "consumes a sister feature's primitive." Bandwidth-driven switching across codec/channel/language boundaries is more aggressive and exits this feature's surface | ## What's in scope vs out of scope **In scope:** - All six phases above for HLS multi-bitrate audio rendition groups - Audio-side bandwidth sampling via `createTrackedFetch` - `switchAudioQuality` behavior + interaction with the (existing) `selectQuality` algorithm - `userAudioTrackSelection` constraint slot (or shared-slot alternative — see Open questions) - Same-codec, same-channel-count, same-language ABR switching **Out of scope (separate Media-src candidate features):** - **[multi-language-audio](./multi-language-audio.md)** *(coarse)* — cross-language switching is its concern. Audio-ABR consumes multi-language-audio's rendition-group surfacing (Tier 1) when multiple renditions exist within a language; ABR switches *within* a language, not across. - **[5.1-surround-selection](./5.1-surround-selection.md)** — cross- channel-count switching consumes its `changeType()` primitive (mid-stream codec change phase). Audio-ABR switches *within* a channel-count tier; if ABR wants to cross a channel-count boundary (e.g., downgrade from AC-3 5.1 to AAC stereo under bandwidth pressure), that's 5.1-surround-selection's `changeType()` phase. - **`[multi-signal-abr]`** *(candidate)* — non-bandwidth signals (CPU / thermal / battery) feeding ABR. Same shape as the candidate for video; would apply to audio too if it lands. - **Audio rendition caps** — max-bitrate, max-channels on the audio side. Open whether these fold into [rendition-selection-caps.md](./rendition-selection-caps.md) (which is currently video-only) or carry separately. Cross-ref to that doc's "Audio caps" open question. **Out of scope (different architectural layer):** - Adapter-level audio-quality picker UI / "audio quality menu" customer surfaces. - Above-engine "current audio bitrate" / "currently switching" surfaces. Consumers read `selectedAudioTrackId` + the resolved presentation's audio track bandwidth directly. ## Likely cross-cutting impact Things this feature probably forces decisions on, not just additions: - **`bandwidthState` multi-writer (if shared).** Today `setupVideoBufferActors` is sole writer per the JSDoc at `setup-buffer-actors.ts:210`. If audio shares the slot, `setupAudioBufferActors` becomes a second writer to the same slot — same decision domain (network bandwidth from per-chunk EWMA), same trigger (segment fetch sample), same cost (cheap write). The easiest multi-writer shape per [clusters.md § Multi-writer state slots](./clusters.md#multi-writer-state-slots); the only real design question is how the EWMA accumulator handles samples from two sources. Two sub-questions: (a) does one shared accumulator absorb both video and audio samples (mixes them at sample time), or (b) does the slot hold separate per-source accumulators that compose at read time? The current `BandwidthState` shape would need a small extension either way. - **`selectedAudioTrackId` writer characterization.** As of multi- language-audio Tier 2 landing, the slot stays **single-writer** — `switchAudioTrack` (in `playback/behaviors/dom/switch-audio-track.ts`) is the sole writer. Consumer intent flows through the `userAudioTrackSelection` constraint+filter slot. Mid-stream flush is dispatched from `switchAudioTrack` itself (slot owner = flush dispatcher — keeps slot writes and consequences co-located, sitting architecturally between `SegmentLoaderActor` and `SourceBufferActor`). When audio-abr ships, `switchAudioQuality` extends `switchAudioTrack` with bandwidth + ABR algorithm — either by rename + extension or as a sibling variant that supplants it in the engine composition. Either way, the slot stays single-writer and the filter shape carries over. - **Constraint+filter vs multi-writer for the manual-override slot.** video-abr's `userVideoTrackSelection` slot is a *constraint+filter* (the selection writer reads it as a filter; doesn't write the selection directly). Audio's manual-override slot could be the same shape (`userAudioTrackSelection` filters the candidate set for `switchAudioQuality`), OR multi-language-audio Tier 2 could introduce a *separate* programmatic-write path that directly writes `selectedAudioTrackId` for language switches. Whether bitrate-pinning and language-pinning share a slot or split affects the multi-writer count on `selectedAudioTrackId`. - **EWMA accumulator under sample-source mixing.** If video and audio samples both feed the same accumulator, the EWMA estimate is biased by the sample-size distribution: a small audio segment (~50KB) and a large video segment (~2MB) both produce one sample, but their bandwidth measurements may differ (audio fetches may not warm the network path as effectively). Two mitigation shapes: (a) weight samples by byte count at accumulator-update time, (b) keep per- source accumulators and read `min(video, audio)` similar to today's `min(fast, slow)` read pattern. The dual-EWMA pattern already precedents the latter approach. - **`selectQuality` algorithm reuse.** `selectQuality` in `media/abr/quality-selection.ts` is a pure function over `Track[]` + `BandwidthState` + config. It should reuse verbatim for audio; the algorithm doesn't depend on track type. However, the safety/ upgrade-margin defaults are tuned for video bitrate scales (Mbps); audio scales are ~10× smaller (kbps). Whether the same defaults work for audio or whether audio wants its own tuning is an open question. - **Engine composition order.** `switchVideoQuality` is composed after `setupSourceBuffers` + `trackCurrentTime` per [hls-engine.md](../../../../packages/spf/docs/hls-engine.md) (Stage 6). `switchAudioQuality` would compose at the same point or immediately after. Engine variants without audio-ABR continue to use `selectAudioTrack` (the single-pick behavior); engine variants with audio-ABR substitute `switchAudioQuality` — same mutually-exclusive pattern as video-abr's `switchVideoQuality`/`selectVideoTrack` alternatives. - **Audio caps integration with [rendition-selection-caps.md](./rendition-selection-caps.md).** That doc explicitly flags audio caps as an open question ("Audio renditions don't have height analogs but could have max-bitrate or max-channels caps. Extend this feature to cover audio or carry audio caps separately?"). When audio-ABR lands, audio caps become load-bearing; the open question needs resolution. ## Open questions - **Shared `bandwidthState` slot vs separate `audioBandwidthState`.** Per the cross-cutting note above. Trade-off: shared = one network estimate, samples from two sources biased toward one if sizes differ greatly; separate = independent per-type estimates, but redundant data structure with similar semantics. Decision likely depends on whether the EWMA pattern wants extension to handle mixed-source sampling, or whether two slots is genuinely cleaner. - **Manual-override slot sharing.** `userAudioTrackSelection` slot exists now (added by multi-language-audio Tier 2) and serves language-pinning. Open: does audio-abr reuse the same slot for bitrate-pinning too (one slot, dual role, mirrors video) — likely, since the partial-track shape supports both. Side-effect concerns (mid-stream flush) live in `switchAudioTrack`'s flush effect and key off `initTrackId` vs `selectedAudioTrackId` mismatch — they fire on language-switch and would naturally also fire on cross-codec bitrate-switch (whose handling routes to 5.1-surround-selection's `changeType()` path via a different `flushStrategy`). - **EWMA accumulator design under shared bandwidth state.** Single unified accumulator absorbing both video and audio samples, vs per-source accumulators read with a combining function (e.g., `min(videoEwma, audioEwma)` similar to today's `min(fast, slow)` pattern). The dual-accumulator pattern is precedented; the audio- inclusive variant is a small extension. - **Audio-side safety / upgrade margin tuning.** Video defaults (`safetyMargin: 0.85`, `upgradeMargin: 1.15`) are tuned for video scales. Audio bitrate ratios are different (a "next quality" audio rendition is often 2× the bitrate of the previous, vs video's more granular tiers); margin tuning may differ. Likely defer until empirical data. - **Cross-codec-tier ABR scope.** When AAC stereo bitrate variants coexist with AC-3 5.1 in the same audio rendition group (or selection set), can ABR cross the codec boundary, or stays within one codec? Closes back into 5.1-surround-selection's `changeType()` territory; this feature's scope is "within a codec" by default. - **Audio caps inclusion in rendition-selection-caps.md.** Per the cross-cutting note. Resolution likely punts to when audio-ABR + the first audio cap motivation both land. - **`picker` config for audio-ABR initial selection.** video-abr's config includes a `picker` for overriding initial selection. Whether audio-ABR exposes the same `picker` config (parallel `audio.picker`?) or relies on `audio-playback`'s existing `SelectAudioTrackConfig.picker` for initial pick before ABR re-evaluation kicks in. ## Related features - **[video-abr](./video-abr.md)** *(structural template)* — the direct parallel-sibling on the video axis. Same sampling-baked- into-loading pattern, same algorithm, same constraint-slot manual- override. The audio doc mirrors the video doc's shape with audio- specific design decisions (bandwidth-state sharing, multi-writer with multi-language-audio Tier 2) layered on top. - **[audio-playback](./audio-playback.md)** — the single-rendition baseline this feature extends. The "What's not implemented" Audio ABR bullet there is resolved by this doc. - **[multi-language-audio](./multi-language-audio.md)** *(partial, sketched)* — prerequisite per Notion #8 for the rendition-group machinery, now mostly implemented. Audio-ABR operates within a language; multi-language-audio handles across-language switching. Shared infrastructure landed: `userAudioTrackSelection` filter slot, `switchAudioTrack` behavior (slot owner; precursor to `switchAudioQuality`), and pluggable `flushStrategy` config with `nextSegmentBoundaryFlushStrategy` as the default. Audio-abr Phase 3's `switchAudioQuality` either renames `switchAudioTrack` + adds bandwidth logic, or coexists with it as a sibling variant in the engine composition. - **[5.1-surround-selection](./5.1-surround-selection.md)** — consumes its `changeType()` primitive for cross-channel-count switching when audio-ABR wants to cross those boundaries. - **[buffer-management](./buffer-management.md)** — sampling lands in this feature's audio fetch path (`setupAudioBufferActors`); same shape as video's `bandwidthState` sample-producer relationship to buffer-management. - **[mse-mms-pipeline](./mse-mms-pipeline.md)** — owns `setupAudioBufferActors` where the `createTrackedFetch` swap happens. Same-codec, same-channel-count switching doesn't touch the SourceBuffer lifecycle. - **[rendition-selection-caps](./rendition-selection-caps.md)** — the "Audio caps" open question becomes load-bearing when audio-ABR ships. Cross-link. - **[multi-signal-abr](./multi-signal-abr.md)** — non-bandwidth signals feeding ABR; extends both video and audio via shared signal- monitor infrastructure with per-type bias calculation. - **`[bandwidth-estimation]`** *(coarse, not yet documented, candidate)* — the dual-EWMA accumulator is a reusable primitive. Audio-ABR is the second consumer; promoting `BandwidthState` to its own feature doc may become worthwhile. ## Use cases that compose this feature - **[`audio-only-mode-override`](../use-cases/audio-only-mode-override.md)** *(coarse)* — Phase 2 constituent. When audio-abr is implemented, the audio-only delivery variant composes it for multi-bitrate audio support. Used as-is. ## See also - [video-abr.md](./video-abr.md) — structural template; the parallel-sibling doc this one mirrors on the audio axis - [clusters.md § Track & variant registry](./clusters.md#track--variant-registry) — cluster C description - [clusters.md § Sampling-baked-into-loading](./clusters.md#sampling-baked-into-loading) — cross-cluster pattern this feature instantiates; audio is the second consumer of the pattern - [clusters.md § Multi-writer state slots](./clusters.md#multi-writer-state-slots) — `bandwidthState` (if shared) and `selectedAudioTrackId` both become multi-writer; characterized in cross-cutting-impact bullets - [conventions/signals.md](../conventions/signals.md) — multi-writer slot conventions - [packages/spf/docs/hls-engine.md](../../../../packages/spf/docs/hls-engine.md) — Stage 6 documents the video-ABR composition point; audio-ABR composes at the same Stage 6 location - [SPF Epics Working Doc](https://www.notion.so/35f97a7f89d08123a13fecab1ca1cac4) — source material; epic #8 (Audio ABR / Multi-bitrate), depends on #7 (Multi-language Audio Tracks) - [Mux Video Permutations Matrix](https://www.notion.so/32c97a7f89d08191b84dd30f06685490) — Stream Type / Audio bitrate section