/insights · MixLab
Construire MixLab Analyzer : le LUFS BS.1770-4 dans le navigateur
La visite guidée par un ingénieur de terrain du fonctionnement de MixLab Analyzer dans le navigateur : biquads de pondération K, FFT, true peak, corrélation stéréo, et les choix de conception derrière le maintien de l’ensemble côté client.
MixLab Analyzer a commencé comme une démo destinée à prouver une idée : la plupart des outils de « mastering IA » sont une boîte noire, et il n’y a pas besoin d’une boîte noire pour donner aux créateurs une lecture sobre de leur mix. Il faut un meter, un spectre, une corrélation stéréo, et la discipline d’écrire le résultat en langage clair.
Voici la visite guidée par un ingénieur de terrain de la manière dont la version actuelle est construite. Le but n’est pas une théorie DSP exhaustive (il existe des manuels pour cela), mais de montrer les choix de conception et les compromis que nous avons faits pour garder l’ensemble côté client, rapide et honnête.
Le pipeline, en un coup d’œil
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
Il n’y a aucun modèle dans ce graphe. Il n’y a aucun appel d’API. Il y a quelques centaines de lignes de TypeScript qui lisent les tableaux Float32 que votre navigateur a déjà décodés.
Étape 1 : Décoder sans uploads
Le plus grand gain d’UX pour un analyseur audio, c’est « votre fichier ne quitte jamais le navigateur ». WebAudio vous l’offre gratuitement :
const ctx = new AudioContext();
const arrayBuffer = await file.arrayBuffer();
const buffer = await ctx.decodeAudioData(arrayBuffer.slice(0));
decodeAudioData gère WAV, MP3, M4A, OGG, FLAC et Opus sur les navigateurs modernes. Une fois qu’il a retourné, vous disposez d’un AudioBuffer avec numberOfChannels, sampleRate et un Float32Array par canal. À partir de là, tout n’est que de l’arithmétique JavaScript.
Le .slice(0) a son importance : certains navigateurs consomment le buffer sous-jacent lors du décodage, ce qui casse les lectures ultérieures. Le slicing crée une copie défensive.
Étape 2 : Pondération K BS.1770-4
Ce que la plupart des « loudness meters » font mal, c’est qu’ils rapportent du RMS en dBFS. C’est du metering peak/RMS, pas du loudness. Le loudness exige une pondération perceptuelle.
ITU-R BS.1770-4 (sur lequel s’appuie EBU R128) spécifie une cascade de filtres en deux étages :
- Un pré-filtre : un high-shelf à ~1681 Hz qui approxime la head-related transfer function.
- Un filtre RLB : un high-pass à ~38 Hz qui approxime le rolloff basse fréquence de l’oreille.
Les deux sont de simples biquads. Pour 48 kHz, la spécification publie directement les coefficients. Pour les autres fréquences d’échantillonnage, on les redérive avec un high-shelf et un high-pass de type RBJ aux mêmes fréquences prototypes et valeurs de Q :
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
}
Appliquer un biquad, c’est une direct-form-I à 5 multiplications par échantillon :
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;
}
Voilà l’intégralité du filtre de pondération K. Faites-le tourner sur la gauche et la droite (ou seulement la gauche en mono) en les gardant séparés.
Étape 3 : Gating et intégration
Le LUFS, ce n’est pas simplement l’énergie moyenne du signal pondéré K. La spécification impose un gating :
- Découpez le signal pondéré K en fenêtres de 400 ms avec 75 % de recouvrement (hop de 100 ms).
- Calculez la moyenne quadratique (mean square) par bloc.
- Calculez le loudness du bloc :
L_k = -0.691 + 10·log10(mean_square). - Écartez tout bloc dont
L_k < -70 LUFS(gate absolu). - Calculez la moyenne des mean-squares des blocs restants et dérivez le seuil relatif :
L_rel = -0.691 + 10·log10(mean) - 10. - Écartez tout bloc dont
L_k < L_rel(gate relatif). - Calculez la moyenne des mean-squares de ces blocs. C’est l’integrated loudness.
L’offset de -0.691 n’est pas arbitraire : c’est le point où la mean-square sommée sur les canaux s’aligne avec les niveaux de référence. Pour la stéréo, vous sommez les mean-squares de L et R avant de prendre le logarithme.
Implémenter cela en 30 lignes de TypeScript constitue l’intégralité de la base de code du metering LUFS. Pas de librairie, pas de modèle.
// per block
const ms = (sumL + sumR) / blockSize;
blockMeanSquares.push(ms);
blockLoudness.push(-0.691 + 10 * Math.log10(ms + 1e-12));
Pour le Loudness Range (LRA, selon EBU 3342), vous prenez la série de loudness short-term (fenêtre glissante de 3 secondes), vous gatez à -70 absolu et integrated - 20, vous triez les valeurs restantes, et vous prenez le 95e percentile moins le 10e. Voilà votre LRA.
Étape 4 : True peak par suréchantillonnage 4×
Le peak dans le domaine des échantillons rate les pics intersample : le pic analogique réel après reconstruction peut dépasser n’importe quel échantillon individuel. Les codecs de streaming amplifient le problème.
La parade standard consiste à suréchantillonner 4× avant de mesurer les pics. Pour l’estimation en qualité preview de MixLab, nous utilisons l’interpolation cubique Catmull-Rom :
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);
}
C’est plus rapide qu’un sinc fenêtré et cela s’approche à ~0,3 dB d’un meter certifié sur du matériel typique. Pour une conformité de niveau mastering, vous remplaceriez par un FIR polyphase, mais pour « est-ce assez fort pour m’alarmer ? », le cubique suffit.
Nous limitons la mesure du true peak aux 10 premières secondes du fichier afin de garder une latence raisonnable sur les fichiers longs.
Étape 5 : Mid/Side et largeur stéréo
Cette partie est presque trop simple :
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) vous donne un ratio de largeur. Mettez-le à l’échelle et clampez-le entre 0 et 1 pour l’affichage. Nous calculons également la corrélation de Pearson entre les canaux gauche et droit bruts :
const correlation = (sumProduct - n * meanL * meanR) / Math.sqrt(denL * denR);
Une corrélation inférieure à -0,05, c’est le signal « votre mix va perdre des éléments en mono ». C’est ce qui attrape le canal inversé en phase et l’image trop élargie.
Étape 6 : Analyse spectrale
Pour l’équilibre tonal, il nous faut un spectre de magnitude. Recette standard :
- Fenêtrez 4096 échantillons avec une fenêtre de Hann.
- Faites tourner une FFT radix-2 Cooley-Tukey.
- Prenez
|X[k]|pour la moitié inférieure. - Overlap-add la trame suivante (50 % de recouvrement).
- Moyennez sur les trames.
La FFT elle-même fait ~30 lignes de JS vanilla. On ne s’embête pas avec une librairie :
function fft(real: Float32Array, imag: Float32Array): void {
const n = real.length;
// bit reversal permutation
// butterfly stages
}
Pour une FFT de 4096 points sur 10 secondes d’audio à 48 kHz, vous traitez ~234 trames. Sur une machine moderne, c’est bien en deçà de 50 ms.
À partir du spectre de magnitude moyenné, nous dérivons :
- Centroïde spectral : la fréquence moyenne pondérée par la magnitude. Correspond à la brillance perçue.
- Rolloff spectral (85 %) : la fréquence en dessous de laquelle se situe 85 % de l’énergie totale. Une seconde lecture de la brillance.
- Platitude spectrale (flatness) : moyenne géométrique sur moyenne arithmétique. Proche de 1 = de type bruit, proche de 0 = tonal.
- Énergies par bande : magnitude intégrée dans 20–60, 60–200, 200–500, 500–2k, 2k–4k, 4k–8k, 8k–16k.
À partir des énergies par bande, le score de harshness est presence_band / mean(neighbour_bands). Tout ce qui dépasse nettement 1 signifie que la bande 2–4 kHz est plus chaude que ses voisines : le mordant qui fatigue les auditeurs.
Étape 7 : La lecture en langage clair
C’est là que la plupart des outils d’analyse s’arrêtent. On vous donne des chiffres et vous êtes censé savoir quoi en faire.
La couche de feedback de MixLab est bête, dans le meilleur sens du terme : un switch sur chaque plage de métrique avec un paragraphe écrit à la main pour chaque bande :
if (r.integratedLufs > -10) {
cards.push({
level: 'warn',
title: 'Loud — likely over-limited',
body: `${r.integratedLufs.toFixed(1)} LUFS sits well above streaming targets.`,
});
}
Le membre de droite de > est un nombre que vous lisez à l’écran. Le body est le genre de feedback qu’un ingénieur de terrain taperait dans un DM Slack. Il n’y a pas de modèle, pas de moteur de templates, pas d’IA : juste quelques dizaines de branches écrites avec soin.
Ce que nous avons délibérément évité
- Pas de bouton « enhance ». L’analyseur nomme les problèmes. Il ne les corrige pas. C’est un autre produit.
- Pas de modèle dans la boucle. Tout se fait au niveau du signal. La couche en langage clair, ce sont des recherches dans un dictionnaire, pas de la génération.
- Pas d’upload. La raison même pour laquelle cela fonctionne pour la confidentialité des créateurs, c’est qu’aucun audio ne traverse jamais le réseau.
- Pas de « score ». Un nombre unique couvrant toutes les métriques serait faux à chaque fois. Des lectures par métrique sont honnêtes.
Et ensuite
Le pipeline actuel tourne sur le thread principal. Sur les fichiers longs (10+ minutes), cela fait visiblement saccader l’UI pendant la passe LUFS. La prochaine itération déplace le gros du travail dans un AudioWorklet afin que le navigateur reste réactif d’un bout à l’autre. Mêmes algorithmes, runtime différent.
Pour une conformité de niveau mastering (soumissions broadcast, livrables de festival), nous ajouterons un mode meter certifié utilisant un suréchantillonneur polyphase et les coefficients vérifiés à toutes les fréquences d’échantillonnage standard. Pour l’instant, MixLab est le bon outil pour « ce mix est-il dans la bonne forme ? », et le mauvais outil pour « est-ce techniquement conforme R128 pour une soumission à BBC Radio ? »
En lien
More in MixLab
-
Pourquoi le mastering par IA a plafonné, et ce que veulent les créateurs ensuite
La première vague de mastering par IA a prouvé la viabilité de la catégorie. La deuxième vague doit la mériter. Un regard sur la façon dont un retour lisible pourrait remplacer le bouton « enhance » de la boîte noire.
-
Outils de mastering IA, comparés honnêtement (2026)
La comparaison par un ingénieur du métier de LANDR, eMastered, BandLab, iZotope Ozone AI et CloudBounce. Ce qu'ils font bien, là où ils pèchent, et lequel utiliser pour quelle tâche.
-
Sonomètre EBU R128 en ligne : ce qu'il mesure et pourquoi c'est important
Ce que LUFS, LU, LRA et true peak mesurent réellement, comment un sonomètre EBU R128 / BS.1770-4 fonctionne directement dans le navigateur, et quand en avoir besoin pour le podcast, le mastering, Spotify, Apple Music et le broadcast.