Commit 6411504d by PLN (Algolia)

feat(cosmicfest): the EDA answered — the missing air is CYMBALS, so render an A/B

The stem EDA ran on all 14 sections and settled the one parameter that was an
admitted guess. The air shelf sat at +5 dB against a measured 8-16 kHz share of
0.12%, held back because a 320 kbps MP3 puts codec hiss in exactly that band and a
broadband boost cannot tell hiss from hats.

Per stem, mean share of each stem's OWN energy above 8 kHz: drums 0.367%, vocals
0.084%, other 0.033%, bass 0.0%. A 4.4x concentration in drums. Evenly-spread
energy would have meant noise and a shut shelf; concentration means cymbals the
room ate, and opening up is justified by measurement.

HOW FAR is not justified by measurement — that is taste. So --air-gain exists and
the master unit now renders TWO full streaming masters, identical but for the
shelf (+5 and +8), because OPAL already taught us that an effect judged on a clip
is not the same effect judged in place. PLN compares two records tomorrow instead
of ratifying a number I happened to like.

The -9 LUFS "club" target is deliberately dropped, and the reasoning is a
correction to my own earlier conflation of two axes. PLN's "one live club mix"
means the CONTINUOUS set as opposed to the album split — a FORM. Our club variant
is a LOUDNESS. Since no CosmicFest track is cut, the continuous mix simply IS the
master file, so both of his forms were already covered and the -9 render was
optional polish that would have missed anyway (the LRA floor tripped at -10.5 on
the trial). That CPU now buys the A/B, which answers a question he actually has.

Q2, unasked for and worth having: kick presence per track, 40-120 Hz active time
against each track's own p95. Mean 43.4%, against the 59% project_floor_problem
measured for OPAL-26 from real orbit stems. Worst real offenders are #2 There's
Something About Drums at 22.8% and #3 Am i Doing it Right at 33.4%. A stage mic in
a room is a plausible cause on its own, so this is a lead rather than a verdict —
but it is the first time this gig's floor has been measurable at all.

Two things the lens caught that no label would have, both vindicating
feedback_label_and_lens applied to the separator's own names. #14's drums stem is
-84.9 dBFS RMS, essentially empty, which confirms PLN's note that the outro is
"pure dubsiren noise and fun" — so its kick percentage was measuring a noise floor
and reading as a real number. kick_activity is now gated on presence and returns
None with a reason instead, because a percentage of nothing is not a percentage
(presence_is_a_precondition). And #10 PunkAChien's bass stem reads -81.0 dBFS
while its `other` stem is the loudest in the whole table at -26.8: demucs put the
bass INTO other. Anyone reading stem filenames as roles would have concluded
PunkAChien has no bass.

