Skip to content

WS3c: in-browser loudness parity readout (flag-gated, WAV-only) - #133

Merged
slittycode merged 2 commits into
mainfrom
claude/wasm-frontend-readout
Jun 2, 2026
Merged

WS3c: in-browser loudness parity readout (flag-gated, WAV-only)#133
slittycode merged 2 commits into
mainfrom
claude/wasm-frontend-readout

Conversation

@slittycode

Copy link
Copy Markdown
Owner

What this is

An additive, default-off in-browser loudness readout: it measures the uploaded WAV with the asa-dsp (WASM core) and compares the LUFS scalars against the authoritative Phase 1 Essentia values. Phase 1 stays the source of truth (PURPOSE.md invariant #1) — this is a parity diagnostic, gated behind VITE_ENABLE_BROWSER_LOUDNESS.

Services (pure, fully unit-tested)

  • wavDecoder.ts — decodes uncompressed WAV (PCM 16/24/32 + float32, incl. WAVE_FORMAT_EXTENSIBLE) to interleaved f32 at the file's native rate. Deliberately not AudioContext.decodeAudioData, which resamples to the context rate and breaks ±0.1 LU EBU conformance. WAV-only; FLAC/MP3 report "WAV only" rather than guessing (FLAC decoding is a documented follow-up).
  • parity.tscomputeLoudnessParity → per-field deltas vs Phase 1, gated at ±0.1 LU. True peak is excluded (asa-dsp diverges on broadband per the WS3a: asa-dsp ↔ Essentia loudness parity harness (checkpoint) #129 parity report).
  • loader.ts — guarded dynamic import of the WASM module from a runtime URL (VITE_BROWSER_LOUDNESS_WASM_URL) with @vite-ignore, so the production build never tries to resolve the unbuilt, gitignored package. Returns null → "unavailable" on any failure. The result adapter (NaN→null, free()) is unit-tested.

UI

BrowserLoudnessPanel (flag-gated mount in AnalysisResults) reuses the already-retained App audioFile, with graceful states for non-WAV, wasm-unavailable, and error. Default-off → no Chromatic snapshot churn.

How the wasm-artifact decision was resolved

The plan flagged that CI gitignores pkg/ and doesn't build wasm. Rather than commit a binary or add a wasm toolchain to the frontend CI, the loader dynamically imports at runtime — so the build stays clean with no new CI step and no committed binary. Activation is a documented runtime step (build packages/loudness-spectro-wasm, serve pkg/, set the URL).

Verification limits (honest)

The WAV decoder, parity math, and result adapter are unit-tested (13 new tests). The actual WASM run and browser behavior are not verified here — no browser in CI, and building web-wasm needs wasm-bindgen-cli. The unavailable state documents activation. npm run lint + 729 unit tests + build all pass.

Notes

https://claude.ai/code/session_01Wq1vE1D7o3W9xZKSHXz43Q


Generated by Claude Code

Adds an additive browser-side loudness readout that measures the uploaded WAV
with the asa-dsp (WASM core) and compares the LUFS scalars against the
authoritative Phase 1 Essentia values. Phase 1 stays the source of truth
(PURPOSE.md invariant #1); this is a diagnostic, default-off behind
VITE_ENABLE_BROWSER_LOUDNESS.

Services (pure, fully unit-tested):
- wavDecoder.ts: decodes uncompressed WAV (PCM 16/24/32 + float32, incl.
  WAVE_FORMAT_EXTENSIBLE) to interleaved f32 at the file's NATIVE rate — no
  AudioContext.decodeAudioData resampling, which would break ±0.1 LU EBU
  conformance. WAV only; FLAC/MP3 report "WAV only" rather than guessing.
- parity.ts: computeLoudnessParity → per-field deltas vs Phase 1, gated at
  ±0.1 LU. True peak is deliberately excluded (asa-dsp diverges on broadband
  per #129).
- loader.ts: guarded dynamic import of the WASM module from a runtime URL
  (VITE_BROWSER_LOUDNESS_WASM_URL) with @vite-ignore so the build never tries
  to resolve the (unbuilt, gitignored) package; returns null → "unavailable"
  on any failure. The result adapter (NaN→null, free()) is unit-tested.

UI: BrowserLoudnessPanel (flag-gated mount in AnalysisResults) reuses the
retained App `audioFile`; graceful states for non-WAV, wasm-unavailable, error.

Verification limits (honest): the WAV decoder + parity math + result adapter are
unit-tested; the actual WASM run and browser behavior are NOT verified here (no
browser, and building web-wasm needs wasm-bindgen-cli). Activation requires
building packages/loudness-spectro-wasm and serving pkg/ — documented in the
unavailable state. 729 unit tests pass; build clean (no static wasm import).

https://claude.ai/code/session_01Wq1vE1D7o3W9xZKSHXz43Q

@slittycode slittycode left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE (submitted as COMMENT — can't self-approve)

Summary

Adds a flag-gated, default-off in-browser LUFS parity diagnostic that decodes WAV at native sample rate, runs the WASM core, and shows per-field deltas against the authoritative Phase 1 Essentia values. Phase 1 is read-only throughout; no mutations, no re-derivation. 13 new unit tests, all pass. Full suite (746 tests) passes clean. Frontend and WASM CI checks are green.

Findings

Worth considering:

wavDecoder.ts — the WAVE_FORMAT_EXTENSIBLE guard is chunkSize >= 24, but the SubFormat GUID starts at byte offset 24 of the chunk body, so safely reading 2 bytes there requires chunkSize >= 26. Off by 2. In any real EXTENSIBLE file chunkSize is 40, so this never fires. If it did (malformed file), DataView.getUint16 throws a RangeError that propagates to the panel's catch handler and shows the error state — no crash, just a confusing error message for a file that wasn't going to decode correctly anyway. Worth fixing to >= 26 to be accurate.

loader.tswasmModuleUrl casts import.meta to a looser type to reach env, but VITE_BROWSER_LOUDNESS_WASM_URL is already declared in vite-env.d.ts, so import.meta.env.VITE_BROWSER_LOUDNESS_WASM_URL works directly. The cast is harmless (lint accepts it with deps installed) but it's extra noise.

Test results

746 passed, 4 skipped (pre-existing fixture-gated tests), 0 failed. New browser-loudness suite: 13/13 — parity math, WAV decoder (including EXTENSIBLE, LIST chunk walk, 24-bit sign extension, null paths), and WASM adapter adapter all have meaningful tests.

Phase boundary check

Clean. parity.ts uses Pick<Phase1Result, …> in read-only fashion; the parity report is a new type that never feeds back into any Phase 1 field. BrowserLoudnessPanel reads phase1 for display only. AnalysisResults gains audioFile as a prop but Phase 1 state is unaffected.


Generated by Claude Code

…a cast

- wavDecoder: WAVE_FORMAT_EXTENSIBLE guard needs chunkSize >= 26 (not 24) to
  safely read the 2-byte SubFormat tag at body+24; add a test that decodes a
  real 40-byte EXTENSIBLE fmt chunk (the branch was previously untested).
- loader: VITE_BROWSER_LOUDNESS_WASM_URL is declared in vite-env.d.ts, so the
  import.meta cast is unnecessary — read import.meta.env directly.

https://claude.ai/code/session_01Wq1vE1D7o3W9xZKSHXz43Q
@slittycode
slittycode merged commit 2e64fe5 into main Jun 2, 2026
4 checks passed
@slittycode
slittycode deleted the claude/wasm-frontend-readout branch June 2, 2026 03:46
slittycode added a commit that referenced this pull request Jun 2, 2026
…ard (#135)

Two defects surfaced by an independent review of the loudness-v2 program
(#128#133):

patchSmith read spectralBalance bands as "dB above/below the mix average", but
the analyzer emits absolute band energy (10·log10(mean_energy), ~ -100..0). On
real tracks the sub oscillator never engaged (guard subBass<=1.0 against a real
~ -41 dB reading) and the acid cutoff pinned to the 200 Hz floor, while the
cited rationale misstated the measurement (invariant #2). Fix locally: add
meanBandDb() and read each band as a prominence relative to the 7-band mix
average — the analyzer contract is unchanged (mixDoctor/dashboard rely on the
absolute scale). Rationales are now true; tests use realistic absolute dB with
a regression case that fails on the old logic.

phase1Version was emitted but never consumed, so persisted pre-#128 runs
(stored verbatim in analysis_runtime, never migrated) replayed v1-unit values
into v2 consumers: a linear truePeak of 0.98 read as a +0.98 dBTP over (false
TRUE_PEAK_OVER / MISSING_LOUDNESS_ACTION) and a raw bpmConfidence of 3.2
rendered as 320%. Add _migrate_v1_loudness_units() at the top of _build_phase1
(the single chokepoint every persisted snapshot passes through): a payload
lacking "phase1.v2" gets truePeak linear→dBTP, bpmConfidence raw→0-1, plr
dropped and recomputed, then stamped v2. No-op for v2 payloads.

Verified: tsc clean; 750 frontend unit tests; vite build; 1102 backend
unittests (skipped=3). Playwright smoke could not run (chromium download
blocked by the sandbox network allowlist); both changes are pure logic covered
by the suites that ran.

https://claude.ai/code/session_01C2VkJk4JAUGtz3b9BWqVGE

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants