Commit 6099df85 by PLN (Algolia)

feat(opal26): every boundary settled — segments_v4 generated, 14 tracks

PLN's last call: "00:31:3 is a decent end of crimewave to cut early on end of
the crime sound". Read off the transport, which shows CLIP time, and #14's clip
starts at 4135.0 — so master 4166.3. The audio agrees rather than merely
permitting it: the crime sound gaps to -71 dB at 4164.5 and 4166.3 sits just
past that on its decay, 13.7 s before the nominal 4180.0. Recorded with that
mapping written out, because clip-vs-master is the one confusion that would
shift a cut by 45 s and look plausible.

apply_boundaries.py turns the ear file into segments_v4.json. Made a tool rather
than a hand edit because this is the step that shipped a wrong album once, and
its three rules are each silently destructive to get wrong:

  - starts come from the ear, ends from the neighbour (butt-joined), so editing
    one start moves two tracks;
  - a track CUT from the release still owns its boundary — Desire is dropped but
    the edge into Desire is where Vague de CRIME ends, and ignoring it would run
    the last official track into Desire's intro;
  - release position is not track number: dropping a track renumbers the album
    but must not renumber the provenance, so both `n` and `track` are emitted.

It refuses to write on overlap, non-positive duration, out-of-master range, or
an ear-verified start that failed to reach the output. All green:
14 tracks, 73.4 min kept of a 79.8 min master, 6.4 min dropped (Desire).

Remaining unverified edge, for the record: #2's start (531.35) is still the
nominal, so #1's end rides on it. It was never flagged in the judge pass.