Also fixed the JSON writer, which died on numpy's bool_ after printing every
useful number, and a headline ratio that divided by an epsilon guard when one stem
measured a legitimate 0.000% and announced a 367000x concentration. It now reports
against the runner-up.
parent 9ab93ffb
...@@ -107,7 +107,7 @@ def stem_row(path: Path) -> dict | None: ...@@ -107,7 +107,7 @@ def stem_row(path: Path) -> dict | None:
mono, sr = read(path) mono, sr = read(path)
rms, pk = float(np.sqrt((mono ** 2).mean())), float(np.abs(mono).max()) rms, pk = float(np.sqrt((mono ** 2).mean())), float(np.abs(mono).max())
row = {"rms_dbfs": round(db(rms), 1), "peak_dbfs": round(db(pk), 1), row = {"rms_dbfs": round(db(rms), 1), "peak_dbfs": round(db(pk), 1),
"present": db(rms) > PRESENCE_DBFS, "dur_s": round(len(mono) / sr, 1), "present": bool(db(rms) > PRESENCE_DBFS), "dur_s": round(len(mono) / sr, 1),
"bands": band_shares(mono, sr)} "bands": band_shares(mono, sr)}
row["hf_share_4k_16k"] = round(row["bands"]["4000-8000"] row["hf_share_4k_16k"] = round(row["bands"]["4000-8000"]
+ row["bands"]["8000-16000"], 2) + row["bands"]["8000-16000"], 2)
...@@ -169,8 +169,14 @@ def main() -> int: ...@@ -169,8 +169,14 @@ def main() -> int:
missing.append(f"{name}/{src}") missing.append(f"{name}/{src}")
continue continue
if src == "drums": if src == "drums":
if r["present"]:
mono, sr = read(d / f"{src}.wav") mono, sr = read(d / f"{src}.wav")
r["kick"] = kick_activity(mono, sr) r["kick"] = kick_activity(mono, sr)
else:
# A kick percentage computed on an empty stem measures the
# noise floor and reads as a real number. Refuse it.
r["kick"] = {"active_pct": None, "reason":
f"drums stem absent ({r['rms_dbfs']} dBFS RMS)"}
if not a.no_global: if not a.no_global:
r["vs_global"] = compare_global(d / f"{src}.wav", r["vs_global"] = compare_global(d / f"{src}.wav",
stems / "global" / f"{src}.wav", stems / "global" / f"{src}.wav",
...@@ -189,6 +195,7 @@ def main() -> int: ...@@ -189,6 +195,7 @@ def main() -> int:
v = st.get(src) v = st.get(src)
cells.append(f"{v['rms_dbfs']:>7.1f}" if v else f"{'—':>7}") cells.append(f"{v['rms_dbfs']:>7.1f}" if v else f"{'—':>7}")
kick = st.get("drums", {}).get("kick", {}).get("active_pct") kick = st.get("drums", {}).get("kick", {}).get("active_pct")
kick = kick if kick is not None else "n/a"
hf = st.get("drums", {}).get("hf_share_4k_16k") hf = st.get("drums", {}).get("hf_share_4k_16k")
print(f"{r['track']:>3} {r['title'][:30]:<30} " + " ".join(cells) print(f"{r['track']:>3} {r['title'][:30]:<30} " + " ".join(cells)
+ f" {kick if kick is not None else '—':>6} {hf if hf is not None else '—':>8}") + f" {kick if kick is not None else '—':>6} {hf if hf is not None else '—':>8}")
...@@ -203,15 +210,22 @@ def main() -> int: ...@@ -203,15 +210,22 @@ def main() -> int:
known = {k: v for k, v in agg.items() if v is not None} known = {k: v for k, v in agg.items() if v is not None}
if known: if known:
top = max(known, key=known.get) top = max(known, key=known.get)
spread = max(known.values()) / max(min(known.values()), 1e-6) others = [v for k, v in known.items() if k != top]
print(f" -> concentrated in '{top}' by {spread:.1f}x. " runner = max(others) if others else 0.0
+ ("Cymbals: the air shelf can open up." # Ratio against the RUNNER-UP, not the minimum: one stem legitimately
# measuring 0.000% would otherwise divide by the epsilon guard and print
# a six-figure "spread" that means nothing.
spread = known[top] / runner if runner > 0 else float("inf")
print(f" -> highest in '{top}' ({known[top]}%), runner-up {runner}% "
+ (f"-> {spread:.1f}x" if spread != float("inf") else "-> runner-up is zero"))
print(" -> " + ("CYMBALS: the HF is drum content, so the air shelf can open up."
if top == "drums" and spread >= 2 if top == "drums" and spread >= 2
else "NOT drum-dominated — treat the HF as noise and keep the shelf shut.")) else "NOT drum-dominated — treat the HF as noise, keep the shelf shut."))
print("\n--- Q2 · kick presence per track (40-120 Hz active time) ---") print("\n--- Q2 · kick presence per track (40-120 Hz active time) ---")
ks = [(r["track"], r["title"], r["stems"]["drums"]["kick"]["active_pct"]) ks = [(r["track"], r["title"], r["stems"]["drums"]["kick"]["active_pct"])
for r in report if "drums" in r["stems"] and "kick" in r["stems"]["drums"]] for r in report if "drums" in r["stems"] and "kick" in r["stems"]["drums"]
and r["stems"]["drums"]["kick"].get("active_pct") is not None]
if ks: if ks:
print(f" mean {np.mean([k[2] for k in ks]):.1f}% · " print(f" mean {np.mean([k[2] for k in ks]):.1f}% · "
f"worst: " + ", ".join(f"#{t} {ti[:18]} {p}%" f"worst: " + ", ".join(f"#{t} {ti[:18]} {p}%"
......
...@@ -22,13 +22,18 @@ PA's treble never reaches the mic position, the room absorbs what does, and ...@@ -22,13 +22,18 @@ PA's treble never reaches the mic position, the room absorbs what does, and
mid-bass builds up. So the dominant move is a broadband TILT, not gain — which mid-bass builds up. So the dominant move is a broadband TILT, not gain — which
is why this file exists instead of a `--target-lufs` flag on the stem mixer. is why this file exists instead of a `--target-lufs` flag on the stem mixer.
Restraint where the measurement cannot support enthusiasm: the air shelf is +5, Restraint where the measurement cannot support enthusiasm: the air shelf defaults
not the +12 the deficit suggests. The source is a 320 kbps MP3, so the 8-16 kHz to +5, not the +12 the raw deficit suggests, because a 320 kbps MP3 puts codec
band holds codec residue as well as cymbals, and a big boost there lifts hiss residue in the 8-16 kHz band alongside cymbals and a big boost lifts hiss into the
into the master. Demucs stems (running separately) are the lens that can say how master.
much of that band is drums versus noise; until they do, the shelf stays modest.
`feedback_trim_threshold_ear_correction` — do not let a number talk you into a **The demucs stem EDA has since settled which it is: CYMBALS.** `drums` holds
move the ear has not sanctioned. 0.367% of its own energy above 8 kHz against 0.084% for the runner-up — a 4.4x
concentration, where evenly-spread energy would have meant noise. So opening the
shelf is licensed by measurement. HOW FAR is not: that is taste, so `--air-gain`
exists and the answer comes from an A/B of two full masters rather than from a
number I liked (`feedback_trim_threshold_ear_correction`, and OPAL's lesson that
an effect judged on a clip is not the same effect judged in place).
Bass-mono is done in MID/SIDE, not by splitting and re-summing bands. A Bass-mono is done in MID/SIDE, not by splitting and re-summing bands. A
`lowpass(120)` summed with a `highpass(120)` puts a phase notch at the crossover; `lowpass(120)` summed with a `highpass(120)` puts a phase notch at the crossover;
...@@ -61,19 +66,19 @@ TARGETS = { # MASTERING.md's table; TP -1 dBTP for every platform ...@@ -61,19 +66,19 @@ TARGETS = { # MASTERING.md's table; TP -1 dBTP for every platform
} }
def tone_chain() -> str: def tone_chain(air_g: float = AIR_G) -> str:
"""Everything before loudness. One string so it is identical in both passes.""" """Everything before loudness. One string so it is identical in both passes."""
return ( return (
f"highpass=f={HPF}," f"highpass=f={HPF},"
f"equalizer=f={BOX_F}:t=q:w={BOX_W}:g={BOX_G}," f"equalizer=f={BOX_F}:t=q:w={BOX_W}:g={BOX_G},"
f"equalizer=f={PRES_F}:t=q:w={PRES_W}:g={PRES_G}," f"equalizer=f={PRES_F}:t=q:w={PRES_W}:g={PRES_G},"
f"highshelf=f={AIR_F}:g={AIR_G}," f"highshelf=f={AIR_F}:g={air_g},"
f"lowpass=f={LPF}" f"lowpass=f={LPF}"
) )
def filter_complex(loudnorm: str, pre_gain: float | None = None, def filter_complex(loudnorm: str, pre_gain: float | None = None,
out_rate: int = 44100) -> str: out_rate: int = 44100, air_g: float = AIR_G) -> str:
"""Tone -> M/S bass-mono -> glue -> [staged push] -> loudnorm. """Tone -> M/S bass-mono -> glue -> [staged push] -> loudnorm.
`pre_gain` is what makes the club target reachable, and getting it wrong is `pre_gain` is what makes the club target reachable, and getting it wrong is
...@@ -89,7 +94,7 @@ def filter_complex(loudnorm: str, pre_gain: float | None = None, ...@@ -89,7 +94,7 @@ def filter_complex(loudnorm: str, pre_gain: float | None = None,
handles what survives. One limiter doing all 16 dB pumps audibly. handles what survives. One limiter doing all 16 dB pumps audibly.
""" """
parts = [ parts = [
f"[0:a]{tone_chain()},asplit=2[a][b]", f"[0:a]{tone_chain(air_g)},asplit=2[a][b]",
"[a]pan=mono|c0=0.5*c0+0.5*c1[M]", "[a]pan=mono|c0=0.5*c0+0.5*c1[M]",
f"[b]pan=mono|c0=0.5*c0-0.5*c1,highpass=f={BASS_MONO_F}[S]", f"[b]pan=mono|c0=0.5*c0-0.5*c1,highpass=f={BASS_MONO_F}[S]",
"[M][S]join=inputs=2:channel_layout=stereo[ms]", "[M][S]join=inputs=2:channel_layout=stereo[ms]",
...@@ -119,11 +124,11 @@ def filter_complex(loudnorm: str, pre_gain: float | None = None, ...@@ -119,11 +124,11 @@ def filter_complex(loudnorm: str, pre_gain: float | None = None,
def measure(src: Path, pre_gain: float | None = None, def measure(src: Path, pre_gain: float | None = None,
out_rate: int = 44100) -> dict: out_rate: int = 44100, air_g: float = AIR_G) -> dict:
"""loudnorm pass 1. Two-pass is not optional: pass 2 needs measured_* to """loudnorm pass 1. Two-pass is not optional: pass 2 needs measured_* to
apply ONE fixed offset (linear=true). A single-pass loudnorm rides the gain apply ONE fixed offset (linear=true). A single-pass loudnorm rides the gain
and smears transients — the exact thing -14 LUFS is chosen to preserve.""" and smears transients — the exact thing -14 LUFS is chosen to preserve."""
fc = filter_complex("loudnorm=print_format=json", pre_gain, out_rate) fc = filter_complex("loudnorm=print_format=json", pre_gain, out_rate, air_g)
p = subprocess.run(["ffmpeg", "-nostdin", "-hide_banner", "-i", str(src), p = subprocess.run(["ffmpeg", "-nostdin", "-hide_banner", "-i", str(src),
"-filter_complex", fc, "-map", "[out]", "-f", "null", "-"], "-filter_complex", fc, "-map", "[out]", "-f", "null", "-"],
capture_output=True, text=True) capture_output=True, text=True)
...@@ -135,14 +140,15 @@ def measure(src: Path, pre_gain: float | None = None, ...@@ -135,14 +140,15 @@ def measure(src: Path, pre_gain: float | None = None,
def render(src: Path, dest: Path, target: dict, m: dict, def render(src: Path, dest: Path, target: dict, m: dict,
pre_gain: float | None = None, out_rate: int = 44100) -> None: pre_gain: float | None = None, out_rate: int = 44100,
air_g: float = AIR_G) -> None:
ln = (f"loudnorm=I={target['I']}:TP={target['TP']}:LRA={target['LRA']}:linear=true" ln = (f"loudnorm=I={target['I']}:TP={target['TP']}:LRA={target['LRA']}:linear=true"
f":measured_I={m['input_i']}:measured_TP={m['input_tp']}" f":measured_I={m['input_i']}:measured_TP={m['input_tp']}"
f":measured_LRA={m['input_lra']}:measured_thresh={m['input_thresh']}" f":measured_LRA={m['input_lra']}:measured_thresh={m['input_thresh']}"
f":offset={m['target_offset']}") f":offset={m['target_offset']}")
dest.parent.mkdir(parents=True, exist_ok=True) dest.parent.mkdir(parents=True, exist_ok=True)
cmd = ["ffmpeg", "-nostdin", "-hide_banner", "-v", "error", "-y", "-i", str(src), cmd = ["ffmpeg", "-nostdin", "-hide_banner", "-v", "error", "-y", "-i", str(src),
"-filter_complex", filter_complex(ln, pre_gain, out_rate), "-filter_complex", filter_complex(ln, pre_gain, out_rate, air_g),
"-map", "[out]", "-ar", str(out_rate), "-map", "[out]", "-ar", str(out_rate),
# 24-bit: the source is lossy, but the chain's gain and EQ produce # 24-bit: the source is lossy, but the chain's gain and EQ produce
# values that deserve the headroom, and every downstream split # values that deserve the headroom, and every downstream split
...@@ -182,6 +188,13 @@ def main() -> int: ...@@ -182,6 +188,13 @@ def main() -> int:
help="default: every variant in the spec") help="default: every variant in the spec")
ap.add_argument("--source", help="override the spec's source (for testing on a slice)") ap.add_argument("--source", help="override the spec's source (for testing on a slice)")
ap.add_argument("--out-dir", help="override releaseRoot") ap.add_argument("--out-dir", help="override releaseRoot")
ap.add_argument("--air-gain", type=float, default=AIR_G,
help=f"high-shelf gain at {AIR_F} Hz (default {AIR_G}). The stem EDA "
"settled that the 8-16 kHz deficit is CYMBALS (drums hold 0.367%% "
"of their energy there vs 0.084%% for the runner-up, 4.4x), so "
"opening this up is justified by measurement. HOW FAR is a taste "
"call — render an A/B and let PLN's ears decide, per "
"feedback_trim_threshold_ear_correction.")
a = ap.parse_args() a = ap.parse_args()
spec = json.loads(Path(a.spec).read_text()) spec = json.loads(Path(a.spec).read_text())
...@@ -196,12 +209,12 @@ def main() -> int: ...@@ -196,12 +209,12 @@ def main() -> int:
capture_output=True, text=True).stdout.strip().split("\n")[0]) capture_output=True, text=True).stdout.strip().split("\n")[0])
print(f"source {src.name} ({out_rate} Hz — every render resamples back to this)") print(f"source {src.name} ({out_rate} Hz — every render resamples back to this)")
print(f"tone {tone_chain()}") print(f"tone {tone_chain(a.air_gain)}")
print(f" bass-mono (mid/side) <{BASS_MONO_F} Hz · glue 1.5:1 @ -22 dB\n") print(f" bass-mono (mid/side) <{BASS_MONO_F} Hz · glue 1.5:1 @ -22 dB\n")
report = {} report = {}
# One tone-only measurement serves every variant: the chain up to the push # One tone-only measurement serves every variant: the chain up to the push
# is identical, so measuring it per variant would burn a full pass each. # is identical, so measuring it per variant would burn a full pass each.
base = measure(src, None, out_rate) base = measure(src, None, out_rate, a.air_gain)
print(f"tone-only: I={base['input_i']} TP={base['input_tp']} LRA={base['input_lra']}\n") print(f"tone-only: I={base['input_i']} TP={base['input_tp']} LRA={base['input_lra']}\n")
for v in variants: for v in variants:
...@@ -225,11 +238,11 @@ def main() -> int: ...@@ -225,11 +238,11 @@ def main() -> int:
# better-sounding render rather than clipping our way to a number. # better-sounding render rather than clipping our way to a number.
attempts, got, m, best = [], None, base, None attempts, got, m, best = [], None, base, None
for i in range(3): for i in range(3):
m = measure(src, pre_gain, out_rate) if staged else base m = measure(src, pre_gain, out_rate, a.air_gain) if staged else base
if staged: if staged:
print(f" attempt {i+1}: pre-gain {pre_gain:+.1f} dB -> " print(f" attempt {i+1}: pre-gain {pre_gain:+.1f} dB -> "
f"I={m['input_i']} TP={m['input_tp']} LRA={m['input_lra']}") f"I={m['input_i']} TP={m['input_tp']} LRA={m['input_lra']}")
render(src, dest, t, m, pre_gain, out_rate) render(src, dest, t, m, pre_gain, out_rate, a.air_gain)
got = verify(dest) got = verify(dest)
lra_out = got["LRA"] if got["LRA"] is not None else 0.0 lra_out = got["LRA"] if got["LRA"] is not None else 0.0
hit = got["I"] is not None and abs(got["I"] - t["I"]) <= 0.5 hit = got["I"] is not None and abs(got["I"] - t["I"]) <= 0.5
...@@ -257,7 +270,7 @@ def main() -> int: ...@@ -257,7 +270,7 @@ def main() -> int:
f"{'✓' if ok else '✗ MISSED TARGET'}" f"{'✓' if ok else '✗ MISSED TARGET'}"
+ (f" · dynamics lost {squash:.1f} LU" if squash and squash > 1 else "")) + (f" · dynamics lost {squash:.1f} LU" if squash and squash > 1 else ""))
print(f" {dest}") print(f" {dest}")
report[v] = {"target": t, "measured_tone": base, "measured_in": m, report[v] = {"target": t, "air_gain_db": a.air_gain, "measured_tone": base, "measured_in": m,
"measured_out": got, "in_spec": ok, "staged": staged, "measured_out": got, "in_spec": ok, "staged": staged,
"pre_gain_db": pre_gain, "lra_lost": squash, "pre_gain_db": pre_gain, "lra_lost": squash,
"attempts": attempts, "path": str(dest)} "attempts": attempts, "path": str(dest)}
......
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