Loudness levelling (EBU R128, -14 LUFS) measured in the browser with WebAssembly

- frontend/wasm/loudness.c → loudness-wasm.js (1.5 KB, base64-embedded; built by
  scripts/build-loudness-wasm.sh with clang or Zig's clang): K-weighting + 100 ms
  energy steps. loudness.js does the 400 ms gating (abs -70 LUFS, rel -10 LU)
  with an identical JS fallback. EBU Tech 3341 sine cases measure -23.0/-33.0
  on both engines; a real track reads -21.7 vs ffmpeg ebur128 -21.8.
- The device measures from audio it already has (saved copy or the server
  cache's m4a sidecar, songs up to 12 min, decoded natively at 22.05 kHz) and
  reports the number to /api/loudness, so each song is measured once for
  everyone; the server stores numbers only, no audio processing.
- The EQ graph gains a level stage (gain -> limiter) that eases each track
  toward -14 LUFS (-12..+9 dB). Settings -> Playback -> Level volume: on by
  default on Android/desktop, opt-in on iPhone (Web Audio stops sound when the
  screen locks there).
This commit is contained in:
Jonathan Sykes
2026-10-03 07:44:44 +08:00
parent 796293c0a5
commit ea9e11d054
10 changed files with 434 additions and 4 deletions

27
scripts/build-loudness-wasm.sh Executable file
View File

@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# Rebuild frontend/loudness-wasm.js (base64-embedded WebAssembly loudness core)
# from frontend/wasm/loudness.c. Uses clang (wasm32 + wasm-ld) when present,
# else Zig's bundled clang: `uv pip install ziglang` into a venv and set
# ZIG="<venv>/bin/python -m ziglang" (default: ~/.local/share/ytp-zig).
set -euo pipefail
cd "$(dirname "$0")/.."
tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
FLAGS=(-O3 -nostdlib -fno-builtin -ffreestanding
-Wl,--no-entry -Wl,--export=inbuf -Wl,--export=steps -Wl,--export=count -Wl,--export=setup -Wl,--export=feed
-Wl,--export-memory -Wl,--initial-memory=4325376 -Wl,-z,stack-size=8192 -Wl,--strip-all)
if command -v clang >/dev/null && clang --print-targets 2>/dev/null | grep -q wasm32; then
clang --target=wasm32 "${FLAGS[@]}" frontend/wasm/loudness.c -o "$tmp/l.wasm"
else
ZIG=${ZIG:-"$HOME/.local/share/ytp-zig/bin/python -m ziglang"}
$ZIG cc -target wasm32-freestanding "${FLAGS[@]}" frontend/wasm/loudness.c -o "$tmp/l.wasm"
fi
b64=$(base64 -w0 "$tmp/l.wasm")
cat > frontend/loudness-wasm.js <<JS
/* GENERATED by scripts/build-loudness-wasm.sh from frontend/wasm/loudness.c — do not edit.
* WebAssembly K-weighting + 100 ms energy steps for EBU R128 loudness (see loudness.js). */
(function (root) {
root.LOUDNESS_WASM_B64 = '$b64';
if (typeof module !== 'undefined' && module.exports) module.exports = root.LOUDNESS_WASM_B64;
})(typeof globalThis !== 'undefined' ? globalThis : this);
JS
echo "wasm: $(wc -c < "$tmp/l.wasm") bytes"