Commit 0a123d3b by PLN (Algolia)

feat(armada): compare_masters — put an A/B in numbers, so the ear judges taste and not arithmetic

PLN has to choose between two masters that differ only in a high-shelf setting.
The ear is the authority on which one is better and this does not try to replace
it — but "which do you prefer" is a much easier question when you also know WHAT
differs and by how much, because a shelf change does not stay in the band it
names. It moves true peak, it changes how hard the limiter works, and it shifts
where the loudness normaliser lands.

So: integrated loudness, true peak, loudness range, L/R correlation and the
band-energy distribution for each file, then a delta table against the first,
because two absolute tables side by side is exactly how you miss a change that
matters. Band deltas are reported as RATIOS rather than differences in
percentage points: a band going from 0.12% to 0.24% is a doubling, and "+0.12
points" hides that entirely.

Measured on the RENDERED files, never predicted from the filter settings. A shelf
set to +3 dB does not necessarily put +3 dB in the master, since the limiter
downstream has opinions — feedback_verify_own_renders, the machine's job is the
objective facts so PLN's ears are spent on taste.

The smoke test proved the point immediately. Comparing the trial streaming master
against the harder-pushed club one, the loud version shows 0.92x the 60-120 Hz
energy and 1.08x the 300-1000 Hz: that is the limiter eating bass transients and
relatively lifting the mids, at a cost of 1.4 LU of range for 3.5 dB of loudness.
A measurable signature of what loudness costs, rather than a vague sense that the
loud one sounds smaller.

