Zum Inhalt
Anmelden

/insights · MixLab

MixLab Analyzer bauen: BS.1770-4 LUFS im Browser

Der Praxis-Rundgang eines Engineers, wie MixLab Analyzer im Browser funktioniert: K-Weighting-Biquads, FFT, True Peak, Stereokorrelation und die Designentscheidungen, die alles client-seitig halten.

2. Juni 2026 18 min read mixlabwebaudiolufsbs.1770build logdsp

MixLab Analyzer begann als Demo, um einen Punkt zu beweisen: Die meisten „AI-Mastering”-Tools sind eine Black Box, und man braucht keine Black Box, um Creators eine nüchterne Einschätzung ihres Mixes zu geben. Man braucht einen Meter, ein Spektrum, eine Stereokorrelation und die Disziplin, das Ergebnis in Klartext zu formulieren.

Dies ist der Praxis-Rundgang eines Engineers, wie die aktuelle Version gebaut ist. Das Ziel ist keine erschöpfende DSP-Theorie (dafür gibt es Lehrbücher), sondern die Designentscheidungen und die Kompromisse zu zeigen, die wir gemacht haben, um das Ganze client-seitig, schnell und ehrlich zu halten.

Die Pipeline auf einen Blick

user file
  ↓ decodeAudioData (WebAudio)
AudioBuffer (Float32 PCM, sample rate, channels)

  ├─ peak / RMS / crest                       → levels
  ├─ K-weighting (BS.1770 pre + RLB biquads)  → LUFS / LRA
  ├─ true peak (4× polyphase upsample)        → dBTP
  ├─ Mid/Side decomposition                   → width
  ├─ Pearson correlation                      → mono safety
  ├─ FFT (radix-2, Hann-windowed)             → spectrum
  ├─ band integrals                           → tonal balance
  └─ heuristics on band ratios                → harshness / muddiness

            AnalysisResult

        plain-language feedback

In diesem Graphen steckt kein Modell. Es gibt keinen API-Aufruf. Es gibt ein paar hundert Zeilen TypeScript, die die Float32-Arrays lesen, die dein Browser bereits dekodiert hat.

Schritt 1: Dekodieren ohne Uploads

Der mit Abstand größte UX-Gewinn für einen Audio-Analyser ist „deine Datei verlässt nie den Browser”. WebAudio liefert dir das kostenlos:

const ctx = new AudioContext();
const arrayBuffer = await file.arrayBuffer();
const buffer = await ctx.decodeAudioData(arrayBuffer.slice(0));

decodeAudioData verarbeitet WAV, MP3, M4A, OGG, FLAC und Opus über moderne Browser hinweg. Sobald die Funktion zurückkehrt, hast du einen AudioBuffer mit numberOfChannels, sampleRate und einem Float32Array pro Kanal. Ab hier ist alles JavaScript-Arithmetik.

Das .slice(0) ist wichtig: Manche Browser verbrauchen den zugrunde liegenden Buffer beim Dekodieren, was nachfolgende Lesezugriffe zerstört. Slicing erzeugt eine defensive Kopie.

Schritt 2: BS.1770-4 K-Weighting

Was die meisten „Loudness-Meter” falsch machen: Sie geben RMS in dBFS aus. Das ist Peak/RMS-Metering, keine Loudness. Loudness braucht perzeptive Gewichtung.

ITU-R BS.1770-4 (worauf EBU R128 aufbaut) spezifiziert eine zweistufige Filterkaskade:

  1. Ein Pre-Filter: ein High-Shelf bei ~1681 Hz, das die Head-Related Transfer Function annähert.
  2. Ein RLB-Filter: ein High-Pass bei ~38 Hz, das den Tieffrequenz-Rolloff des Ohrs annähert.

Beide sind einfache Biquads. Für 48 kHz veröffentlicht die Spezifikation die Koeffizienten direkt. Für andere Sample-Raten leitet man sie neu her, mit einem High-Shelf und High-Pass im RBJ-Stil bei denselben Prototyp-Frequenzen und Q-Werten:

function preFilterCoeffs(sr: number): Biquad {
  if (sr === 48000) {
    return { b0: 1.53512485958697, b1: -2.69169618940638, b2: 1.19839281085285,
             a1: -1.69065929318241, a2: 0.73248077421585 };
  }
  // RBJ high-shelf derivation as substitute
  const f0 = 1681.974450955533;
  const G = 3.999843853973347; // dB
  const Q = 0.7071752369554196;
  // ... compute b0..b2, a1..a2
}

Das Anwenden eines Biquads ist eine Direct-Form-I mit 5 Multiplikationen pro Sample:

