58 KiB
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–OQ6 are resolved (see §6 — each marked RESOLVED, kept visible per file convention). 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 +Rangehandler + the new seek-index and setup-header sidecar endpoints, §3.4a) +DeepDrftPublicproxy (TrackProxyController) +DeepDrftPublic.Clientplayer stack (StreamingAudioPlayerService,TrackMediaClient) +DeepDrftPublic/Interop/audioTS decoders (AudioPlayer.createFormatDecoderregistry, a newOpusFormatDecoder). - 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 inDeepDrftPublic(alongsideDarkModeService).
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 thetracksvault.- 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 onContent-Type:audio/mpeg|audio/mp3→Mp3FormatDecoder,audio/flac|audio/x-flac→FlacFormatDecoder, default →WavFormatDecoder. All three decoders exist and implementIFormatDecoder.IFormatDecoder(IFormatDecoder.ts) is a clean per-format strategy:tryParseHeader,getAlignedSegmentSize,wrapSegment,calculateByteOffset, plus aFormatInfocarryingbyteRate,blockAlign,audioDataOffset, and aseekDataaccelerator slot (already polymorphic:Mp3VbrSeekData | FlacSeekData). This is the seam anOpusFormatDecoderslots 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 <media> 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→PlaybackSchedulerpipeline. (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
tracksvault 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'sRange: bytes=X-→206primitive (clientTrackMediaClient→DeepDrftPublicproxy →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 sameIFormatDecoder.calculateByteOffsetseam the MP3/FLAC decoders already use — no second seek mechanism. - C4 — Opus slots the
IFormatDecoderregistry; no format branches leak elsewhere. The newOpusFormatDecoderis selected byAudioPlayer.createFormatDecoderonContent-Type: audio/ogg/audio/opus. The rest of the player stack stays format-agnostic. Noif (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/EntryKeyaddresses both artifacts; the format is a parameter on the stream request (query param orAcceptnegotiation — 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 existingtracksvault under a derived key, or in a new sibling vault (see §3.2 options). Either way it isAudioBinarywith the.opus/.oggextension 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. anOpusTranscodeService/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 placeWaveformProfileServicecomputes 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:
- Transfer + store + persist (existing, synchronous). The WAV body streams in (the landed
ProgressStreamContenttwo-phase cancellation), the source is stored in the vault, theTrackEntityis 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. - 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=opusfor 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.razoralready 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 existingoffsetquery 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 rightContent-Type, which the player's existingcreateFormatDecoderthen dispatches on. No new decoder-selection mechanism — the response content-type does the work it already does. - D-b — HTTP content negotiation (
Accept: audio/oggvsaudio/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.getAlignedSegmentSizealigns to Ogg page boundaries — scan for theOggScapture pattern (analogous to FLAC's frame-sync scan; theIFormatDecoderinterface already passesrawDatatogetAlignedSegmentSizefor exactly this reason).wrapSegment/ the continuation path prepends theOpusHead/OpusTagssetup bytes to a mid-stream page run before handing it todecodeAudioData(analogous to FLAC'sstreamInfoBytescarry inFlacSeekData). The setup bytes come from the setup-header mechanism (§3.4a), not from re-reading the stream start.- A new
OpusSeekDatavariant joinsMp3VbrSeekData | FlacSeekDatain theseekDataaccelerator 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:
- Seeking must be preserved for Opus without having the full PCM decoded in memory.
- Raw byte-offset seek is inadequate — a VBR Opus stream has no linear time↔byte relationship, so
byteRatemath and even rough page interpolation are not accurate enough. - We need an accurate transfer function: seek-time → true file byte offset.
- 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. auint64 granulepos+uint64 byteOffsetper 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 (the one real tuning knob). 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. Recommend a coarser bucket: one index entry per ~1–2 seconds of audio (snap each bucket boundary to the nearest enclosing page start, so every indexed offset is still an exact page boundary). At ~1 s granularity a 1-hour mix is ~3,600 entries × 16 bytes ≈ ~58 KB — a trivial one-time fetch, and 1 s seek resolution is more than fine (the decoder re-syncs to the exact page within the bucket anyway — see the client flow). Per-page precision is the fallback if 1 s buckets ever prove too coarse, at a larger index. The number is staff-engineer's call; the shape (precomputed exact granule→byte, bucketed, snapped to page starts) is fixed.
- 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; thetrack-waveformsprecedent). Keep it a separate vault resource (e.g.{entryKey}.seekidxin atrack-opusvault, or its owntrack-opus-indexvault) 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/OpusTagspages as the opening bytes.OpusFormatDecoder.tryParseHeadercaptures and retains those setup bytes (exactly asWavFormatDecoderretains the parsed WAV header forreinitializeForRangeContinuationtoday, and FLAC retainsstreamInfoBytes). 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 theOpusHead/OpusTagsbytes, 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:
- Resolve time → byte offset (accurate). Listener seeks to
tseconds.OpusFormatDecoder.calculateByteOffset(t)does a binary search in the index for the largest entry withtime ≤ t, returns its exact (page-start)byteOffset. No interpolation, nobyteRatemath. (For WAV this method stays the exact CBR calculation it is today — the seam is identical; only the Opus implementation reads an index.) - Range fetch from the offset. Issue
GET api/track/{id}?format=opuswithRange: bytes={byteOffset}-— the landed Phase 4 Range primitive, unchanged. Server streams raw Opus bytes from that exact page boundary (206 Partial Content). - Prepend the cached setup header + decode. The continuation path (the Opus analogue of
StreamDecoder.reinitializeForRangeContinuation) prepends the retained/sidecarOpusHead/OpusTagsbytes to the incoming page run, then feeds it todecodeAudioData. Because the index offset is an exact page start, the stream is immediately Ogg-sync-aligned. - 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 attprecisely. With ~1 s buckets the trim is sub-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); theIFormatDecoder.calculateByteOffsetseam; 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 insideOpusFormatDecoder. 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 (thetrack-waveformsprecedent 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 (DeepDrftAPIresolves a param to an artifact, and serves the sidecar); decode is theOpusFormatDecoder's concern; byte↔time math stays inside that decoder viacalculateByteOffset(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/EntryKeyis 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
TrackEntityrow (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
DarkModeSettingsanalogue, generalized — call it e.g.PublicSiteSettings/ListenerSettings. Dark mode's existingDarkModeSettingscan 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"):
- Cookie — a
streamQualitycookie (365-day, likedarkMode), the durable truth. - Server prerender read — a service in
DeepDrftPublic(sibling toDarkModeService) 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). PersistentComponentStatebridge — the seeded preference carries from server prerender into the WASM render (the same bridgeDarkModeSettingsandNowPlayingStats/StatsClientalready use), so the client boots already knowing the quality without a re-read flash or a re-fetch.- 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=opusrequested, 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
Rangepath; the offset comes from theOpusFormatDecoder'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/OpusTagssetup 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 six original open questions are resolved. Kept visible per file convention, each with the decision and the section that now carries it. One new open question (OQ7) is raised by the seek-model design; it is a narrow tuning/scoping call, not a blocker.
- 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
streamQualitycookie seeded at server prerender → settings object →PersistentComponentStatebridge 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(broadestdecodeAudioDatasupport). 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) — narrow, non-blocking:
- OQ7 — Seek-index granularity (tuning, leans implementation). The seek index trades precision against
size: per-Ogg-page (most precise, largest) vs. coarser time buckets snapped to page starts. Recommend
~1–2 s buckets (~58 KB for a 1-hour mix at 1 s; the decoder fine-re-syncs within the bucket so seek
accuracy is unaffected — only the in-bucket trim distance changes). This is largely staff-engineer's
call at implementation; flagged because the number is a deliberate choice and Daniel may have a feel
for acceptable index size vs. in-bucket trim. Does not block — the shape (precomputed exact
granule→byte, page-snapped) is fixed regardless of the bucket size.
[Daniel steer — not a blocker]
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=opuson 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
Rangepath (C3). Forward and backward seek in an Opus stream resolve through the landedRange: bytes=X-primitive, with the offset coming fromOpusFormatDecoder.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 (intoOpusSeekData) before issuing any seek. - AC9 (the seek-accuracy criterion) — an Opus seek lands at the correct time, not approximately.
Seeking to time
tin an Opus stream resolves via the precomputed index and lands playback att(within the fine-resync tolerance — sub-second at the recommended bucket granularity), measurably accurate, not abyteRate/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, itsOpusSeekData(carrying the index), the onecreateFormatDecoderselection 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
streamQualitycookie 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 inDeepDrftContent, invoked post-store fromUnifiedTrackService.UploadAsyncalongsideWaveformProfileService, 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 theOpusHead/OpusTagssetup 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 rightAudioBinary+ 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|losslesson theDeepDrftAPItrack stream endpoint (resolves via 18.2), forwarded through theDeepDrftPublicTrackProxyController(mirror the existingoffsetparam threading), and theRangehandler serving the chosen artifact's bytes; plus serving the seek/setup sidecar (aGET …/opus/seekdata-style path, proxied the same way). The player sends the format param viaTrackMediaClient. Depends on 18.2. Parallel-ok with 18.4. - 18.4 —
OpusFormatDecoder+ the index-based seek resolver in the player stack. NewIFormatDecoderimplementation: Ogg-page-alignedgetAlignedSegmentSizeviaOggSscan;OpusHead/OpusTagssetup carry inwrapSegment/the continuation path (sourced from the cached sidecar, §3.4a B);calculateByteOffsetthat binary-searches the precomputed seek index (NOT interpolation), with anOpusSeekDataaccelerator holding the parsed index + setup bytes; the one-time sidecar fetch + parse on track load. One new arm inAudioPlayer.createFormatDecoderonaudio/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/ListenerSettingsobject, and the dark-mode-pattern persistence seam (streamQualitycookie + aDeepDrftPublicprerender-read service +PersistentComponentStatebridge + 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.mdPhase 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 usesOpusFormatDecoder.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.mdPhase 4 (landed) /COMPLETED.md— the HTTPRange: bytes=X-primitive Opus seek reuses.PLAN.mdPhase 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 inOpusHead); 1.6's byte-scan-to-next-frame is the Ogg-page-sync analogue; 1.7's Safari floor intersects §3.4's Ogg-OpusdecodeAudioDatasupport (Safari < 18.4).PLAN.mdPhase 12 /product-notes/phase-12-waveform-visualizer-generalization.md— theWaveformProfileServicederived-artifact-at-ingest + regenerate pattern this transcode mirrors (compute on upload, regenerate via CMS action / endpoint, its owntrack-waveformsvault → the S2 precedent).PLAN.mdPhase 9 — defines theMixmedium (single long track), the canonical low-data case.PLAN.mdPhase 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 theWaveformProfileServicederived-artifact seam; 18.1 lives here.DeepDrftPublic/Interop/audio/IFormatDecoder.ts— the strategy interfaceOpusFormatDecoderimplements;FlacFormatDecoder.tsis 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 (mirroringoffset), and proxy the new seek/setup sidecar fetch.- Root
CLAUDE.md"Theming and dark mode" +DarkModeService(inDeepDrftPublic) +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.ClientNowPlayingStats.razor/StatsClient— thePersistentComponentStateprerender-bridge precedent (prerender fetch carried into WASM without a re-fetch/flash), the pattern the quality preference's bridge follows; see thetracksview-persistent-state-seamauto-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/