~/textes/measured-audio-mastering
Remettre les basses qui n'ont jamais été enregistrées
J'ai préparé une sélection musicale pour un système maison de 1500 watts et synthétisé une octave de sous-graves manquante dans des pistes coupées sous les 40 Hz. Mesurer d'abord, corriger seulement ce que les chiffres justifient, ne toucher à rien d'autre.
Le système est un problème si on n'y prend pas garde. Deux amplis TPA3255, quelque chose comme 1200 watts dans les enceintes principales, un DAC ESS qui les alimente, et une paire de caissons Velodyne 15 pouces amplifiés qui montent le total à environ 1500 watts. Assez pour le sentir dans le sternum. Le travail consistait à préparer une sélection musicale qui frapperait vraiment sur ce système sans saboter les pistes qui n'avaient pas besoin d'aide.
Cette dernière phrase, c'est toute la discipline. Enfoncer une courbe en sourire partout, c'est facile. Laisser un bon matriçage tranquille, c'est plus dur. Alors je me suis donné la même règle que je m'applique pour tout ce que je touche : mesurer d'abord, changer seulement ce que la mesure justifie, revenir en arrière là où elle ne le justifie pas. Le bon matriçage, ce n'est pas du goût appliqué avec assurance. C'est de la mesure appliquée honnêtement.
Obtenir les bits en premier lieu
Avant tout DSP, il y avait un problème d'approvisionnement. Je fais court, c'est la partie la moins intéressante. Le téléchargeur que j'utilisais plafonnait silencieusement la sortie à de l'AAC 320k tout en rapportant un succès. C'est le genre de bogue le plus sournois : il n'échoue pas, il te remet tranquillement quelque chose de pire que ce que tu as demandé. Le vrai chemin sans perte arrivait sous forme de manifeste DASH MPD, alors j'ai dû analyser le manifeste, tirer la bonne représentation et remuxer le flux MP4 fragmenté en FLAC avec ffmpeg.
# The lossless representation arrives as fragmented MP4 inside a DASH manifest.
# No re-encode: copy the audio stream straight into a FLAC container.
ffmpeg -i "$FRAGMENTED_MP4" -c:a flac -compression_level 8 "out.flac"Aucune information d'authentification dans ce billet. Le travail intéressant vient en aval, une fois qu'on a les vrais bits. À partir d'ici, supposez que chaque entrée est du vrai sans perte.
Le déficit qu'il faut prouver avant de le corriger
Beaucoup de musique des années 1960 et 1970 est réellement coupée sous les 40 Hz. Taillée pour le vinyle, mixée sur des haut-parleurs incapables de reproduire cette octave de toute façon, alors il n'y avait aucune raison d'y mettre de l'énergie. Sur un système normal, on ne le remarquerait jamais. Sur une paire de caissons 15 pouces, ça veut dire que la partie la plus capable du système n'a rien à jouer. Les transducteurs restent assis là sur des pistes qui devraient faire trembler la pièce.
Le geste tentant, c'est d'attraper un rehaussement de graves et de déclarer ça réglé. Le bon geste, c'est de d'abord se demander si les graves manquent vraiment, parce qu'un filtre en plateau bas sur une piste qui a déjà du grave plein le bas du spectre ne fait que la rendre boueuse et écrêtée.
Alors j'ai mesuré l'énergie sous 40 Hz dans chaque candidate avant de toucher à quoi que ce soit. « Coupé à la source » et « je veux plus de basses » sont deux affirmations différentes. Une seule des deux est un fait.
import numpy as np, soundfile as sf
from scipy.signal import butter, sosfiltfilt
def sub_band_db(path, hi=40.0):
x, sr = sf.read(path, always_2d=True)
mono = x.mean(axis=1)
full_rms = np.sqrt(np.mean(mono**2)) + 1e-12
# Energy that lives strictly below 40 Hz, the octave the subs want.
sos = butter(8, hi, btype="low", fs=sr, output="sos")
sub = sosfiltfilt(sos, mono)
sub_rms = np.sqrt(np.mean(sub**2)) + 1e-12
# Ratio of sub-band energy to total, in dB. Very negative = no bass there.
return 20.0 * np.log10(sub_rms / full_rms)Les pistes qui revenaient bien dans les négatifs étaient de vraies candidates. Celles qui avaient déjà une énergie substantielle dans la bande sous-grave ne l'étaient pas. Il y en avait une dont j'étais sûr qu'elle avait besoin d'aide. Elle mesurait correctement. Le mesureur disait de la laisser tranquille, alors je l'ai laissée tranquille. Aucun traitement. Ce retour en arrière n'est pas une note de bas de page, c'est ce qui fait du procédé un procédé plutôt qu'une préférence déguisée en sarrau de laboratoire.
La discipline en une ligne
Un rehaussement de graves sur une piste qui manque de graves la restaure. Le même rehaussement sur une piste qui a déjà des graves ne fait qu'écrêter et gronder. Tu ne peux pas savoir dans quel cas tu te trouves en écoutant à fort volume. Tu peux le savoir en mesurant d'abord la bande sous-grave.
Synthétiser l'octave manquante
Une fois le déficit prouvé, la question est de savoir comment mettre de l'énergie dans une octave qui n'a jamais été enregistrée. Tu ne peux pas égaliser à la hausse ce qui n'est pas là. Rien sous 40 Hz à amplifier. Il faut la générer à partir de ce que la piste possède, c'est-à-dire le grave une octave plus haut.
Quatre étapes. Isoler la bande de graves pour ne travailler qu'avec les basses fréquences. La descendre d'une octave avec un algorithme qui préserve la durée, pour que le nouveau contenu atterrisse une octave plus bas sans changer la longueur ni le tempo de la chanson. Filtrer le résultat en passe-bas sous environ 75 Hz pour ne garder que le nouveau contenu sous-grave et pas un bas-médium doublé et boueux. Puis le porter, pour que le sous-grave synthétisé ne parle que sur les vraies notes de basse et reste silencieux sur le bruit, le souffle et les fuites entre les notes. Le remixer sous l'original à un niveau que la mesure soutient.
Le décalage de hauteur est ce qui compte le plus. Le filtre rubberband de ffmpeg le fait tout en préservant la durée exactement.
# 1. Isolate the bass band, 2. drop it an octave (duration preserved),
# 3. keep only true sub content, 4. gate so it speaks on notes not noise.
ffmpeg -i in.flac -af "
lowpass=f=180,
rubberband=pitch=0.5,
lowpass=f=75,
agate=threshold=0.02:ratio=4:attack=15:release=120
" sub_layer.flac
# Blend the synthesized octave UNDER the original at a measured level.
ffmpeg -i in.flac -i sub_layer.flac \
-filter_complex "[1:a]volume=-7dB[s];[0:a][s]amix=inputs=2:weights=1 0.7" \
out.flacLe portier n'est pas optionnel. Sans lui, la couche descendue en hauteur reproduit tout ce qu'il y a dans la bande de graves, y compris les espaces entre les notes, ce qui sur un transducteur de 15 pouces se transforme en un grondement bas et constant qui barbouille toute la piste. Avec le portier réglé pour s'ouvrir seulement sur de vraies attaques de note, la couche sous-grave se comporte comme un instrument qui joue avec la ligne de basse au lieu d'un brouillard installé en dessous.
Le bogue d'ordre de chaîne, ou pourquoi le DSP n'est pas commutatif
C'est mon genre de bogue préféré, parce que le symptôme ment sur la cause.
J'ai construit la synthèse, mesuré un déficit net, mélangé la nouvelle octave. Le rehaussement n'était pas dans la sortie. Relancé. Toujours disparu. La mesure de bande sous-grave sur le fichier rendu avait l'air presque identique à l'entrée non traitée, comme si l'étape de synthèse n'avait rien fait. Chaque étape individuelle marchait bien isolément. Assemblées dans la chaîne complète, les graves s'évaporaient.
La cause, c'était l'ordre. La synthèse de graves tournait avant un compresseur de liaison RMS placé plus loin dans la chaîne de matriçage. Le compresseur faisait exactement son travail : il voyait la nouvelle énergie basse fréquence, décidait que la piste était devenue plus forte et la repoussait vers le bas. Je générais du sous-grave et je l'envoyais directement dans une étape dont le but entier est de retirer l'énergie qui dépasse. Elle écrasait précisément ce que je venais d'ajouter.
BROKEN: isolate -> pitch -> gate -> blend -> [RMS glue comp] -> limiter
^ boost added here, then eaten here ^
FIXED: isolate -> pitch -> gate -> [RMS glue comp] -> blend -> limiter
^ boost survives ^Déplacer le mélange après le compresseur de liaison a tout réglé. Le compresseur fixe la dynamique de la piste originale, et le sous-grave synthétisé est posé dessous ensuite, là où rien en aval ne tente de l'aplatir. Le DSP n'est pas commutatif : les mêmes étapes dans un ordre différent forment un processeur différent, et le mesureur est la seule chose qui te dit quel ordre tu as réellement construit.
Investigation de la sonie
Les matriçages sources n'étaient pas cohérents entre eux. Environ 8 dB d'écart en sonie intégrée mesurée selon EBU R128, ce qui veut dire qu'à bouton de volume fixe certaines pistes arrivent timides et d'autres arrivent en criant. Pire, plusieurs écrêtaient au niveau inter-échantillon, des crêtes réelles autour de +2,29 dBTP. Crêtes d'échantillon sous 0 dBFS, crêtes réelles bien au-dessus. C'est la signature classique d'un matriçage poussé dans un limiteur et jamais vérifié sur un mesureur de crête réelle.
J'ai mesuré les deux, LUFS intégrée et crête réelle, sur chaque piste avant de décider quoi que ce soit.
import subprocess, json, re
def measure(path):
# ffmpeg's loudnorm in measurement mode reports R128 + true peak.
cmd = ["ffmpeg", "-i", path, "-af",
"loudnorm=I=-14:TP=-1.0:LRA=11:print_format=json",
"-f", "null", "-"]
out = subprocess.run(cmd, capture_output=True, text=True).stderr
blob = re.search(r"\{.*\}", out, re.S).group(0)
m = json.loads(blob)
return float(m["input_i"]), float(m["input_tp"]) # LUFS, dBTPPlutôt que d'imposer une seule cible de sonie à tout, j'ai rendu trois ensembles pour trois pièces.
L'archive garde toute la plage dynamique et le grave synthétisé, aucune normalisation de sonie, parce que c'est la copie maîtresse dont tout le reste descend. L'ensemble souper est normalisé près de -14 LUFS pour rester sous la conversation sans lui faire concurrence. L'ensemble danse tourne plus chaud à -11 LUFS pour quand la pièce est engagée. Chaque ensemble a été limité en crête réelle pour garder les dépassements inter-échantillon loin du DAC, et la réduction finale de profondeur de bits a utilisé un tramage à mise en forme de bruit shibata, pour que le bruit de quantification soit poussé vers le haut, là où l'oreille est la moins sensible, au lieu de rester plat sur toute la bande.
Trois cibles, une source mesurée, aucune devinette sur celle qu'une soirée donnée appelle.
La mesure, pas les oreilles
J'ai mis une octave de graves dans des chansons enregistrées avant que personne ne puisse l'entendre, et laissé telles quelles celles qui n'en avaient pas besoin. La différence entre ces deux résultats, ce n'étaient pas mes oreilles. C'était la mesure de bande sous-grave qui me disait dans quel cas j'étais, l'ordre de chaîne qui décidait si le rehaussement survivait, et le mesureur de sonie qui était en désaccord avec ce que je voulais assez souvent pour me garder honnête.