function applyBiquad(data: Float32Array, c: Biquad): Float32Array {
  const out = new Float32Array(data.length);
  let x1 = 0, x2 = 0, y1 = 0, y2 = 0;
  for (let i = 0; i < data.length; i++) {
    const x0 = data[i];
    const y0 = c.b0*x0 + c.b1*x1 + c.b2*x2 - c.a1*y1 - c.a2*y2;
    out[i] = y0;
    x2 = x1; x1 = x0; y2 = y1; y1 = y0;
  }
  return out;
}

Das ist das vollständige K-Weighting-Filter. Lass es über links und rechts laufen (oder nur über links bei Mono) und halte beide getrennt.

Schritt 3: Gating und Integration

LUFS ist nicht einfach die mittlere Energie des K-gewichteten Signals. Die Spezifikation verlangt Gating:

  1. Teile das K-gewichtete Signal in Fenster von 400 ms mit 75% Überlappung (Hop von 100 ms).
  2. Berechne das mittlere Quadrat (Mean Square) pro Block.
  3. Berechne die Block-Loudness: L_k = -0.691 + 10·log10(mean_square).
  4. Verwirf jeden Block mit L_k < -70 LUFS (absolutes Gate).
  5. Berechne den Mittelwert der Mean-Squares der verbleibenden Blöcke und leite den relativen Schwellenwert ab: L_rel = -0.691 + 10·log10(mean) - 10.
  6. Verwirf jeden Block mit L_k < L_rel (relatives Gate).
  7. Berechne den Mittelwert der Mean-Squares dieser Blöcke. Das ist die Integrated Loudness.

Der Offset von -0.691 ist nicht willkürlich: es ist der Punkt, an dem das über die Kanäle summierte Mean-Square mit Referenzpegeln übereinstimmt. Bei Stereo summierst du die Mean-Squares von L und R, bevor du den Logarithmus nimmst.

Dies in 30 Zeilen TypeScript zu implementieren ist die gesamte Codebasis für LUFS-Metering. Keine Library, kein Modell.

// per block
const ms = (sumL + sumR) / blockSize;
blockMeanSquares.push(ms);
blockLoudness.push(-0.691 + 10 * Math.log10(ms + 1e-12));

Für Loudness Range (LRA, nach EBU 3342) nimmst du die Short-Term-Loudness-Reihe (gleitendes 3-Sekunden-Fenster), gatest bei -70 absolut und integrated - 20, sortierst die verbleibenden Werte und nimmst das 95. Perzentil minus das 10. Das ist deine LRA.

Schritt 4: True Peak via 4×-Oversampling

Der Peak im Sample-Bereich verfehlt Intersample-Peaks: der tatsächliche analoge Peak nach der Rekonstruktion kann jedes einzelne Sample überschreiten. Streaming-Codecs verstärken das Problem.

Die Standardlösung ist, vor der Peak-Messung mit 4× zu oversamplen. Für MixLabs Schätzung in Preview-Qualität verwenden wir Catmull-Rom-Kubikinterpolation:

for (let k = 0; k < 4; k++) {
  const t = k / 4;
  const a0 = -0.5 * y0 + 1.5 * y1 - 1.5 * y2 + 0.5 * y3;
  const a1 = y0 - 2.5 * y1 + 2 * y2 - 0.5 * y3;
  const a2 = -0.5 * y0 + 0.5 * y2;
  const a3 = y1;
  const v = ((a0 * t + a1) * t + a2) * t + a3;
  if (Math.abs(v) > peak) peak = Math.abs(v);
}

Das ist schneller als ein Windowed-Sinc und kommt bei typischem Material bis auf ~0,3 dB an einen zertifizierten Meter heran. Für Compliance auf Mastering-Niveau würdest du einen Polyphase-FIR einsetzen, aber für „ist das laut genug, um mich zu alarmieren?” reicht Kubik.

Wir beschränken die True-Peak-Messung auf die ersten 10 Sekunden der Datei, um die Latenz bei langen Dateien vertretbar zu halten.

Schritt 5: Mid/Side und Stereobreite

Dieser Teil ist fast zu einfach:

for (let i = 0; i < n; i++) {
  mid[i]  = (left[i] + right[i]) * 0.5;
  side[i] = (left[i] - right[i]) * 0.5;
}

rms(side) / rms(mid) ergibt ein Breitenverhältnis. Skaliere und klemme es für die Anzeige auf 0..1. Wir berechnen außerdem die Pearson-Korrelation zwischen den rohen linken und rechten Kanälen:

const correlation = (sumProduct - n * meanL * meanR) / Math.sqrt(denL * denR);

