Files
ytplayer/frontend/wasm/loudness.c
Jonathan Sykes ea9e11d054 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).
2026-10-03 07:44:44 +08:00

55 lines
2.1 KiB
C

/* loudness.c — EBU R128 / ITU-R BS.1770 K-weighting + 100 ms energy steps.
*
* The heavy per-sample part of integrated-loudness measurement, compiled to
* WebAssembly (scripts/build-loudness-wasm.sh → frontend/loudness-wasm.js).
* JavaScript computes the filter coefficients for the sample rate, feeds the
* decoded audio in chunks through inbuf(), then reads the per-step energies
* and applies the 400 ms block gating (absolute -70 LUFS, relative -10 LU).
* No libc: only multiply-adds here. */
#define CHUNK 65536
#define MAXSTEPS 400000 /* 100 ms steps: ~11 hours */
static float in0[CHUNK], in1[CHUNK];
static double stepE[MAXSTEPS];
static int nsteps, nch, stepLen, stepPos;
static double acc;
static double k1[5], k2[5]; /* b0 b1 b2 a1 a2 for each stage */
static double st[2][8]; /* per channel: stage1 x1 x2 y1 y2, stage2 x1 x2 y1 y2 */
float *inbuf(int ch) { return ch ? in1 : in0; }
double *steps(void) { return stepE; }
int count(void) { return nsteps; }
void setup(int channels, int steplen,
double a0, double a1, double a2, double a3, double a4,
double b0, double b1, double b2, double b3, double b4) {
nch = channels > 1 ? 2 : 1;
stepLen = steplen > 0 ? steplen : 1;
stepPos = 0; nsteps = 0; acc = 0;
k1[0] = a0; k1[1] = a1; k1[2] = a2; k1[3] = a3; k1[4] = a4;
k2[0] = b0; k2[1] = b1; k2[2] = b2; k2[3] = b3; k2[4] = b4;
for (int c = 0; c < 2; c++) for (int j = 0; j < 8; j++) st[c][j] = 0;
}
void feed(int n) {
if (n > CHUNK) n = CHUNK;
for (int i = 0; i < n; i++) {
double e = 0;
for (int c = 0; c < nch; c++) {
double *s = st[c];
double x = c ? in1[i] : in0[i];
double y = k1[0] * x + k1[1] * s[0] + k1[2] * s[1] - k1[3] * s[2] - k1[4] * s[3];
s[1] = s[0]; s[0] = x; s[3] = s[2]; s[2] = y;
double z = k2[0] * y + k2[1] * s[4] + k2[2] * s[5] - k2[3] * s[6] - k2[4] * s[7];
s[5] = s[4]; s[4] = y; s[7] = s[6]; s[6] = z;
e += z * z;
}
acc += e;
if (++stepPos == stepLen) {
if (nsteps < MAXSTEPS) stepE[nsteps++] = acc;
acc = 0; stepPos = 0;
}
}
}