7.2 KiB
JamesDSP in ytplayer — integration plan (2026-10-09)
What exists
- Standalone PWA/port:
~/development/personal/jamesdsp-web(GPL-3 port of JamesDSPManager's portable core to WASM).build.sh->web/dsp/jamesdsp.{js,wasm}(1.2 MB wasm), ES modulesweb/jamesdsp-node.js,params.js,worklet.js. Docs:INTEGRATION.md,PLAN.md,VERIFICATION.md(Chromium-only verified: 44.1/48 kHz, 15-band EQ, compander, bass, convolver, stereo, reverb, crossfeed, tube, liveprog/DDC/arbitrary EQ; fixed 128 MiB WASM heap per node; no real-phone/Safari testing). - Prototype in ytplayer: branch
codex/jamesdsp(3 commits on old base93d3b05): vendor/jamesdsp/{controller,jamesdsp-node,params,worklet}.js + wasm,JamesDSPIIFE in app.js, Settings group (enable, 5 presets, 3 EQ sliders, bass, crossfeed, limiter), sw.js precache, beta compose stack (docker-compose.beta.yml, beta.worship.hesed.sbs, already a deploy target "codex/jamesdsp"),tests/jamesdsp.spec.js.
Problems the prototype does not solve (why it can't just be merged)
- Second AudioContext + second MediaElementSource: app.js already owns
EQ(app.js ~5500: 10-band biquad EQ, Level/limiter, VocalReducer, widener) which callscreateMediaElementSourceonels.video/els.audio. An element can have only ONE source, ever. The two systems fight; whichever attaches first wins and the other throws/silences. - iOS background playback: Safari suspends Web Audio when the screen locks; the existing EQ is disabled on iOS unless
liveAudioIOSis on, and iOS saved songs get EQ rendered into a copy (EqRender,eqAudioUrl). The prototype ignores this -> likely silence on lock screen. - Dual elements / audio-continuity (
audio-continuity.js, master/secondary,Player.soundEl) and crossfade/preload swaps. - Assets: now manifest-driven, lazy
assets.jsongroups withcontract, N-1 retention, offline completion job, per-file hashed URLs. The wasm/worklet must be a lazy group (feature:jamesdsp), in the manifest, cached for offline, never in first paint/install-critical set, and fetched only after opt-in (or idle-prefetch only on Wi-Fi). The worklet module URL must be a hashed immutable URL resolvable offline; wasm bytes are fetched on the main thread and passed via processorOptions (already designed so). - CORS/proxy: streaming
<audio>must be same-origin (/api/...proxy) or havecrossOrigin='anonymous'set before src; OPFS blob URLs are fine. Verify every playback path (stream, saved/OPFS, P2P, rendered eqAudioUrl). - Memory/CPU: 128 MiB heap per node; one node only, created lazily, destroyed on disable; low-end phone CPU for FIR EQ/reverb/convolver; settings changes suspend audio briefly.
- Settings persistence: new
settings.jamesDspmust survive updates/profile sync (see settings-persist harnessperf/settings-persist.mjs), default OFF. - Licence: DSP port is GPL-3.0-or-later; ytplayer's licence/distribution must be checked and
frontend/vendor/jamesdsp/LICENSE.txt+ corresponding-source pointer + attribution in Settings/About. Owner decision flagged below.
Design decision (Claude, final)
One audio graph, one context. Do not add a second pipeline. Refactor the existing EQ graph into a small shared "AudioGraph" owner (single AudioContext, one MediaElementSource per element, retained) with an ordered insert slot: source -> [JamesDSP node OR legacy EQ/Level/Vocal/Widener chain] -> destination. Modes (Settings -> Playback & sound -> "Sound engine"): Standard (today's EQ, default) | JamesDSP (replaces the 10-band EQ/vocal/widener while on; Level/limiter remain via JamesDSP's own limiter+gain, loudness gain preserved). Never both EQ chains active -> no double processing. Bypass = exact (setBypass) and, when engine = Standard, JamesDSP is not loaded at all.
iOS: JamesDSP live only when liveAudioIOS (same opt-in as today's EQ) because of lock-screen suspension; for iOS saved songs offer the existing render-into-copy approach only if feasible via OfflineAudioContext + the same worklet (investigate; otherwise document unsupported and show the warning). Android/desktop: live.
UI: Settings group (keep prototype controls but extended: preset list, 15-band EQ graph/sliders, bass, stereo, crossfeed, reverb preset, tube, limiter, output gain, compressor on/off, A/B bypass button, import preset JSON, IR upload, Reset), themed per layout, works in all 4 layouts + dark/light/contrast. No Liveprog/DDC text effects in v1 (trusted-code risk) — hidden behind an "Advanced" flag off by default.
Phases (single Codex thread, sequential, one commit per step, branch codex/dsp-integrate from main; port the useful prototype code, do not merge the old branch wholesale)
- Audit + design note:
plans/jamesdsp-audit.md— map everycreateMediaElementSource/AudioContext/liveAudioIOS/EqRender/audio-continuity/Level/VocalReducer use; confirm single-source constraint; decide the AudioGraph seam; list iOS facts. - Refactor: extract AudioGraph (single ctx/source owner) with the Standard chain unchanged; all existing EQ tests + new unit tests pass; behaviour identical (EQ harness, level tests).
- Assets: vendor files from the standalone repo (pin the commit hash in
frontend/vendor/jamesdsp/VERSION), addassets.jsongroupfeature:jamesdsp(+contract), manifest entries, sw precache removal in favour of the manifest sync, offline-complete + lazy + migration harnesses green, first-paint bytes unchanged (assertperfcold-start numbers do not regress). - Engine: JamesDSP controller on AudioGraph, lazy create, one node, error -> auto fall back to Standard + toast (never silence), 44.1/48 rate check (create the AudioContext at 48000 where allowed; otherwise fall back), suspend/resume handling, destroy on disable, pause-safe config changes (never while
Player._wantsPlayingglitch-check: apply with short mute ramp). - UI: Settings group + themed controls + preset library (Flat, Bass boost, Vocal, Loudness, Headphone crossfeed, Bright, Warm), A/B bypass in the Now Playing "more" menu; persisted
settings.jamesDsp; included in profile sync + import/export; settings-persist harness extended. - Verification: Playwright (Chromium; WebKit where Web Audio worklets work) — measured RMS change per effect with a real clip, bypass exact, switching tracks/elements/crossfade keeps processing, saved/OPFS + streamed + P2P sources all processed, offline reload with engine on, error injection (corrupt wasm) falls back dry, memory (one 128 MiB heap only), CPU timing for worst-case preset,
node --test, manifest/hash, migration, ui-geometry/icons all green. Writeplans/jamesdsp-integration-report.mdwith limits (iOS lock-screen, real-ear tuning). - Beta: push branch only; deploy to beta.worship.hesed.sbs via app-deploy (branch codex/dsp-integrate) for owner listening tests BEFORE merging to main. Main merge only after owner OKs on beta.
Owner decisions needed (defaults chosen)
- Licence: GPL-3 inclusion in a (possibly public) repo — default: keep ytplayer repo as is, vendor under its own LICENSE, add attribution; confirm you are OK shipping GPL code in this app.
- iOS: default off unless
liveAudioIOSopt-in (matches today's EQ). - Default engine stays Standard until you approve on beta.