Skip to content

WS3b: optional asa-dsp loudness backend behind ASA_LOUDNESS_BACKEND (default-off) - #132

Merged
slittycode merged 2 commits into
mainfrom
claude/wasm-backend-loudness
Jun 2, 2026
Merged

WS3b: optional asa-dsp loudness backend behind ASA_LOUDNESS_BACKEND (default-off)#132
slittycode merged 2 commits into
mainfrom
claude/wasm-backend-loudness

Conversation

@slittycode

Copy link
Copy Markdown
Owner

What this is

Wires the loudness-spectro-wasm core (asa-dsp) into the backend as a default-off alternative loudness source, selected by ASA_LOUDNESS_BACKEND=essentia|wasm (default essentia). 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-cli binary (source-identical to the WASM core) on a temp WAV.

Concern pyo3 (planned) measure-cli subprocess (this PR)
panic = "abort" blocker requires separate workspace sidestepped (measure-cli is already a binary)
loudness-wasm CI risk of reddening (libpython link) cannot red it — pure Python this side
Input formats needs PCM marshalling format-agnostic (backend already decoded → temp WAV)
New build artifact a pyo3 .so CI must build none — reuses the parity binary

Scope — LUFS scalars only (honest)

Overrides the four scalars the WS3a parity harness validated at ±0.1 LU: lufsIntegrated, lufsRange, lufsMomentaryMax, lufsShortTermMax. Deliberately not swapped:

So it's a scalar override, not a full engine replacement. The loudness contract (field names, units, rounding) is unchanged.

Safety / verification

  • Default essentiaapply_loudness_backend is a no-op. test_analyze + the golden snapshot pass unchanged (the default path is byte-identical).
  • Every WASM failure mode (missing binary, non-zero exit, unparseable or null-integrated output) degrades gracefully back to Essentia with a stderr [warn] — analysis never crashes.
  • 10 unit tests mock the subprocess (the binary isn't built in the Python CI job) and cover selection, the scalar override + curve passthrough, and all fall-back paths.

Caveats

https://claude.ai/code/session_01Wq1vE1D7o3W9xZKSHXz43Q


Generated by Claude Code

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 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: 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 root

loudness_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] = value

Test 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
@slittycode
slittycode merged commit c17612f into main Jun 2, 2026
3 checks passed
@slittycode
slittycode deleted the claude/wasm-backend-loudness branch June 2, 2026 03:44
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