Eine Korrelation unter -0,05 ist das Signal „dein Mix verliert Elemente in Mono”. Es fängt den phasengedrehten Kanal und das überbreite Stereobild ab.

Schritt 6: Spektrumanalyse

Für die tonale Balance brauchen wir ein Magnitudenspektrum. Standard-Rezept:

  1. Fenstere 4096 Samples mit einem Hann-Fenster.
  2. Lass eine Radix-2-Cooley-Tukey-FFT laufen.
  3. Nimm |X[k]| für die untere Hälfte.
  4. Overlap-add das nächste Frame (50% Überlappung).
  5. Mittele über die Frames.

Die FFT selbst sind ~30 Zeilen Vanilla-JS. Eine Library machen wir uns nicht zur Mühe:

function fft(real: Float32Array, imag: Float32Array): void {
  const n = real.length;
  // bit reversal permutation
  // butterfly stages
}

Für eine 4096-Punkte-FFT über 10 Sekunden 48-kHz-Audio verarbeitest du ~234 Frames. Auf einer modernen Maschine liegt das deutlich unter 50 ms.

Aus dem gemittelten Magnitudenspektrum leiten wir ab:

  • Spektraler Schwerpunkt (Centroid): die nach Magnitude gewichtete mittlere Frequenz. Korrespondiert mit der wahrgenommenen Brillanz.
  • Spektraler Rolloff (85%): die Frequenz, unterhalb derer 85% der Gesamtenergie liegen. Eine zweite Sicht auf die Brillanz.
  • Spektrale Flachheit (Flatness): geometrisches Mittel über arithmetisches Mittel. Nahe 1 = rauschartig, nahe 0 = tonal.
  • Band-Energien: integrierte Magnitude in 20–60, 60–200, 200–500, 500–2k, 2k–4k, 4k–8k, 8k–16k.

Aus den Band-Energien ist der Harshness-Score presence_band / mean(neighbour_bands). Alles deutlich über 1 bedeutet, dass das 2–4-kHz-Band heißer ist als seine Nachbarn: der Biss, der Hörer ermüdet.

Schritt 7: Die Einschätzung in Klartext

Hier hören die meisten Analyse-Tools auf. Du bekommst Zahlen und sollst wissen, was du damit anfangen sollst.

MixLabs Feedback-Schicht ist im besten Sinne dumm: ein Switch über jeden Metrik-Bereich mit einem handgeschriebenen Absatz für jedes Band:

if (r.integratedLufs > -10) {
  cards.push({
    level: 'warn',
    title: 'Loud — likely over-limited',
    body: `${r.integratedLufs.toFixed(1)} LUFS sits well above streaming targets.`,
  });
}

Die rechte Seite von > ist eine Zahl, die du vom Bildschirm abliest. Der Body ist die Art von Feedback, die ein praktizierender Engineer in eine Slack-DM tippen würde. Es gibt kein Modell, keine Template-Engine, keine AI, nur ein paar Dutzend sorgfältig geschriebene Branches.

Was wir bewusst nicht gemacht haben

  • Kein „Enhance”-Button. Der Analyser benennt Probleme. Er behebt sie nicht. Das ist ein anderes Produkt.
  • Kein Modell im Loop. Alles läuft auf Signalebene. Die Klartext-Schicht besteht aus Wörterbuch-Lookups, nicht aus Generierung.
  • Kein Upload. Der ganze Grund, warum das für die Privatsphäre von Creators funktioniert, ist, dass nie Audio über das Netzwerk geht.
  • Kein „Score”. Eine einzelne Zahl über alle Metriken hinweg wäre jedes Mal falsch. Einschätzungen pro Metrik sind ehrlich.

Was als Nächstes kommt

Die aktuelle Pipeline läuft auf dem Main Thread. Bei langen Dateien (10+ Minuten) ruckelt das während des LUFS-Durchlaufs sichtbar in der UI. Die nächste Iteration verlagert die schwere Arbeit in ein AudioWorklet, damit der Browser durchgehend reaktionsfähig bleibt. Dieselben Algorithmen, eine andere Laufzeit.

Für Compliance auf Mastering-Niveau (Broadcast-Einreichungen, Festival-Deliverables) fügen wir einen Modus für zertifizierte Meter hinzu, mit einem Polyphase-Upsampler und den verifizierten Koeffizienten bei allen Standard-Sample-Raten. Für jetzt ist MixLab das richtige Tool für „ist dieser Mix in der richtigen Form?”, aber das falsche Tool für „ist das technisch R128-konform für eine Einreichung bei BBC Radio?”

Verwandt