Commit db6529dc by PLN (Algolia)

fix(postprod): drop the apad — it was ending the record in 2.3s of digital silence

The reverb tail shipped with `apad=pad_dur=2.5` on the reasoning that ffmpeg
would otherwise chop the echo at the last sample. That reasoning was wrong, and
the verify stage is what exposed it: REVOLUTION came out 207.64s against a
predicted 207.40, and chasing the 0.24s led to the actual behaviour.

Measured, four ways:

    no apad   -> 5.240s wet, 0.000s trailing silence   (full ring, nothing cut)
    apad=0.05 -> 5.290s wet, 0.050s trailing silence
    apad=0.35 -> 5.590s wet, 0.350s trailing silence
    apad=2.5  -> 7.740s wet, 2.500s trailing silence

aecho extends its OWN output by its longest tap (240ms for subtle), so the ring
was never at risk. Every second of apad simply became trailing digital silence,
one for one. The rendered record therefore ended with ~2.3s of pure zeros —
which is precisely the defect PLN flagged at the other end of the record
("trim leading silence tho"), reintroduced by me at the close.

Two things worth keeping from how this surfaced. The 0.24s mismatch was real
information, and the tempting fix — `atrim` the output back to the predicted
length — would have clipped the end of the ring while making the check go green:
the prediction was wrong, not the audio. And the fix only became findable
because verify compares against a number derived from intent rather than from
whatever the renderer happened to produce.

expected_dur is now duration + aecho's longest tap, exactly. REVOLUTION: 205.14s.
parent 10829fde
......@@ -45,7 +45,7 @@ from build_judge_set import clean_title, ffprobe_dur, peaks # noqa: E402
# is allowed to ring past the end of the record rather than being cut off, which
# is the difference between an echo and a truncation. Everything before the tail
# stays bit-identical to the dry render.
from render_release import CLOSE_AB, RING # noqa: E402 — one table, not two
from render_release import CLOSE_AB # noqa: E402 — one table, not two
TAIL = 20.0 # seconds of the outgoing track
HEAD = 20.0 # seconds of the incoming track
......@@ -67,16 +67,15 @@ def close_variant(src: Path, out: Path, ss: float, dur: float,
Split / process / concat rather than one global filter: a reverb applied to
the whole clip would wash the entire ending, and PLN asked for the tail. The
dry head is passed through untouched, the last `wet_s` seconds get the echo,
and `apad` gives the ring somewhere to go — without it the tail is chopped at
the last sample and the "echo as last sound" becomes another hard cut.
and the echo extends the output by its own longest tap — no `apad`, which
measurement showed becomes trailing digital silence one second per second.
"""
if echo is None:
return cut(src, out, ss, dur)
split = max(0.0, dur - wet_s)
fc = (
f"[0:a]atrim=0:{split:.4f},asetpts=N/SR/TB[dry];"
f"[0:a]atrim={split:.4f},asetpts=N/SR/TB,apad=pad_dur={RING},"
f"aecho={echo}[wet];"
f"[0:a]atrim={split:.4f},asetpts=N/SR/TB,aecho={echo}[wet];"
f"[dry][wet]concat=n=2:v=0:a=1[out]"
)
r = subprocess.run(
......@@ -195,8 +194,8 @@ def main() -> int:
"note": ("Last sound of the album, dry — what ships today."
if echo is None else
f"A/B option '{name}': the last {a.wet:.0f}s get an echo that "
f"rings {RING}s past the final sample. Everything before the "
f"tail is identical to DRY."),
f"rings past the final sample. Everything before the tail is "
f"identical to DRY."),
})
doc = {"gig": f"{spec['gig']}-release", "title": f"{spec.get('title','')} — release check",
......
......@@ -56,7 +56,15 @@ CLOSE_AB = {
"subtle": "0.8:0.9:60|140|240:0.28|0.18|0.10",
"more": "0.8:0.9:120|260|420|650:0.38|0.28|0.20|0.13",
}
RING = 2.5 # seconds the reverb may ring past the last sample
# NO apad. The first version padded 2.5s "so the ring has somewhere to go",
# reasoning that ffmpeg would otherwise chop the echo at the last sample. That
# reasoning was wrong and the measurement settles it: aecho extends its own
# output by its longest tap, and every second of apad becomes trailing DIGITAL
# SILENCE one-for-one.
# no apad -> 5.240s wet, 0.000s trailing silence (full ring, nothing cut)
# apad=2.5 -> 7.740s wet, 2.500s trailing silence
# Shipped once: the record ended with ~2.3s of pure zeros — exactly the defect
# PLN flagged at the START of the record ("trim leading silence tho").
def sh(cmd, **kw):
......@@ -107,14 +115,30 @@ def reverb_complex(seg: dict) -> str:
chain = f"[0:a]{pre + ',' if pre else ''}asplit=2[p][q];"
return (chain +
f"[p]atrim=0:{split:.4f},asetpts=N/SR/TB[dry];"
f"[q]atrim={split:.4f},asetpts=N/SR/TB,apad=pad_dur={RING},"
f"[q]atrim={split:.4f},asetpts=N/SR/TB,"
f"aecho={CLOSE_AB[seg['reverb_preset']]}[wet];"
f"[dry][wet]concat=n=2:v=0:a=1[out]")
def echo_extension(preset: str) -> float:
"""How much longer `aecho` makes its input: its LONGEST delay tap.
Missed on the first render and the verify stage caught it — REVOLUTION came
out 207.64s against an expected 207.40, exactly the subtle preset's 240ms
top tap. Worth stating plainly because the temptation was to `atrim` the
output back to the predicted length, which would have silently clipped the
end of the very ring the reverb exists to produce. The prediction was wrong,
not the audio.
"""
delays = CLOSE_AB[preset].split(":")[2]
return max(float(d) for d in delays.split("|")) / 1000.0
def expected_dur(seg: dict) -> float:
"""A reverb tail makes the track LONGER by its ring — not a mismatch."""
return (seg["end"] - seg["start"]) + (RING if has_reverb(seg) else 0.0)
"""A reverb tail makes the track longer by exactly aecho's longest tap."""
if not has_reverb(seg):
return seg["end"] - seg["start"]
return (seg["end"] - seg["start"]) + echo_extension(seg["reverb_preset"])
def sanitize(name: str) -> str:
......
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