Ear feedback archived in performance_notes.md, including the finding that #11
and #12 disagree about which fraction is right — takeover on one, mid on the
other, 8 s apart. Two adjacent cuts, opposite answers: where a cut belongs is a
musical judgement about that handover, not a parameter to fit.
parent af60c81a
#!/usr/bin/env python3
"""apply_boundaries — turn ear-verified cuts into the segments the splitter reads.
This is the step that shipped a wrong album once already, so it is a tool with
assertions rather than a one-off edit. Three rules, and every one of them exists
because getting it wrong is silent:
1. **Starts come from the ear, ends come from the neighbour.** The set is
butt-joined, so a track's end is the NEXT track's start. Editing a start
therefore moves two tracks, which is exactly how a hand edit drifts.
2. **A track cut from the release still owns its boundary.** Desire is dropped,
but the edge into Desire is where Vague de CRIME *ends*. Dropping the track
without honouring its start would run the last official track straight into
Desire's intro. The cut track's verified start becomes the previous track's
end, and only then is it removed.
3. **Release position is not track number.** The originals are numbered by the
PERFORMANCE order; removing one track renumbers the album but must not
renumber the provenance. Both are emitted: `track` (as performed, the key
everything else joins on) and `n` (position on the release).
Validated on the way out: no overlaps, no negative durations, every segment
inside the master, and every ear-verified start actually present in the output.
python3 apply_boundaries.py judge_specs/opal26.json \
--ear judge_specs/opal26_boundaries_ear.json \
--out /home/pln/Work/Sound/Prod/Opal26_master/segments_v4.json
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
HERE = Path(__file__).parent
sys.path.insert(0, str(HERE))
from build_judge_set import ffprobe_dur # noqa: E402
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("spec")
ap.add_argument("--ear", required=True)
ap.add_argument("--out", required=True)
a = ap.parse_args()
spec = json.loads(Path(a.spec).read_text())
ear = json.loads(Path(a.ear).read_text())
segs = json.loads(Path(spec["segments"]).read_text())
verified = {int(k): v for k, v in ear.get("verified", {}).items()}
cut = {int(k) for k in ear.get("cut_from_release", {})}
mdur = ffprobe_dur(Path(spec["master"]))
# 1 — apply every ear-verified start, keeping the nominal where none exists
starts: dict[int, float] = {}
for s in segs:
t = s["track"]
v = verified.get(t, {})
starts[t] = float(v["start"]) if v.get("start") is not None else float(s["start"])
order = sorted(starts)
rows, n = [], 0
for i, t in enumerate(order):
if t in cut:
continue # rule 2: its start was already used below
nxt = order[i + 1] if i + 1 < len(order) else None
end = starts[nxt] if nxt is not None else mdur # incl. a cut track's start
n += 1
src = next(s for s in segs if s["track"] == t)
rows.append({
"n": n, # rule 3: release position
"track": t, # ...and performance order, the join key
"title": src["title"],
"start": round(starts[t], 3),
"end": round(end, 3),
"duration": round(end - starts[t], 3),
"bpm": src.get("bpm"),
"source": "ear" if verified.get(t, {}).get("start") is not None else "nominal",
})
# 2 — assertions. Each one has cost us a re-render at some point.
problems = []
for r in rows:
if r["duration"] <= 0:
problems.append(f"#{r['track']} {r['title']}: non-positive duration {r['duration']}")
if r["start"] < 0 or r["end"] > mdur + 0.01:
problems.append(f"#{r['track']}: [{r['start']}, {r['end']}] outside master (0, {mdur:.2f})")
for x, y in zip(rows, rows[1:]):
if y["start"] < x["end"] - 0.001:
problems.append(f"overlap: #{x['track']} ends {x['end']} but #{y['track']} starts {y['start']}")
for t, v in verified.items():
if v.get("start") is None or t in cut:
continue
if not any(abs(r["start"] - float(v["start"])) < 0.001 for r in rows):
problems.append(f"ear-verified #{t} start {v['start']} did not reach the output")
if problems:
print("REFUSING TO WRITE — " + f"{len(problems)} problem(s):")
for p in problems:
print(" ✗ " + p)
return 1
doc = {
"_comment": [
"Generated by apply_boundaries.py from the ear-verified boundaries.",
"Do not hand-edit: edit judge_specs/<gig>_boundaries_ear.json and re-run.",
"`track` is performance order (the join key everywhere else);",
"`n` is position on the release, which differs when a track is cut.",
],
"gig": spec["gig"],
"master": spec["master"],
"master_duration": round(mdur, 3),
"cut_from_release": sorted(cut),
"segments": rows,
}
Path(a.out).write_text(json.dumps(doc, indent=1) + "\n")
total = sum(r["duration"] for r in rows)
print(f"{'n':>2} {'#':>3} {'title':<32} {'start':>9} {'end':>9} {'dur':>8} src")
for r in rows:
print(f"{r['n']:>2} {r['track']:>3} {r['title'][:32]:<32} "
f"{r['start']:>9.2f} {r['end']:>9.2f} {r['duration']:>8.2f} {r['source']}")
print(f"\n{len(rows)} tracks, {total/60:.1f} min kept of {mdur/60:.1f} min master "
f"({(mdur-total)/60:.1f} min dropped)")
print(f"✓ {a.out}")
return 0
if __name__ == "__main__":
sys.exit(main())
...@@ -71,6 +71,11 @@ ...@@ -71,6 +71,11 @@
"15": { "15": {
"start": 4547.57, "start": 4547.57,
"note": "ear call via boundary lab 2026-08-16 (source: playhead)" "note": "ear call via boundary lab 2026-08-16 (source: playhead)"
},
"14": {
"start": 4166.3,
"note": "PLN 2026-08-16 in the boundary lab: '00:31:3 is a decent end of crimewave to cut early on end of the crime sound'. Read off the transport, which shows CLIP time; #14's clip starts at 4135.0, so 31.3 -> master 4166.3. This is the END of #13 Vague de CRIME, not a track start: Desire is cut from the release. Audio agrees: the crime sound gaps to -71 dB at 4164.5, and 4166.3 sits just past that on its decay, 13.7 s earlier than the nominal 4180.0.",
"is_end_of": 13
} }
}, },
"unjudged": [], "unjudged": [],
......
...@@ -541,3 +541,28 @@ set, and a during-gig HUD that warns when the floor has been gone too long — ...@@ -541,3 +541,28 @@ set, and a during-gig HUD that warns when the floor has been gone too long —
Unit note for whoever builds it: the set runs **89–166 BPM**, so a Unit note for whoever builds it: the set runs **89–166 BPM**, so a
seconds-based budget is wrong by up to 2x. Count BARS. seconds-based budget is wrong by up to 2x. Count BARS.
## OPAL-26 · boundary lab session — 2026-08-16
Ear calls on the five open cuts, made in the boundary lab (bounds.html). Times
are absolute MASTER seconds.
| cut | call | PLN's words | vs machine |
|---|---|---|---|
| #11 Ghosts in the Toilets | 3557.33 | *"takeover is the right cut time here."* | takeover +0.13 s |
| #12 Mafia | 3680.10 | *"here the right cut is mid :)"* | takeover −8.00 s |
| #13 Vague de CRIME | 3887.25 | — | ear-only, no candidate possible |
| #14 (= END of #13) | 4166.30 | *"00:31:3 is a decent end of crimewave to cut early on end of the crime sound"* | ear-only |
| #15 REVOLUTION | 4547.57 | — | ear-only, no candidate possible |
**The reaction worth keeping:** on #11 the machine's `takeover` was right to
0.13 s; on #12 the same estimator was 8 s late and PLN picked `mid` instead.
Two adjacent cuts, opposite answers. There is no universal fraction-through-the-
crossfade — where a cut belongs is a musical judgement about that particular
handover, not a parameter. Offer candidates, never average them.
**On #14** he is cutting the last official track EARLY, on the decay rather than
after it: the crime sound gaps to −71 dB at 4164.5 and he takes 4166.3, 13.7 s
before the nominal 4180.0. Preference to note for future ends — he'd rather stop
on the tail of the sound than let the bar run out.
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