Wired into the finisher, so the summary PLN reads over coffee has the two masters
with their differences quantified next to the files themselves.
parent 19957cf5
#!/usr/bin/env python3
"""compare_masters — put two or more finished masters side by side, in numbers.
Built for an A/B that PLN has to judge by ear. The ear is the authority and this
does not try to replace it — but "which one do you prefer" is a much easier
question when you also know WHAT differs and by how much. A tilt of a few dB in a
shelf changes more than the band it names: it moves true peak, it changes how hard
the limiter works, and it shifts where the loudness normaliser lands.
So this reports, per file: integrated loudness, true peak, loudness range, and the
band-energy distribution — then a delta table against the first file, because the
differences are the point and eyeballing two absolute tables is how you miss a
0.3% change that matters.
Deliberately measured on the RENDERED files rather than predicted from the filter
settings. `feedback_verify_own_renders`: the machine's job is to catch objective
facts so PLN's ears are spent on taste. A shelf set to +3 dB does not necessarily
put +3 dB in the master — the limiter downstream has opinions.
"""
from __future__ import annotations
import argparse
import re
import subprocess
import sys
from pathlib import Path
import numpy as np
import soundfile as sf
BANDS = [(20, 60), (60, 120), (120, 300), (300, 1000),
(1000, 4000), (4000, 8000), (8000, 16000), (16000, 22050)]
def ebur128(path: Path) -> dict:
p = subprocess.run(["ffmpeg", "-nostdin", "-hide_banner", "-i", str(path),
"-af", "ebur128=peak=true", "-f", "null", "-"],
capture_output=True, text=True)
tail = p.stderr[-2000:]
def grab(label):
m = re.search(rf"{label}:\s*(-?[\d.]+)", tail)
return float(m.group(1)) if m else None
return {"I": grab("I"), "LRA": grab("LRA"), "peak": grab("Peak")}
def bands(path: Path) -> tuple[dict, float]:
"""Band shares over the whole file, streamed — these are hour-long masters."""
n = 1 << 15
win = np.hanning(n)
acc = np.zeros(len(BANDS))
corr_num = corr_l = corr_r = 0.0
with sf.SoundFile(str(path)) as f:
sr = f.samplerate
fr = np.fft.rfftfreq(n, 1 / sr)
idx = [(fr >= lo) & (fr < hi) for lo, hi in BANDS]
while True:
blk = f.read(n, dtype="float32", always_2d=True)
if blk.shape[0] < n:
break
L, R = blk[:, 0], blk[:, 1]
corr_num += float((L * R).sum())
corr_l += float((L * L).sum())
corr_r += float((R * R).sum())
S = np.abs(np.fft.rfft(blk.mean(axis=1) * win)) ** 2
for j, m in enumerate(idx):
acc[j] += float(S[m].sum())
tot = acc.sum() or 1.0
denom = (corr_l * corr_r) ** 0.5
return ({f"{lo}-{hi}": 100 * v / tot for (lo, hi), v in zip(BANDS, acc)},
corr_num / denom if denom > 0 else 0.0)
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("files", nargs="+")
ap.add_argument("--label", action="append", default=[],
help="label per file, in order (default: the filename)")
a = ap.parse_args()
paths = [Path(f) for f in a.files]
for p in paths:
if not p.exists():
print(f"MISSING {p}", file=sys.stderr)
return 1
labels = [a.label[i] if i < len(a.label) else paths[i].stem
for i in range(len(paths))]
rows = []
for p in paths:
lo = ebur128(p)
bs, corr = bands(p)
rows.append({"loud": lo, "bands": bs, "corr": corr,
"size_mb": p.stat().st_size / 1e6})
w = 13
print(f"{'':<14}" + "".join(f"{l[:w]:>{w}}" for l in labels))
print("-" * (14 + w * len(labels)))
for key, fmt in (("I", "{:.1f} LUFS"), ("peak", "{:.1f} dBTP"), ("LRA", "{:.1f} LU")):
print(f"{key:<14}" + "".join(
f"{(fmt.format(r['loud'][key]) if r['loud'][key] is not None else '—'):>{w}}"
for r in rows))
print(f"{'L/R corr':<14}" + "".join(f"{r['corr']:>{w}.3f}" for r in rows))
print(f"{'size':<14}" + "".join(f"{r['size_mb']:>{w-3}.0f} MB" for r in rows))
print(f"\nband energy share (%)")
print(f"{'':<14}" + "".join(f"{l[:w]:>{w}}" for l in labels))
for band in rows[0]["bands"]:
print(f"{band:<14}" + "".join(f"{r['bands'][band]:>{w}.3f}" for r in rows))
if len(rows) > 1:
print(f"\nDELTA vs {labels[0]} — the differences are the point")
print(f"{'':<14}" + "".join(f"{l[:w]:>{w}}" for l in labels[1:]))
base = rows[0]
for key in ("I", "peak", "LRA"):
if base["loud"][key] is None:
continue
print(f"{key:<14}" + "".join(
f"{r['loud'][key] - base['loud'][key]:>+{w}.2f}" for r in rows[1:]))
for band in base["bands"]:
b0 = base["bands"][band]
cells = []
for r in rows[1:]:
d = r["bands"][band]
# ratio, not difference: a band at 0.12% going to 0.24% is a
# doubling, and "+0.12 points" hides that completely.
cells.append(f"{d / b0:>{w}.2f}x" if b0 > 1e-9 else f"{'—':>{w}}")
print(f"{band:<14}" + "".join(cells))
return 0
if __name__ == "__main__":
sys.exit(main())
......@@ -80,6 +80,18 @@ for pair in "+5:$ROOT/Cosmic26_v1_streaming.flac" "+8:$ROOT/ab_air8/streaming.fl
fi
done
# Numbers beside the A/B, because "which do you prefer" is an easier question
# when you also know what differs. Measured on the RENDERED files: a shelf set to
# +3 dB does not necessarily put +3 dB in the master, since the limiter
# downstream has opinions.
if [ -f "$ROOT/Cosmic26_v1_streaming.flac" ] && [ -f "$ROOT/ab_air8/streaming.flac" ]; then
echo
echo "--- A/B measured side by side ---"
"$PY" "$TT/compare_masters.py" \
"$ROOT/Cosmic26_v1_streaming.flac" "$ROOT/ab_air8/streaming.flac" \
--label "air +5" --label "air +8"
fi
echo
echo "--- what is on disk now ---"
for v in streaming; do
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment