- 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).
55 lines
2.1 KiB
C
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;
|
|
}
|
|
}
|
|
}
|