# Phase 18 — Opus Low-Data Streaming (dual-format lossless + Opus delivery) Product spec. Status: **design / framing — open questions RESOLVED (Daniel, 2026-06-23); implementation-ready.** Author: product-designer. Date: 2026-06-23. **No code has been written by this doc.** > **Resolution pass (Daniel, 2026-06-23).** OQ1–OQ7 are resolved (see §6 — each marked RESOLVED, kept > visible per file convention; OQ7 — seek-index granularity — set to **0.5 s buckets**). Two resolutions > reshaped the spec materially: (a) the listener quality > selection lives inside a **new public-site Settings menu surface** (not a bare app-bar control) — §4 + > §4a; and (b) Daniel rejected the "approximate page-interpolation" seek hand-wave outright — **VBR-safe > *accurate* seeking is now a first-class part of the architecture** (a precomputed seek-index artifact + > a separately-available setup header). §3.4 is rewritten and a dedicated seek-model section (§3.4a) > added. The Phase 21 cross-reference is updated to read "accurate index-based mapping," not > "approximate." This phase is the concrete realization of the long-deferred **"Non-WAV formats"** intent (`CONTEXT.md §5`, the "1.2" the streaming-feature items reference). It supersedes the abstract "a processor per format + a decoder strategy" framing with a specific, Daniel-directed product: **two delivery formats per track — the existing lossless WAV path and a new low-data Ogg Opus path — so the listener gets a choice, with Opus the bandwidth-friendly default-candidate.** Surfaces (named precisely): - **Ingest / preprocessing:** `DeepDrftContent` (`AudioProcessor` / `AudioProcessorRouter` / `TrackContentService` / `WaveformProfileService`) + `DeepDrftAPI` (upload/persist — `UnifiedTrackService.UploadAsync`, replace-audio) + `DeepDrftManager` (CMS upload form — the **Post-Processing phase** on the existing upload progress meter, §3.1a). - **Delivery / decode:** `DeepDrftAPI` (the track stream endpoint + `Range` handler + the new **seek-index** and **setup-header** sidecar endpoints, §3.4a) + `DeepDrftPublic` proxy (`TrackProxyController`) + `DeepDrftPublic.Client` player stack (`StreamingAudioPlayerService`, `TrackMediaClient`) + `DeepDrftPublic/Interop/audio` TS decoders (`AudioPlayer.createFormatDecoder` registry, a new `OpusFormatDecoder`). - **Listener settings (NEW surface):** `DeepDrftPublic.Client` — a public-site **Settings menu** (app-bar menu/popover) hosting the quality toggle as its first occupant, with a dark-mode-pattern persistence seam (cookie → settings object → `PersistentComponentState` → client cookie service). §4a. The prerender-cookie read lives in `DeepDrftPublic` (alongside `DarkModeService`). **Sequencing headline: Phase 18 comes BEFORE Phase 21 (Windowed Streaming Buffer).** Phase 21's windowing must work across both formats — its C5 invariant already anticipated this ("must not foreclose MP3/FLAC"); Opus is now the concrete VBR/containerized driver of that invariant. See §6 and the Phase 21 cross-reference. --- ## 0. State of the world (what already exists — verified 2026-06-23) This phase is **much further along than the "Non-WAV formats" backlog line implies**, on both sides. Two prior efforts already built most of the multi-format substrate; what is *missing* is specifically the **derived-Opus-artifact** idea, not generic format support. **Producer side is already multi-format (router landed):** - `AudioProcessorRouter.ProcessAudioFileAsync(filePath)` routes by extension — `.wav` → `AudioProcessor`, `.mp3` → `Mp3AudioProcessor`, `.flac` → `FlacAudioProcessor` (`DeepDrftContent/CLAUDE.md`). - `TrackContentService.AddTrackAsync(filePath, mimeType)` is **format-agnostic**: it selects the processor, generates an entry GUID, and **stores the original bytes** with correct extension/MIME in the `tracks` vault. - So today the system can *ingest and store* WAV/MP3/FLAC. It **does not transcode** — it keeps the original. There is no derived artifact and no second format per track. **Decoder side is a wired strategy registry (not "implemented-not-wired" anymore):** - `AudioPlayer.createFormatDecoder(contentType)` (`AudioPlayer.ts:117`) dispatches on `Content-Type`: `audio/mpeg|audio/mp3` → `Mp3FormatDecoder`, `audio/flac|audio/x-flac` → `FlacFormatDecoder`, default → `WavFormatDecoder`. All three decoders exist and implement `IFormatDecoder`. - `IFormatDecoder` (`IFormatDecoder.ts`) is a clean per-format strategy: `tryParseHeader`, `getAlignedSegmentSize`, `wrapSegment`, `calculateByteOffset`, plus a `FormatInfo` carrying `byteRate`, `blockAlign`, `audioDataOffset`, and a `seekData` accelerator slot (already polymorphic: `Mp3VbrSeekData | FlacSeekData`). **This is the seam an `OpusFormatDecoder` slots into.** - **Correction to the Phase 21 spec's §2 C3 note** ("MP3/FLAC implemented, not yet wired"): the registry *is* wired and dispatches on content-type today. Phase 21's invariant still holds; the parenthetical is stale and is corrected by this phase's reconciliation. **What this means for the gap.** Daniel's direction is **not** "add format support" — that substrate exists. It is "**derive a second, low-data artifact (Opus fullband 320) at ingest and let the listener pick which to stream.**" That is two genuinely new things: (1) a **transcode-at-ingest** step that produces a derived artifact per track (the router stores originals; nothing derives Opus), and (2) a **per-format delivery selection** so the same track can be served as either WAV or Opus on request. --- ## 1. Goal **Dual-format delivery.** Every track is streamable in two formats: - **Lossless** — the existing WAV path, unchanged. The archival / audiophile option. - **Low-data** — a derived **Ogg Opus, fullband, 320 kbps** artifact. The bandwidth-friendly default-candidate. The listener chooses; Opus is the recommended default. The bespoke Web Audio decode→schedule graph is **retained by deliberate choice** (Daniel) — Opus is fed through the same `IFormatDecoder` strategy seam, not through an HTML `` element or MSE. **Why Opus fullband 320.** Opus is the modern, royalty-free, best-in-class lossy codec; "fullband" (48 kHz, full 20 kHz audio bandwidth) at 320 kbps is transparent-to-most-listeners quality at roughly **1/4 to 1/5 the bytes of 16-bit/44.1 stereo WAV** (~1411 kbps). For a 1 GB DJ MIX (Phase 9 `Mix` medium), that is the difference between a ~1 GB transfer and a ~220 MB transfer — the headline low-data win, and directly relevant to the Phase 21 long-stream case. **Non-goals.** This phase does not retire WAV (it stays as the lossless option), does not change the bespoke graph for MSE (explicitly rejected — see §2 / Phase 21 OQ5), and does not add new transport mechanisms beyond the existing stream + `Range` primitive. --- ## 2. Constraints / invariants (the contract that must hold) - **C1 — Keep the bespoke Web Audio graph. MSE is rejected (Daniel, deliberate).** The custom decode→schedule graph is a long-term commitment, not a stopgap. Opus is fed through the existing `IFormatDecoder` → `StreamDecoder` → `PlaybackScheduler` pipeline. (This is the same decision recorded as **Phase 21 OQ5 = NO**; the two phases share it.) - **C2 — Preprocessing is additive; the WAV path is untouched.** The Opus artifact is a **second derived artifact per track**, not a replacement. The existing WAV in the `tracks` vault stays byte-for-byte as it is today; the lossless stream path is unchanged. A track with no Opus artifact (legacy rows, or a transcode that hasn't run yet) must still play losslessly — Opus is strictly additive. - **C3 — Reuse the landed `Range`/offset seek path; do not fork it.** Phase 4's `Range: bytes=X-` → `206` primitive (client `TrackMediaClient` → `DeepDrftPublic` proxy → `DeepDrftAPI`) is the substrate for Opus seek too. Opus seek math differs from WAV (VBR / container-paged, see §3.4) but it is expressed through the **same** `IFormatDecoder.calculateByteOffset` seam the MP3/FLAC decoders already use — no second seek mechanism. - **C4 — Opus slots the `IFormatDecoder` registry; no format branches leak elsewhere.** The new `OpusFormatDecoder` is selected by `AudioPlayer.createFormatDecoder` on `Content-Type: audio/ogg`/`audio/opus`. The rest of the player stack stays format-agnostic. No `if (opus)` outside the decoder and the one selection point. - **C5 — Format selection is a delivery-time decision, resolved server-side from a listener signal.** The same `TrackEntity` / `EntryKey` addresses both artifacts; the *format* is a parameter on the stream request (query param or `Accept` negotiation — see §3.3), not a different track id and not a different vault entry key. One track, two renderings (the standing "one source, multiple views" preference applied to delivery). - **C6 — Transcode failure must not block ingest.** If the Opus transcode fails or is slow, the track still persists with its lossless artifact and is playable. Opus is generated best-effort and can be (re)generated later — mirror the **waveform-datum** model (`WaveformProfileService`: compute on upload, regenerate on demand via a CMS action), which is exactly the "derived artifact, generated at ingest, regenerable" pattern this needs. - **C7 — The vault model holds: derived artifact is a new entry, not a mutation.** The Opus bytes live in the FileDatabase under the track's `EntryKey` — either in the existing `tracks` vault under a derived key, or in a new sibling vault (see §3.2 options). Either way it is `AudioBinary` with the `.opus`/`.ogg` extension and correct MIME, registered like any other vault resource. --- ## 3. Architectural shape ### 3.0 The mental model A track has one **source artifact** (the uploaded WAV/MP3/FLAC, stored as-is today) and gains one **derived low-data artifact** (Ogg Opus fullband 320, produced at ingest). The stream endpoint serves *either*, selected per request. The player picks a decoder by the response `Content-Type` exactly as it does today. Seeking uses the same `Range` primitive; the byte↔time math is the decoder's job. ``` INGEST (DeepDrftContent + DeepDrftAPI) upload → AudioProcessorRouter (existing) → store SOURCE artifact in vault [unchanged] → TRANSCODE to Opus 320 → store DERIVED artifact [NEW] → WaveformProfileService (existing, unchanged) DELIVERY (DeepDrftAPI → DeepDrftPublic proxy → DeepDrftPublic.Client → Interop/audio) GET api/track/{id}?format=opus|lossless → serve the chosen artifact's bytes (+ Range) [NEW param] player: createFormatDecoder(Content-Type) → OpusFormatDecoder | Wav | Mp3 | Flac [+1 decoder] ``` ### 3.1 Where the transcode lives (relative to existing processing) The transcode is a **new processor sibling** to the existing format processors, invoked **after** the source is stored, in the same orchestration that already calls `WaveformProfileService`: - It belongs in `DeepDrftContent` (the binary-content domain library) as e.g. an `OpusTranscodeService` / `OpusProcessor`, **not** in a host and **not** in a controller (per the `*.Services`-owns-domain-logic convention). - It is invoked from `UnifiedTrackService.UploadAsync` (the same place `WaveformProfileService` computes the high-res datum on every new track) and from the **replace-audio** path (which already regenerates both waveform datums — Opus is the third derived thing to regenerate there). - Like the waveform datum, it gets a **regenerate trigger**: a CMS per-track / bulk action and an ApiKey-gated endpoint, so existing tracks can be backfilled. This mirrors the landed "Generate All Profiles / Backfill High-res" bulk actions on `Releases.razor` — **Backfill Opus** is the natural third bulk action. **The transcode engine itself is staff-engineer's call** (FFmpeg/libopus via a process invocation, a managed binding, or a libopus P/Invoke). The spec fixes the *artifact* (Ogg Opus, fullband, 320 kbps) and the *seam* (a derived artifact produced post-store, regenerable, failure-tolerant), not the tool. Note a real operational constraint to flag for implementation: transcoding a 1 GB WAV is **CPU- and time-expensive** and must not block the upload response — it wants the same off-the-hot-path treatment the upload body staging already gets (`Upload:StagingPath`). This is the single biggest implementation risk and is called out as such. The execution model is now **decided** (OQ6): **the source is stored and the track is playable (lossless) first, then the Opus transcode runs as a background job** — see §3.1a for the user-visible consequence on the upload UI. ### 3.1a Transcode execution model + the Post-Processing upload phase (RESOLVED — OQ6) **Execution model (Daniel, 2026-06-23): background process *after* the file is available.** The upload flow is now two distinct server-side stages with a hard ordering: 1. **Transfer + store + persist (existing, synchronous).** The WAV body streams in (the landed `ProgressStreamContent` two-phase cancellation), the source is stored in the vault, the `TrackEntity` is persisted, the waveform datums are computed. At the end of this stage **the track is fully playable losslessly** — nothing about Opus gates a successful upload. 2. **Opus transcode (NEW, background, after stage 1 completes).** A queued/background job reads the stored source, transcodes to Ogg Opus 320, builds the **seek index** and extracts the **setup header** (§3.4a), and stores all three derived artifacts. Until it finishes, `?format=opus` for that track falls back to lossless (C2). On failure the track stays lossless-only and is eligible for Backfill-Opus (C6). **The upload progress meter gains a visible Post-Processing phase.** The CMS upload forms (`BatchUpload.razor` / `BatchEdit.razor`) already render a progress meter driven by `ProgressStreamContent` (byte-transfer progress) and the two-phase cancellation (idle window during transfer, response-wait budget after the body completes). The transcode is a **third visible phase** appended to that meter — after the existing "uploading bytes" and "server is persisting" phases, a **Post-Processing** phase reflects the background transcode's status (queued → transcoding → done / failed). This is an *addition* to the existing meter, not a new UI. - The admin sees: bytes transfer → server persists (track now exists + plays lossless) → **Post-Processing** (Opus being derived). The form may complete/return the admin to the catalogue after stage 1 (the track is live); the Post-Processing phase can continue to report against that track in the browse/release view (the Opus waveform/profile columns on `Releases.razor` already poll-and-show per-track derived-artifact status — Post-Processing status fits the same affordance family). - **How status reaches the UI is staff-engineer's call** (poll the track's Opus-artifact presence, an SSE/ long-poll job channel, or a status field on the track read). The spec fixes that the phase is *visible* and *non-blocking* — the admin is never made to wait on the transcode to consider the upload done. - This composes with the **always-on** decision (OQ4): every upload triggers the background transcode; there is no per-upload opt-out, so the Post-Processing phase always appears. ### 3.2 Where the Opus artifact is stored (two options) **Option S1 — derived key in the existing `tracks` vault (recommended).** Store the Opus bytes under a derived entry key alongside the source, e.g. `{entryKey}` for source and `{entryKey}.opus` (or a parallel key convention) in the same `tracks` vault. *Pro:* no new vault type, co-located with the source, simplest lookup. *Con:* mixes two artifacts per logical track in one vault's index. **Option S2 — a new sibling vault (e.g. `track-opus`).** Mirror the `track-waveforms` precedent (Phase 12 added a dedicated vault for the derived high-res datum). Opus bytes keyed by the same `EntryKey` in a `track-opus` vault. *Pro:* clean separation of source vs. derived, matches the established "derived artifacts get their own vault" pattern (`track-waveforms`), easy to enumerate / backfill / purge independently. *Con:* one more vault to register. **Recommendation: S2** — it is the pattern the codebase already chose for the *other* derived per-track artifact (the high-res waveform datum), so it is the least surprising and keeps the source `tracks` vault meaning exactly one thing. **Final call is staff-engineer's**; both are viable. ### 3.3 How a listener's format choice reaches the bytes The stream endpoint gains a **format selector**. Two candidate mechanisms: - **D-a — explicit query param** `GET api/track/{id}?format=opus|lossless` (recommended). Mirrors the existing `offset` query param the proxy already forwards (`TrackProxyController`). Explicit, cache-friendly (distinct URLs), trivial to thread through the proxy, and the player already knows which it asked for. Server resolves the param → the right artifact → sets the right `Content-Type`, which the player's existing `createFormatDecoder` then dispatches on. **No new decoder-selection mechanism** — the response content-type does the work it already does. - **D-b — HTTP content negotiation** (`Accept: audio/ogg` vs `audio/wav`). More "correct" REST, but the proxy + WASM client wiring is fussier and caches are content-type-varied. Not worth it here. **Recommended: D-a.** The selection *policy* (which format a given listener gets by default, and how they switch) is a genuine **product call — see OQ1/OQ2**, deliberately not decided here. The *mechanism* (a query param resolved server-side to an artifact + content-type) is settled. Server-side fallback rule (C2): if `format=opus` is requested but no Opus artifact exists for that track (not yet transcoded / backfilled), the endpoint **falls back to lossless** rather than 404ing — Opus is additive, so its absence degrades to "you get the lossless one," never to "no audio." ### 3.4 The Opus decoder (the genuinely new decode work) `OpusFormatDecoder implements IFormatDecoder` is the new code on the delivery side. **Ogg Opus is a containerized, paged format — not raw-frame-sliceable** the way WAV PCM is. WAV's `wrapSegment` prepends a 44-byte PCM header to any PCM-aligned byte run; the current model assumes you can wrap an arbitrary aligned raw-audio slice and hand it to `decodeAudioData`. Ogg Opus is page-structured (Ogg pages carrying Opus packets, plus mandatory `OpusHead`/`OpusTags` **setup pages** at the very start). A mid-stream byte slice is **not** independently decodable: it needs (1) the setup header prepended, and (2) to begin on an Ogg **page boundary**. So: - `OpusFormatDecoder.getAlignedSegmentSize` aligns to **Ogg page boundaries** — scan for the `OggS` capture pattern (analogous to FLAC's frame-sync scan; the `IFormatDecoder` interface already passes `rawData` to `getAlignedSegmentSize` for exactly this reason). - `wrapSegment` / the continuation path **prepends the `OpusHead`/`OpusTags` setup bytes** to a mid-stream page run before handing it to `decodeAudioData` (analogous to FLAC's `streamInfoBytes` carry in `FlacSeekData`). The setup bytes come from the **setup-header mechanism** (§3.4a), not from re-reading the stream start. - A new `OpusSeekData` variant joins `Mp3VbrSeekData | FlacSeekData` in the `seekData` accelerator slot — but for Opus it carries the **accurate seek index** (§3.4a), not a heuristic TOC. **The `IFormatDecoder` abstraction already has the shape for both needs** — a format-specific `seekData` accelerator and a setup-bytes carry — because FLAC needed the same kind of thing. The genuinely new part is **where the seek index and setup header come from**, which §3.4a designs. > **Seek is NOT approximate for Opus (Daniel, 2026-06-23 — supersedes the earlier hand-wave).** An earlier > draft of this section proposed "granule-position/Ogg-page interpolation" — a best-effort approximate > offset, the Opus analogue of MP3's Xing TOC. **That is rejected.** Daniel: *"Killing seeking for > decoding is unacceptable… Raw bytes offset for seeking is no longer adequate due to VBR. We need an > accurate transfer function for seek time → true file byte offset."* Opus seeking is **accurate**, backed > by a precomputed index built at transcode time. See §3.4a. **Browser decode-support constraint (real, must be designed around).** The bespoke graph decodes segments via `AudioContext.decodeAudioData`. Ogg-Opus support in `decodeAudioData` is long-standing in Chrome and Firefox but arrived in **Safari only at 18.4 (macOS 15.4 / iOS 18.4, March 2025)**; older Safari decodes Opus only in a CAF container, not Ogg. iOS Safari is a primary music-listening surface, so this is not a corner case. Implications: (1) the **lossless WAV path is the universal fallback** for listeners whose browser can't decode Ogg Opus — which C2's additive design already provides for free; (2) the format default is **capability-gated** (OQ2, RESOLVED) — don't hand Ogg Opus to a Safari that can't decode it; detect support (a probe `decodeAudioData` on a tiny Opus blob, or a UA/version gate) and fall back to lossless. This intersects Phase 1.7 (Safari compatibility) and is flagged there too. ([Browser support: caniuse / WebKit 18.4 release notes — see Sources.]) ### 3.4a VBR-safe accurate seeking (the seek-index artifact + the setup-header mechanism) This is the architectural core of the Opus delivery path, and it must compose with **Phase 21 windowed refill** (where most of the stream is *not* in memory). The requirement, decomposed from Daniel's direction: 1. Seeking must be preserved for Opus **without** having the full PCM decoded in memory. 2. Raw byte-offset seek is inadequate — a VBR Opus stream has **no linear time↔byte relationship**, so `byteRate` math and even rough page interpolation are not accurate enough. 3. We need an **accurate transfer function: seek-time → true file byte offset.** 4. The decode setup header must be **available separately** (or cached before seeking past it), because a mid-stream slice is undecodable without `OpusHead`/`OpusTags`. **The key insight: the one moment we already walk the entire encoded stream is the transcode.** That is precisely when an accurate index can be built for free. We never have to guess at delivery time — we read the answer out of a precomputed artifact. #### A. The seek-index artifact (the accurate transfer function) At transcode time, after the Opus bytes are produced, **walk the encoded Ogg stream once and record, for each Ogg page (or coarser bucket), the page's `granulepos` (a 48 kHz sample count → time) paired with its **byte offset** in the file.** That granule→byte table *is* the exact transfer function. This is the Opus analogue of FLAC's `SEEKTABLE` / MP3's Xing TOC — but **precomputed and exact**, not derived by interpolation guessing. Ogg granule positions are authoritative sample counts, so the mapping is true, not estimated. - **What it contains.** An ordered list of `(timeSeconds | granulepos, byteOffset)` entries, plus the total duration and total byte length (for clamping a seek to range). A binary little-endian array of fixed-width records is the natural shape (e.g. a `uint64 granulepos` + `uint64 byteOffset` per entry); the exact encoding is staff-engineer's, but it should be a **compact binary blob**, fetched once and parsed into a typed array client-side. - **Granularity vs. size — RESOLVED: 0.5 s (half-second) buckets (Daniel, 2026-06-23).** One entry per Ogg page is the most precise but largest; an Ogg page is typically a few KB of audio (~tens of ms to a few hundred ms), so a 1-hour mix could be tens of thousands of pages. The chosen bucket is **one index entry per 0.5 seconds of audio** (snap each bucket boundary to the *nearest enclosing page start*, so every indexed offset is still an exact page boundary). At 0.5 s granularity a 1-hour mix is ~7,200 entries × 16 bytes ≈ **~115 KB** — still a trivial one-time fetch, and 0.5 s seek resolution is finer than required (the decoder re-syncs to the exact page within the bucket anyway — see the client flow — so the in-bucket trim is *sub-half-second*, tighter than the earlier ~1–2 s recommendation). **Per-page precision remains the fallback if 0.5 s buckets ever prove too coarse**, at a larger index. The bucket size is now fixed; the *shape* (precomputed exact granule→byte, bucketed, snapped to page starts) is unchanged. - **Sidecar, not embedded (recommended).** Store the index as a **third derived artifact** alongside the Opus bytes and the waveform datum — the same "derived artifacts get their own vault" pattern this phase already uses (S2 / `track-opus`; the `track-waveforms` precedent). Keep it a separate vault resource (e.g. `{entryKey}.seekidx` in a `track-opus` vault, or its own `track-opus-index` vault) rather than embedding it in the Ogg stream. *Why sidecar:* it is fetched **once, up front** (small, cacheable), independent of the audio byte stream; embedding it in the Ogg would force the client to read into the stream to find it, defeating the "resolve the offset *before* the Range fetch" flow. *Road not taken — derive the index lazily on first seek by scanning server-side:* rejected, because it re-walks the stream at request time (the cost we avoid by computing at transcode) and gives nothing the precomputed sidecar doesn't. #### B. The setup-header mechanism (decodability of any mid-stream slice) Any post-seek slice needs `OpusHead` + `OpusTags` prepended to decode. Two ways to make those bytes available to the client: - **B-a — Client-side caching of the leading setup pages on first read (recommended).** On first play, the stream already begins at byte 0, so the client *already receives* the `OpusHead`/`OpusTags` pages as the opening bytes. `OpusFormatDecoder.tryParseHeader` captures and **retains** those setup bytes (exactly as `WavFormatDecoder` retains the parsed WAV header for `reinitializeForRangeContinuation` today, and FLAC retains `streamInfoBytes`). Every subsequent post-seek continuation prepends the cached setup bytes. *No new endpoint;* it reuses the header-retention discipline already in the codebase. - **B-b — A dedicated setup-header sidecar endpoint** (`GET api/track/{id}/opus/header` → just the `OpusHead`/`OpusTags` bytes, also derivable at transcode time and stored as a tiny artifact). *Pro:* a seek can be served even if the listener seeks **before** the stream start has been read (e.g. a deep-link that begins mid-track, or a Phase 21 window that opens away from byte 0). *Con:* one more endpoint + artifact. **Recommendation: B-a as the primary, B-b as a cheap insurance artifact.** B-a covers the overwhelming common case (play-then-seek) with **zero new surface** — it is the WAV-header-retention pattern applied to Opus. But Phase 21 windowing and deep-links can legitimately open a window that never read byte 0, so the setup header should **also** be derivable on demand. Cheapest reconciliation: **extract the setup bytes at transcode time and store them as a tiny sidecar artifact** (they are a few hundred bytes), and expose them **either** as a small endpoint **or** simply prepend them to the seek-index sidecar's header region so the single up-front index fetch *also* delivers the setup bytes. The latter folds B-b into the B-a fetch: **the client's one up-front sidecar fetch returns both the seek index and the setup header**, so it always has both before it ever issues a seek — and never needs byte 0 to have been read. **Recommended concrete design: one sidecar per track = `[setup-header bytes][seek-index table]`, fetched once on track load, parsed into `OpusSeekData`.** This is the cleanest: one new artifact, one new fetch, both needs met. #### C. The client-side seek flow, end to end With the sidecar (`OpusSeekData` = setup header + granule→byte index) fetched and parsed at track load: 1. **Resolve time → byte offset (accurate).** Listener seeks to `t` seconds. `OpusFormatDecoder.calculateByteOffset(t)` does a binary search in the index for the largest entry with `time ≤ t`, returns its exact (page-start) `byteOffset`. **No interpolation, no `byteRate` math.** (For WAV this method stays the exact CBR calculation it is today — the seam is identical; only the Opus implementation reads an index.) 2. **Range fetch from the offset.** Issue `GET api/track/{id}?format=opus` with `Range: bytes={byteOffset}-` — the **landed Phase 4 Range primitive, unchanged**. Server streams raw Opus bytes from that exact page boundary (`206 Partial Content`). 3. **Prepend the cached setup header + decode.** The continuation path (the Opus analogue of `StreamDecoder.reinitializeForRangeContinuation`) prepends the retained/sidecar `OpusHead`/`OpusTags` bytes to the incoming page run, then feeds it to `decodeAudioData`. Because the index offset is an exact page start, the stream is immediately Ogg-sync-aligned. 4. **Fine re-sync within the bucket.** The granule of the first decoded page tells the decoder the *exact* time it landed at (≤ the bucket granularity ahead of `t`); the scheduler trims/positions to land playback at `t` precisely. With 0.5 s buckets the trim is sub-half-second; with per-page granularity it is near-zero. **Either way the listener lands at the correct time, not approximately** (AC9). #### D. Composition with Phase 21 windowed refill Phase 21's windowed refill controller resolves "I need bytes for playback position `P`" → a byte offset → a Range fetch. **It calls the *same* `OpusFormatDecoder.calculateByteOffset` (the index-based resolver) for Opus** that an explicit seek does — windowed refill is just a seek the listener didn't initiate. So the seek index serves both: explicit seeks and the window's low-water refills both resolve through the index, and both prepend the cached setup header. This is why §3.4a is in **Phase 18** (where the transcode that builds the index lives), and Phase 21 *consumes* it. The Phase 21 spec's "approximate mapping" language for Opus is now wrong and is corrected to **"accurate index-based mapping."** #### E. Reuse vs. extend (the seam discipline) - **Reused verbatim:** the Phase 4 `Range: bytes=X-` → 206 primitive (client → proxy → API); the `IFormatDecoder.calculateByteOffset` seam; the header-retention/continuation discipline (`reinitializeForRangeContinuation`'s Opus analogue); the derived-artifact-in-its-own-vault pattern (`track-waveforms` → `track-opus`); the derive-at-transcode-regenerate-on-backfill lifecycle. - **Extended (new):** the seek-index + setup-header **sidecar artifact** (built at transcode, stored beside the Opus bytes); the one-time **sidecar fetch** on track load (parsed into `OpusSeekData`); the index **binary-search resolver** inside `OpusFormatDecoder`. Three additions, all leaf-level — no change to the Range mechanism, the proxy, or the format-agnostic player. ### 3.5 The three candidate directions (shape-level) Per file convention the alternatives are recorded; the recommendation follows. **Direction A — Derived Opus artifact at ingest + format param on delivery (recommended).** What §3.1 –3.4a describe: transcode to Opus 320 post-store as a **background job** (OQ6), store as derived artifacts (S2 vault) — the Opus bytes **plus the seek-index/setup-header sidecar** (§3.4a) — serve via a `?format=` param resolved server-side to bytes + content-type, decode via a new `OpusFormatDecoder` in the existing registry, **seek accurately via the precomputed index**. *Why recommended:* additive (C2), reuses every existing seam (the processor orchestration, the waveform-datum derived-artifact pattern, the `Range` path, the decoder registry, the header-retention discipline), and the only genuinely new code is one transcode step (+ index build) + one decoder (+ index resolver). **Three** derived artifacts per track (Opus bytes, seek sidecar, and the existing waveform datum), all regenerable. **Direction B — On-the-fly transcode at delivery (no stored Opus artifact).** Transcode WAV→Opus per request in the stream endpoint, streaming the Opus out as it encodes. *Why not (default):* moves expensive CPU onto the **hot request path** (a 1 GB mix transcoded per play is untenable), breaks `Range`/seek (you can't byte-offset into a stream you're encoding live), and defeats caching. It *is* storage-cheaper (no second artifact on disk), so it is the fallback only if disk cost ever dominates — but for a music site where the same tracks are played repeatedly, precompute-once wins decisively. Rejected as the primary. **Direction C — Replace WAV ingest with Opus-only (transcode and discard the lossless source).** Make Opus *the* stored format; drop WAV. *Why not:* violates Daniel's explicit "lossless streaming *optional* — two delivery formats, listener gets a choice." Lossless is a kept option, not a thing to transcode away. Also irreversibly lossy at ingest (you can never recover the WAV). Rejected outright; recorded only because "just store Opus" is the tempting simplification and the spec should say why not. ### 3.6 SOLID / road-not-taken rationale - **OCP, via the existing seams.** The transcode is a new processor sibling (the router pattern is already open for extension); the decoder is a new `IFormatDecoder` (the registry is already open for extension); the artifact is a new derived vault resource (the `track-waveforms` precedent is exactly this). Phase 18 adds **three new leaf implementations** and **zero changes to existing format code** — the strongest possible OCP signal that the seams were designed right. - **SRP, preserved.** Transcoding **and the seek-index build** are content-domain processor concerns (`DeepDrftContent`); delivery selection is a thin endpoint concern (`DeepDrftAPI` resolves a param to an artifact, and serves the sidecar); decode is the `OpusFormatDecoder`'s concern; byte↔time math stays inside that decoder via `calculateByteOffset` (now reading the index, not interpolating). No responsibility crosses a boundary it doesn't already own. The seek index is built **once, where the stream is already walked** (transcode) — the natural home for an exact transfer function, never recomputed at request time. - **DIP / "one source, multiple views."** One `TrackEntity`/`EntryKey` is the single source; "lossless WAV" and "low-data Opus" are two *views* (renderings) of it, diverging only at the delivery/decode layer — the same discipline the dark-mode and track-browse surfaces follow. - **Road not taken — a separate `TrackEntity` row (or a new track id) per format.** Tempting (one row = one streamable file) but it fractures the track identity: shares, queues, play-counts (Phase 16), release membership, and waveform data all key on one track, and doubling rows to carry a format would force every one of those surfaces to dedupe. Format is a *delivery attribute of one track*, not a *second track*. Rejected — keep one identity, two artifacts. --- ## 4. Format selection — the product surface (RESOLVED — global, via a Settings menu) **Resolved (Daniel, 2026-06-23):** the listener's quality choice is **global** (one session/visitor-level "streaming quality" preference, not per-track), Opus is the **default** (capability-gated), and the choice is **remembered** following the dark-mode persistence pattern. Crucially: *"Global is perfect, but we need a menu system for settings, don't just slap the quality control directly in the app bar."* So the toggle does **not** sit bare in the app bar — it lives inside a proper **public-site Settings menu** (§4a), of which it is the **first occupant**. - **What the listener sees.** A Settings affordance in the public app bar opens a Settings menu; inside it, a "Streaming quality" control with two options — **Low-data (Opus)** / **Lossless (WAV)** — defaulting to Low-data. Picking lossless flips the global preference; the player sends the matching `?format=` on subsequent stream requests (§3.3). On a browser that can't decode Ogg Opus, the control is shown but the effective stream is lossless (capability gate, §3.4 / OQ2) — surface this honestly rather than letting the listener pick a format that silently can't play. - **Default before any choice:** Opus, capability-gated (OQ2 RESOLVED). A first-time visitor on a capable browser streams Opus; on an incapable browser, lossless. - **Persistence:** mirror the dark-mode seam exactly (OQ3 RESOLVED) — see §4a. ### 4a. The Settings menu surface (NEW — scoping + the dark-mode persistence pattern) Daniel asked for a **menu system for settings**, not a control bolted onto the app bar, and noted the existing **dark-mode toggle** is a natural future tenant of the same menu (design for adaptability — build the menu so dark mode *could* move into it later, but **do not force that migration now**). **Scoping recommendation: a small sub-track *within* Phase 18 (wave 18.6), not its own phase.** Reasoning: - The menu's only **required** occupant right now is the quality toggle, which Phase 18 owns end to end — splitting the shell into a separate phase would create a phase whose sole deliverable is an empty menu waiting for Phase 18's toggle. That is ceremony, not separation of concerns. - The menu is **small** — an app-bar trigger + a MudBlazor menu/popover + the persistence seam (which the quality toggle needs *anyway*). It is not a platform; it is a container with one tenant. - It carries a real **design-for-adaptability** obligation (it must be able to host dark mode and future settings later), but that is a *shape* requirement on a small surface, not a phase's worth of work. So: **build the Settings-menu shell as part of Phase 18 (wave 18.6), with the quality toggle as its first occupant, designed so dark mode and future preferences can plug in without restructuring.** Flag for Daniel: *if he wants the menu shell proven/landed independently before the quality toggle plugs in*, 18.6 can be split into "menu shell" then "quality toggle plugs in" — but they are small enough to land together. This is **not** recommended as its own top-level phase. (If Daniel disagrees and wants a dedicated "Public Settings Menu" phase that Phase 18's toggle then targets, that is a clean alternative — it just front-loads a surface with no second tenant yet. Recommendation stands: sub-track.) **The menu shell — design-for-adaptability requirements (so it survives new tenants):** - A **settings-item abstraction**, not a hard-coded list. The menu renders a small set of settings entries; adding dark mode later is adding an entry, not rewiring the menu. Each entry is a label + a control bound to a persisted preference. - A **single public-site settings object** carrying all listener preferences (today: streaming quality; tomorrow: dark mode, and whatever follows). This is the `DarkModeSettings` analogue, generalized — call it e.g. `PublicSiteSettings` / `ListenerSettings`. Dark mode's existing `DarkModeSettings` can fold into it *later* without disturbing the menu. **Persistence — mirror the dark-mode seam exactly (OQ3 RESOLVED).** The quality preference follows the *identical* path dark mode already uses (root `CLAUDE.md` "Theming and dark mode"): 1. **Cookie** — a `streamQuality` cookie (365-day, like `darkMode`), the durable truth. 2. **Server prerender read** — a service in `DeepDrftPublic` (sibling to `DarkModeService`) reads the cookie during prerender and seeds the settings object, avoiding a wrong-default flash on first paint (the streaming-quality analogue of the "wrong theme flash" fix). 3. **`PersistentComponentState` bridge** — the seeded preference carries from server prerender into the WASM render (the same bridge `DarkModeSettings` and `NowPlayingStats`/`StatsClient` already use), so the client boots already knowing the quality without a re-read flash or a re-fetch. 4. **Client cookie service** — a runtime client-side service (JS-interop cookie write, like the dark-mode toggle) persists the choice when the listener changes it in the menu. **Why mirror rather than invent:** the dark-mode seam is the codebase's established, working pattern for "a listener preference seeded at prerender, carried to WASM, persisted in a cookie." Reusing its shape means the quality preference inherits the no-flash guarantee for free, and the eventual dark-mode-into-the-menu migration is a *consolidation of two identical seams*, not a reconciliation of two different ones. (This is the "one source, multiple views" / design-for-adaptability discipline applied to listener settings.) --- ## 5. Use cases - **UC1 — Listener streams the low-data Opus of a long mix (the headline win).** A ~1 GB lossless mix transfers as ~220 MB of Opus; playback through the bespoke graph is identical in feel, far cheaper on bandwidth. (Compounds with Phase 21 windowing for the memory side.) - **UC2 — Listener prefers lossless and switches to it.** The same track served as WAV via `?format=lossless`; the bespoke graph decodes it exactly as today. - **UC3 — Legacy / not-yet-transcoded track.** `?format=opus` requested, no Opus artifact yet → server falls back to lossless (C2); the listener still hears the track. A later Backfill-Opus pass produces the artifact. - **UC4 — Admin backfills Opus for the existing catalogue.** A bulk "Backfill Opus" CMS action (the third sibling to the existing Generate-Profiles / Backfill-High-res actions) transcodes every track lacking an Opus artifact. - **UC5 — Replace-audio regenerates Opus.** The existing replace-audio path (which already regenerates both waveform datums and re-derives duration) also regenerates the Opus artifact from the new source. - **UC6 — Seek within an Opus stream (accurately).** Backward/forward seek resolves via the existing `Range` path; the offset comes from the `OpusFormatDecoder`'s **precomputed seek index** (§3.4a) — an exact granule→byte lookup, then fine re-sync to the requested time within the bucket. The listener lands at the **correct** time, not approximately, and without the full PCM decoded in memory. - **UC7 — Safari that can't decode Ogg Opus.** Capability-gated to the lossless path (§3.4), so the listener still plays audio. (Ties to OQ2 + Phase 1.7.) - **UC8 — Listener switches streaming quality in the Settings menu.** The listener opens the public Settings menu, flips "Streaming quality" from Low-data to Lossless (or back); the choice persists (cookie, dark-mode pattern) and applies to subsequent stream requests via `?format=`. On next visit the preference is seeded at prerender (no flash, no re-pick). (§4 / §4a.) - **UC9 — Deep-link / windowed start away from byte 0.** A listener opens a stream at a mid-track position (deep link, or a Phase 21 window that opens past byte 0) without ever reading the stream start. The decoder still has the `OpusHead`/`OpusTags` setup bytes because they arrived with the up-front sidecar fetch (§3.4a B), so the mid-stream slice is decodable immediately. (Composition case for Phase 21.) --- ## 6. Open questions — RESOLVED (Daniel, 2026-06-23) All seven open questions are resolved. Kept visible per file convention, each with the decision and the section that now carries it. OQ7 (raised by the seek-model design) is a narrow tuning call, now set to 0.5 s buckets. - **OQ1 — Selection UX — RESOLVED: global, via a Settings *menu* (not a bare app-bar control).** Daniel: *"Global is perfect, but we need a menu system for settings, don't just slap the quality control directly in the app bar."* So: one global quality preference, surfaced inside a new **public-site Settings menu** (§4 / §4a), of which the quality toggle is the first occupant. The menu is scoped as a **Phase 18 sub-track (wave 18.6)**, designed so dark mode (its natural future tenant) can plug in later. `[RESOLVED — §4 / §4a]` - **OQ2 — Default policy — RESOLVED: Opus by default, capability-gated.** Opus is the default; on a browser that cannot decode Ogg Opus (Safari < 18.4, §3.4), fall back to lossless rather than serving an undecodable stream. Network-awareness (Opus on cellular / lossless on wifi) remains **deferred** as gold-plating. `[RESOLVED — §3.4, §4]` - **OQ3 — Remembered choice — RESOLVED: persisted, following the dark-mode pattern.** A `streamQuality` cookie seeded at server prerender → settings object → `PersistentComponentState` bridge into WASM → client cookie service for runtime writes. The full dark-mode seam mirrored (§4a). `[RESOLVED — §4a]` - **OQ4 — Per-upload Opus control — RESOLVED: always-on + backfill.** Opus is generated for **every** track, always (no per-upload opt-out). **Plus** a bulk **Backfill-Opus** CMS action processes the existing catalogue. (The listener's lossless choice already covers "I want lossless," so a per-track opt-out earns nothing.) `[RESOLVED — §3.1, UC4, wave 18.5]` - **OQ5 — Container — RESOLVED: Ogg Opus.** `.opus` / `audio/ogg` (broadest `decodeAudioData` support). No CAF/WebM fallback — the lossless path already covers browsers that can't decode Ogg Opus (§3.4). `[RESOLVED — §3.4]` - **OQ6 — Transcode execution model — RESOLVED: background job after the file is available; uploader shows a Post-Processing phase.** The source is stored and the track is playable losslessly **first**; the Opus transcode (+ seek-index build) runs as a **background job** afterward; the CMS upload progress meter gains a visible **Post-Processing** phase reflecting the transcode status (§3.1a). A freshly uploaded track is lossless-only until its Opus finishes — accepted, and now made visible rather than implicit. `[RESOLVED — §3.1a]` **New open question raised by the seek-model design (§3.4a) — RESOLVED:** - **OQ7 — Seek-index granularity — RESOLVED: 0.5 s (half-second) buckets (Daniel, 2026-06-23).** The seek index trades precision against size: per-Ogg-page (most precise, largest) vs. coarser time buckets snapped to page starts. Daniel set the bucket at **0.5 s** (finer than the ~1–2 s the spec had recommended): ~7,200 entries × 16 bytes ≈ **~115 KB** for a 1-hour mix — still a trivial one-time fetch. The decoder fine-re-syncs within the bucket so seek *accuracy* is unaffected; at 0.5 s the in-bucket trim is sub-half-second, tighter than before. The shape (precomputed exact granule→byte, page-snapped) is unchanged. `[RESOLVED — §3.4a A]` --- ## 7. Acceptance criteria - **AC1 (headline) — Dual-format delivery works.** A track can be streamed as either lossless WAV or Ogg Opus 320 from the same `EntryKey`, selected per request; both play correctly through the bespoke Web Audio graph. - **AC2 — Opus is the low-data win.** The Opus artifact of a representative track is materially smaller than its lossless source (target ~1/4–1/5 the bytes); a long mix's Opus transfer is correspondingly smaller. - **AC3 — Additive, non-breaking (C2).** The existing lossless WAV path is byte-for-byte unchanged; a track with no Opus artifact still plays losslessly; `?format=opus` on such a track falls back to lossless (no 404, no silence). - **AC4 — Transcode at ingest as a background job, regenerable (C6, OQ6).** A new upload stores the source and is playable losslessly **immediately**; the Opus artifact (+ seek-index/setup-header sidecar) is produced by a **background job** afterward; a transcode failure does not block the upload or break playback; a Backfill-Opus action (re)generates artifacts for existing tracks; replace-audio regenerates the Opus artifact and its sidecar from the new source. - **AC4a — Post-Processing phase is visible on the upload meter (OQ6, §3.1a).** After the byte-transfer and server-persist phases, the CMS upload progress UI shows a **Post-Processing** phase reflecting the background transcode (queued → transcoding → done/failed). The admin is never blocked waiting on the transcode; the track is live before Post-Processing finishes. - **AC5 — Opus seek via the existing `Range` path (C3).** Forward and backward seek in an Opus stream resolve through the landed `Range: bytes=X-` primitive, with the offset coming from `OpusFormatDecoder.calculateByteOffset`; no new seek *transport* mechanism is introduced. - **AC5a — Seek-index + setup-header sidecar exists and is fetched once (§3.4a).** Every track with an Opus artifact has a sidecar carrying the setup header (`OpusHead`/`OpusTags`) and the granule→byte seek index; the client fetches and parses it once on track load (into `OpusSeekData`) before issuing any seek. - **AC9 (the seek-accuracy criterion) — an Opus seek lands at the *correct* time, not approximately.** Seeking to time `t` in an Opus stream resolves via the precomputed index and lands playback at `t` (within the fine-resync tolerance — sub-half-second at the chosen 0.5 s bucket granularity), **measurably accurate**, not a `byteRate`/interpolation estimate. Verifiable: seek to a known marker (e.g. a downbeat at a known timestamp) and confirm playback resumes there, not seconds off. This holds **without** the full PCM decoded in memory (composes with Phase 21). - **AC6 — No format branches leak (C4).** The only Opus-specific code is `OpusFormatDecoder`, its `OpusSeekData` (carrying the index), the one `createFormatDecoder` selection arm, the transcode processor (+ index build), the sidecar artifact + its serving, and the delivery param resolution. The format-agnostic player/scheduler code is unchanged. - **AC7 — Capability-safe default (OQ2).** A browser that cannot decode Ogg Opus is served (or falls back to) the lossless path and plays audio; no listener gets silence because of codec support. - **AC8 — Windowing-ready (the Phase 21 handshake).** The `OpusFormatDecoder`'s **index-based** byte↔time resolver is the one Phase 21's windowed refill calls; Opus playback must be windowable by the same machinery, and a windowed refill that opens away from byte 0 still decodes (setup header from the sidecar — UC9). Verified jointly when Phase 21 lands on top (see §8 / Phase 21 cross-ref). - **AC10 — The Settings menu hosts the quality toggle and persists the choice (§4 / §4a).** The public app bar opens a Settings menu containing a "Streaming quality" control (Low-data / Lossless, defaulting to Low-data, capability-gated); changing it persists via the `streamQuality` cookie and is seeded at prerender on the next visit (no flash). The menu shell is built so a future dark-mode entry can plug in without restructuring. --- ## 8. Wave decomposition Dependency shape: `18.1 → 18.2 → {18.3, 18.4}`, with `18.5` (backfill + e2e) and `18.6` (settings menu) on top. **18.1 (the transcode + seek-index/setup-header derived artifacts) is the cold-start prerequisite** — until those artifacts exist, nothing downstream has bytes to serve, decode, or seek against. 18.3 (delivery param) and 18.4 (the decoder + index resolver) are largely parallel once 18.2 (storage/lookup) settles, but both need artifacts to test against. **18.6 (the Settings menu) is the only wave with no audio-pipeline dependency** — it can proceed in parallel with the whole stack; it merely needs the `?format=` mechanism (18.3) wired before the toggle has anything to drive. - **18.1 — Ingest transcode + seek-index + setup-header (cold-start; load-bearing).** New `OpusTranscodeService`/processor in `DeepDrftContent`, invoked post-store from `UnifiedTrackService.UploadAsync` alongside `WaveformProfileService`, **as a background job** (OQ6, §3.1a); produces Ogg Opus fullband 320; **walks the encoded stream once to build the granule→byte seek index and extract the `OpusHead`/`OpusTags` setup header** (§3.4a A/B); stores the Opus bytes **and** the combined seek/setup **sidecar** as derived artifacts (S2 vault recommended). Failure-tolerant (C6). **Independent of the delivery/decoder waves; can begin immediately.** - **18.2 — Storage + lookup contract.** The derived-artifact key/vault convention (Opus bytes + sidecar) and the server-side resolution "given `EntryKey` + format, return the right `AudioBinary` + content-type (+ the sidecar on its own endpoint/path)," including the C2 fallback (no Opus → lossless). **Depends on 18.1** (artifacts must exist to resolve to). - **18.3 — Delivery: format param + sidecar serving + proxy threading.** `?format=opus|lossless` on the `DeepDrftAPI` track stream endpoint (resolves via 18.2), forwarded through the `DeepDrftPublic` `TrackProxyController` (mirror the existing `offset` param threading), and the `Range` handler serving the chosen artifact's bytes; **plus serving the seek/setup sidecar** (a `GET …/opus/seekdata`-style path, proxied the same way). The player sends the format param via `TrackMediaClient`. **Depends on 18.2.** Parallel-ok with 18.4. - **18.4 — `OpusFormatDecoder` + the index-based seek resolver in the player stack.** New `IFormatDecoder` implementation: Ogg-page-aligned `getAlignedSegmentSize` via `OggS` scan; `OpusHead`/`OpusTags` setup carry in `wrapSegment`/the continuation path (sourced from the cached sidecar, §3.4a B); **`calculateByteOffset` that binary-searches the precomputed seek index** (NOT interpolation), with an `OpusSeekData` accelerator holding the parsed index + setup bytes; the **one-time sidecar fetch + parse** on track load. One new arm in `AudioPlayer.createFormatDecoder` on `audio/ogg`/`audio/opus`. Capability detection for the lossless fallback (§3.4, OQ2). **Depends on 18.2** (needs Opus bytes + sidecar). Parallel-ok with 18.3; they meet at 18.5. - **18.5 — Backfill + replace-audio + end-to-end validation (incl. seek accuracy).** The Backfill-Opus CMS bulk action (third sibling to Generate-Profiles / Backfill-High-res), which (re)builds Opus bytes + the sidecar for existing tracks; replace-audio Opus + sidecar regeneration; and the AC1–AC10 acceptance pass — **including AC9 (an Opus seek lands at the correct time, not approximately)** and AC8's confirmation that Opus is windowable (index resolver + sidecar setup header) so Phase 21 can build on it. **Depends on 18.1–18.4.** - **18.6 — Public Settings menu + the quality toggle (the listener selection UX).** The new public-site Settings-menu shell (§4a): an app-bar trigger + MudBlazor menu hosting a settings-item abstraction, the `PublicSiteSettings`/`ListenerSettings` object, and the dark-mode-pattern persistence seam (`streamQuality` cookie + a `DeepDrftPublic` prerender-read service + `PersistentComponentState` bridge + client cookie service). The **quality toggle is its first occupant** (Low-data/Lossless, Opus default, capability-gated), driving the `?format=` the player sends (needs 18.3). Built design-for-adaptability so dark mode can plug in later without restructuring (not migrated now). **Depends on 18.3** (the toggle needs the format mechanism); the menu *shell* can be built ahead of that. *Splittable* into "menu shell" + "toggle plugs in" if Daniel wants the shell proven first — but small enough to land together (§4a). --- ## 9. Cross-references (read before implementing) - `CONTEXT.md §5` "Non-WAV formats" — the deferred intent this phase realizes (now concrete: derived Opus low-data path, not generic format support). - `PLAN.md` Phase 21 / `product-notes/phase-21-windowed-streaming-buffer.md` — **sequenced AFTER this phase.** Phase 21's C5 invariant ("WAV-only shipping target; must not foreclose MP3/FLAC") is now driven by Opus's VBR/paged seek math; Phase 21 OQ5 (adopt MSE) is resolved **NO** — the bespoke graph stays (the same C1 decision recorded here). Windowing a VBR/Opus stream uses `OpusFormatDecoder.calculateByteOffset`'s **accurate index-based mapping** (§3.4a — *not* the earlier "approximate page-interpolation"; that language in the Phase 21 doc is corrected). Phase 21's windowed refill calls the **same** index resolver an explicit seek does (§3.4a D), and a window that opens away from byte 0 still decodes via the sidecar setup header (UC9). - `PLAN.md` Phase 4 (landed) / `COMPLETED.md` — the HTTP `Range: bytes=X-` primitive Opus seek reuses. - `PLAN.md` Phase 1.5 (gapless) / 1.6 (track-skip on error) / 1.7 (Safari) — 1.5's "encoder padding/priming" caveat applies to Opus (it has pre-skip samples in `OpusHead`); 1.6's byte-scan-to-next-frame is the Ogg-page-sync analogue; 1.7's Safari floor intersects §3.4's Ogg-Opus `decodeAudioData` support (Safari < 18.4). - `PLAN.md` Phase 12 / `product-notes/phase-12-waveform-visualizer-generalization.md` — the `WaveformProfileService` derived-artifact-at-ingest + regenerate pattern this transcode mirrors (compute on upload, regenerate via CMS action / endpoint, its own `track-waveforms` vault → the S2 precedent). - `PLAN.md` Phase 9 — defines the `Mix` medium (single long track), the canonical low-data case. - `PLAN.md` Phase 16 — play/share telemetry keys on one track identity; the §3.6 road-not-taken (one-row-per-format) would have fractured this — kept to one identity, two artifacts. - `DeepDrftContent/Processors/AudioProcessor.cs` + `AudioProcessorRouter` + `DeepDrftContent/CLAUDE.md` — the existing format-router and the `WaveformProfileService` derived-artifact seam; 18.1 lives here. - `DeepDrftPublic/Interop/audio/IFormatDecoder.ts` — the strategy interface `OpusFormatDecoder` implements; `FlacFormatDecoder.ts` is the nearest prior art (setup-bytes carry + frame-sync scan). - `DeepDrftPublic/Interop/audio/AudioPlayer.ts` (`createFormatDecoder`, lines 117–125) — the decoder registry gaining the Opus arm. - `DeepDrftPublic.Client/Clients/TrackMediaClient.cs` + `DeepDrftPublic/Controllers/TrackProxyController.cs` — the media fetch + proxy that thread the new `?format=` param (mirroring `offset`), and proxy the new seek/setup sidecar fetch. - Root `CLAUDE.md` "Theming and dark mode" + `DarkModeService` (in `DeepDrftPublic`) + `DarkModeSettings` (`DeepDrftPublic.Client.Common`) — the cookie → prerender-read → `PersistentComponentState` → client cookie-service seam the **streaming-quality preference** (§4a) mirrors exactly; the eventual dark-mode- into-the-Settings-menu migration consolidates two copies of this seam. - `DeepDrftPublic.Client` `NowPlayingStats.razor` / `StatsClient` — the `PersistentComponentState` prerender-bridge precedent (prerender fetch carried into WASM without a re-fetch/flash), the pattern the quality preference's bridge follows; see the `tracksview-persistent-state-seam` auto-memory. ## Sources - Ogg Opus support in `decodeAudioData`: Chrome/Firefox long-standing; Safari added Ogg-Opus at 18.4 (macOS 15.4 / iOS 18.4, March 2025) — prior Safari decoded Opus only in CAF. https://chromestatus.com/feature/5649634416394240 ; https://www.testmuai.com/learning-hub/opus-audio-codec-browser-support/