WS3b: optional asa-dsp loudness backend behind ASA_LOUDNESS_BACKEND (default-off) - #132
Conversation
Wires the loudness-spectro-wasm core (asa-dsp) into the backend as a default-off alternative loudness source for the LUFS scalars, selected by ASA_LOUDNESS_BACKEND=essentia|wasm (default essentia). Architecture (deviates from the plan's pyo3 sketch, deliberately): - Invokes the already-built native measure-cli binary on a temp WAV rather than binding via pyo3. This sidesteps the workspace's `panic = "abort"` profile (which blocks pyo3), needs no separate Cargo workspace, and — being pure Python on this side — cannot red the loudness-wasm CI job. The backend has already decoded the audio, so the temp WAV makes it format-agnostic. - Scope is the four LUFS scalars (integrated/range/momentary-max/short-term-max) — the EBU loudness the WS3a parity harness validated at ±0.1 LU. truePeak stays Essentia (asa-dsp's true peak diverges on broadband per #129) and lufsCurve stays Essentia (the CLI emits scalars, not per-frame curves). So this is a scalar override, not a full engine replacement. - Default stays Essentia (PURPOSE.md invariant #1); any failure (missing binary, non-zero exit, unparseable/null output) degrades gracefully back to Essentia. apps/backend/loudness_backend.py + a call after analyze_loudness in analyze.py. Tests mock the subprocess (the binary isn't built in the Python CI job): backend selection, scalar override + curve passthrough, and every fall-back path. The default path is unchanged — test_analyze + the golden snapshot still pass. https://claude.ai/code/session_01Wq1vE1D7o3W9xZKSHXz43Q
slittycode
left a comment
There was a problem hiding this comment.
Verdict: REQUEST CHANGES
Summary
Adds a default-off ASA_LOUDNESS_BACKEND=wasm alternative that overrides the four LUFS scalars with the native measure-cli binary (source-identical to the WASM core), falling back gracefully to Essentia on any failure. The design is sound — Phase 1 contract unchanged, truePeak/lufsCurve correctly left on Essentia, all 10 unit tests pass. One bug makes the feature non-functional when enabled without ASA_MEASURE_CLI.
Findings
Blocking
_REPO_ROOT in loudness_backend.py:55 is off by one:
_REPO_ROOT = Path(__file__).resolve().parents[1] # resolves to apps/, not repo rootloudness_backend.py lives at apps/backend/loudness_backend.py, so parents[0] = apps/backend, parents[1] = apps, parents[2] = repo root. The default measure-cli path becomes apps/packages/loudness-spectro-wasm/... which doesn't exist. Setting ASA_LOUDNESS_BACKEND=wasm always silently degrades to Essentia unless ASA_MEASURE_CLI is also set explicitly. The feature is broken by default.
The test output even shows it: looked at /home/user/ableton-sonic-analyzer/apps/packages/loudness-spectro-wasm/...
Fix: parents[2].
Should fix
loudness_backend.py:137-142 — when the CLI returns a valid integrated but null lra, _round1(payload.get("lra")) yields None, and that None gets written into the merged dict, potentially replacing a valid Essentia LRA reading. Integrated LUFS can be non-null for short content where LRA is undefined; those two conditions are not always paired. If the binary doesn't emit lra at all this silently loses the Essentia value. Easiest fix: only merge a field if the CLI value is not None.
for field in _OVERRIDABLE_FIELDS:
value = cli_result.get(field)
if value is not None:
merged[field] = valueTest results
10 / 10 pass. All mocked-subprocess paths exercise the fallback logic correctly. The happy-path test (test_wasm_overrides_lufs_scalars_keeps_curve) correctly asserts the curve passthrough. No test covers ASA_MEASURE_CLI set to a nonexistent path, but that branch is one line and the risk is low.
Phase boundary check
Clean. apply_loudness_backend only touches the Phase 1 loudness dict returned by analyze_loudness; it never reads from or writes to Phase 2/3 data. truePeak and lufsCurve pass through Essentia unchanged. No Phase 1 field names, units, or schema keys are modified.
Generated by Claude Code
…th null Addresses the PR #132 REQUEST CHANGES: - BLOCKING: _REPO_ROOT used parents[1] (= apps/), so the default measure-cli path was apps/packages/loudness-spectro-wasm/... which doesn't exist — the feature silently degraded to Essentia unless ASA_MEASURE_CLI was set. Fixed to parents[2] (repo root). Regression test asserts the default path resolves to the real <repo>/packages/loudness-spectro-wasm dir. - Should-fix: only override a LUFS scalar when the CLI value is non-null, so a null lra (undefined for short content where integrated is still valid) can't clobber Essentia's reading. Regression test covers it. 12/12 loudness_backend tests pass; the warn path now correctly points at <repo>/packages/... (no apps/ prefix). https://claude.ai/code/session_01Wq1vE1D7o3W9xZKSHXz43Q
What this is
Wires the
loudness-spectro-wasmcore (asa-dsp) into the backend as a default-off alternative loudness source, selected byASA_LOUDNESS_BACKEND=essentia|wasm(defaultessentia). Essentia stays authoritative (PURPOSE.md invariant #1); the flip is the owner's call after real-program parity.Architecture — deliberate deviation from the plan's pyo3 sketch
The plan envisioned pyo3 in an isolated Cargo workspace. Building it, I found a simpler, strictly safer path: invoke the already-built native
measure-clibinary (source-identical to the WASM core) on a temp WAV.panic = "abort"blockerloudness-wasmCI.soCI must buildScope — LUFS scalars only (honest)
Overrides the four scalars the WS3a parity harness validated at ±0.1 LU:
lufsIntegrated,lufsRange,lufsMomentaryMax,lufsShortTermMax. Deliberately not swapped:truePeakstays Essentia — asa-dsp's true peak diverges on broadband content (the ~0.6 dBTPwhite_noisegap flagged in WS3a: asa-dsp ↔ Essentia loudness parity harness (checkpoint) #129).lufsCurvestays Essentia — the CLI emits scalars, not per-frame curves.So it's a scalar override, not a full engine replacement. The loudness contract (field names, units, rounding) is unchanged.
Safety / verification
essentia→apply_loudness_backendis a no-op.test_analyze+ the golden snapshot pass unchanged (the default path is byte-identical).[warn]— analysis never crashes.Caveats
main(only the LUFS scalars are touched, and their unit is identical in v1/v2).ASA_LOUDNESS_BACKEND/ASA_MEASURE_CLIdocumented in CLAUDE.md.https://claude.ai/code/session_01Wq1vE1D7o3W9xZKSHXz43Q
Generated by Claude Code