Commit 192c6e9e by PLN (Algolia)

feat(bounds): the boundary lab — decide a cut by ear in seconds, not a full pass

The set-judge answers "is this track a keeper". This answers the question that
actually blocked the release: where exactly does one track become the next.
After three failed detectors (cut_lens3 declared a dead end in 5f7db02a), #13 and
#15 are ear-only by nature — the two tracks share their sounds across the switch,
so no timbral lens can see the handover. The tool's job is therefore to make one
ear call cheap, not to avoid asking.

build_bounds_set.py renders a ±45 s FLAC clip per boundary instead of seeking the
817 MB master. A boundary audition is a scrub — dozens of small seeks around one
point — and seeking a long FLAC depends on a seektable the browser may not use
well; local clips make every seek instant for a few MB total. Clips are
re-encoded, never `-c copy`, because FLAC stream-copy snaps to frame boundaries
and every marker inside the clip would inherit that offset. Clip time and master
time are kept explicitly apart (`clipStart`), converted in exactly two functions.

The UI shows each cut's candidates as labelled markers on the waveform, drawn as
regions rather than positioned divs so they stay glued to the audio through zoom
and scroll. `takeover` leads the list because it landed within 0.7 s on both
boundaries that also had ear answers — with the caveat printed on screen, since
that is n=2 on the cases that were already easy. Boundaries with NO candidate are
shown with an explicit notice rather than omitted: those are precisely the ones
needing a human, and hiding them would hide the work.

Fixes a real bug in the SHARED player while here: `ws.zoom()` needs a decoded
buffer, which the peaks path never has, so it threw "No audio loaded" from an
effect and blanked the entire page. Same message as the peaks+url trap, entirely
different cause. Now guarded and failed-soft — zoom is a convenience and must
never take the tool down. The judge had this latent too.

bounds-smoke.mjs, 11 assertions, all passing. Two of them exist because the first
version of this test passed while proving nothing: querySelector('audio') is null
by construction (wavesurfer owns the element and never puts it in the DOM), so
the playback probe would have "passed" forever on a dead transport. It now reads
the transport clock and the Pause button — what a human sees. A positional
selector also silently picked the header's counter instead of the clock, hence
the explicit data-testid.
parent d3dbe59a
......@@ -33,8 +33,13 @@ from a detector.** Nine boundaries verified by ear (#3 893.0 · #4 1118.4 · #5
- [x] **#1 leading-silence measured** — 1.452 s of *literal digital silence* (all samples zero
to sample 64038; first transient −28 dBFS). Objective, no ear needed.
- [ ] **A1.1 Apply the #1 head trim** — ⭐ fully independent of everything else, do it first.
- [ ] **A1.2 Settle #11, #12** — candidates exist (`cut_candidates_v3run2.json`), need one ear call each.
- [x] **A1.1 #1 head trim recorded**`start: 1.45` in the ear-boundary file, with the
sample-exact measurement stored alongside for audit (`d3dbe59`). Kept as *data*, not
baked into the rendered flac, because A1.4 regenerates the tracks from this file.
- [ ] **A1.2 Settle #11, #12** — machine proposals now live in a `candidates` block in the
ear-boundary file, structurally separate from `verified`. Audition **`takeover` first**:
it landed within 0.7 s on both boundaries that also have ear answers (#8, #10) — n=2 and
on the easy cases, so it earns first listen, not trust.
- [ ] **A1.3 Settle #13, #15** — 🚨 **ear-only, no candidates will ever come from the timbral lens** (see B1).
- [ ] **A1.4** Write `segments_v4.json``v4_provenance/finish.sh` → rebuild judge data.
......
#!/usr/bin/env python3
"""build_bounds_set — data for the boundary webview: audition a cut, mark it.
The set-judge answers "is this track a keeper". This answers a different and
much narrower question: **exactly where does one track become the next**. After
three failed automated attempts (see `cut_lens3.py`'s docstring — the timbral
lens is structurally blind on transitions whose two tracks share their sounds),
some boundaries are ear-only by nature, so the tool's job is to make an ear call
cost seconds instead of a full listening pass.
## Why it renders clips instead of seeking the master
The obvious design streams the 817 MB master and seeks. Two problems: seeking
inside a long FLAC depends on a seektable the browser may not use well, and a
boundary audition is a scrub — dozens of small seeks around one point. Rendering
a ±`--pad` second clip per boundary makes every seek local and instant, and the
clips are a few MB total. Same reasoning as the judge's `heads`, and it reuses
that mount pattern.
Clip time and master time are kept explicitly distinct: each boundary carries
`clipStart` (master seconds at clip sample 0) so every marker converts with one
subtraction. Getting this wrong silently shifts every candidate, which is the
same class of bug as the master→stem `keeps` mapping.
## What it emits
Per boundary: the outgoing/incoming titles, the nominal cut, whatever candidates
exist (`takeover` / `mid` / `onset`), any ear-verified answer, waveform peaks,
and the clip URL. Boundaries with NO candidate are included and flagged — #13
and #15 are exactly the ones that need a human, so omitting them would hide the
work.
python3 build_bounds_set.py judge_specs/opal26.json \
--ear judge_specs/opal26_boundaries_ear.json --render
"""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
from pathlib import Path
import numpy as np
HERE = Path(__file__).parent
sys.path.insert(0, str(HERE))
from build_judge_set import clean_title, ffprobe_dur, peaks # noqa: E402
PAD = 45.0 # seconds either side of the nominal cut
PEAKS_N = 1400 # a 90 s clip: ~15 buckets/second, enough to see a fade
def render_clip(master: Path, out: Path, t0: float, dur: float) -> bool:
"""Cut [t0, t0+dur] from the master. Re-encoded, not stream-copied.
`-c copy` on FLAC snaps to frame boundaries, so the clip would start a few
ms from where we asked and every marker inside it would inherit that error.
"""
out.parent.mkdir(parents=True, exist_ok=True)
r = subprocess.run(
["ffmpeg", "-v", "error", "-y", "-ss", f"{t0:.6f}", "-t", f"{dur:.6f}",
"-i", str(master), "-ac", "2", "-c:a", "flac", "-compression_level", "5",
str(out)], capture_output=True)
if r.returncode != 0:
print(f" ! ffmpeg failed for {out.name}: {r.stderr.decode()[:200]}")
return False
return True
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("spec")
ap.add_argument("--ear", required=True, help="ear-verified + candidates JSON")
ap.add_argument("--pad", type=float, default=PAD)
ap.add_argument("--render", action="store_true", help="(re)render the clips")
ap.add_argument("--only", help="comma-separated track numbers")
ap.add_argument("--out")
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())
master = Path(spec["master"])
if not master.exists():
print(f"master not found: {master}")
return 1
clips_dir = Path(spec.get("boundsDir") or master.parent / "bounds_v4")
verified = ear.get("verified", {})
cands = ear.get("candidates", {})
only = {int(x) for x in a.only.split(",")} if a.only else None
mdur = ffprobe_dur(master)
by_track = {s["track"]: s for s in segs}
out_rows = []
for i in range(1, len(segs)):
nxt = segs[i]
tno = nxt["track"]
if only and tno not in only:
continue
nominal = float(nxt["start"])
t0 = max(0.0, nominal - a.pad)
t1 = min(mdur, nominal + a.pad)
clip = clips_dir / f"{tno:02d}.flac"
if a.render or not clip.exists():
print(f" rendering #{tno:>2} {nxt['title'][:28]:<28} "
f"[{t0:.1f}, {t1:.1f}]")
if not render_clip(master, clip, t0, t1 - t0):
continue
v = verified.get(str(tno), {})
c = cands.get(str(tno), {})
marks = []
# order matters: this is the audition order, best-first. `takeover` came
# within 0.7 s on both boundaries that also had ear answers — n=2, on the
# easy cases, so it leads the list but carries its own caveat in the UI.
for key, label in (("takeover", "takeover"), ("mid", "mid"),
("onset", "onset")):
if c.get(key) is not None:
marks.append({"key": key, "label": label, "t": float(c[key])})
if v.get("start") is not None:
marks.append({"key": "ear", "label": "ear-verified", "t": float(v["start"])})
if v.get("alt_start") is not None:
marks.append({"key": "alt", "label": "ear alt", "t": float(v["alt_start"])})
out_rows.append({
"track": tno,
"title": clean_title(nxt["title"]),
"fromTitle": clean_title(by_track.get(tno - 1, {}).get("title", "?")),
"nominal": nominal,
"clipStart": t0,
"clipEnd": t1,
"url": f"{spec.get('boundsAudioBase', '/audio/bounds')}/{clip.name}",
"peaks": [round(x, 4) for x in peaks(clip, PEAKS_N)],
"marks": marks,
"settled": v.get("start") is not None,
"noCandidate": not marks or all(m["key"] in ("ear", "alt") for m in marks),
"note": c.get("note") or v.get("note") or "",
})
doc = {
"gig": spec["gig"],
"title": spec.get("title", ""),
"generated": True,
"pad": a.pad,
"boundaries": out_rows,
}
# default straight into the SPA's public/ — same convention as the judge's
# judge-<gig>.json, so `vite build` and serve.py both pick it up untouched.
dest = Path(a.out or HERE.parent / "ui" / "public" / f"bounds-{spec['gig']}.json")
dest.write_text(json.dumps(doc, indent=1))
n_open = sum(1 for r in out_rows if not r["settled"])
print(f"\n✓ {dest} ({len(out_rows)} boundaries, {n_open} still open)")
print(f" clips: {clips_dir}")
return 0
if __name__ == "__main__":
sys.exit(main())
......@@ -2,17 +2,18 @@
"_comment": [
"Where /audio/<prefix>/... resolves to on disk. Read by vite.config.ts in dev",
"and by armada/serve.py in production, so the SPA's URLs are identical in both",
" a UI that only works under `npm run dev` is a demo, not a tool.",
"\u2014 a UI that only works under `npm run dev` is a demo, not a tool.",
"",
"Masters live outside the repo (they are gigabytes). Mounting them by prefix",
"keeps the URLs stable while the paths move between gigs, and keeps `heads`",
"and `full` apart `master split` names both '01 - Title.flac', so a single",
"and `full` apart \u2014 `master split` names both '01 - Title.flac', so a single",
"flat mount would serve the 98 MB track when the UI asked for the 16 s head."
],
"mounts": {
"heads": "/home/pln/Work/Sound/Prod/Opal26_master/heads_v4",
"bounds": "/home/pln/Work/Sound/Prod/Opal26_master/bounds_v4",
"full": "/home/pln/Work/Sound/Prod/Opal26_master/tracks_v4_streaming",
"full-club": "/home/pln/Work/Sound/Prod/Opal26_master/tracks_v4_club",
"heads": "/home/pln/Work/Sound/Prod/Opal26_master/heads_v4",
"": "/home/pln/Work/Sound/Tidal/armada/tide-table/punkachien"
}
}
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Boundary Lab · L'Armada</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/bounds.tsx"></script>
</body>
</html>
{
"gig": "opal-festival-2026",
"title": "Sunset Forest",
"generated": true,
"pad": 45.0,
"boundaries": [
{
"track": 11,
"title": "Ghosts in the Toilets",
"fromTitle": "Gimme Acid",
"nominal": 3560.2,
"clipStart": 3515.2,
"clipEnd": 3605.2,
"url": "/audio/bounds/11.flac",
"peaks": [
0.8535,
0.792,
0.7545,
0.8972,
0.796,
0.7951,
0.7719,
0.7187,
0.539,
0.8501,
0.6607,
0.8785,
0.8149,
0.8275,
0.8625,
0.8352,
0.7348,
0.7309,
0.7703,
0.798,
0.9027,
0.8008,
0.864,
0.8132,
0.7255,
0.7012,
0.9774,
0.7334,
0.7922,
0.7813,
0.6597,
0.6543,
0.8443,
0.7485,
0.7344,
0.6663,
0.6863,
0.5488,
0.9652,
0.9214,
0.8693,
0.7426,
0.7242,
0.6555,
0.9885,
0.755,
0.7757,
0.822,
0.7458,
0.9449,
0.7112,
0.9593,
0.8793,
0.8598,
0.6981,
0.8998,
0.7128,
0.7595,
0.5682,
0.6688,
0.7372,
0.8728,
0.7574,
0.8435,
0.6932,
0.6625,
0.6894,
0.7857,
0.7514,
0.7679,
0.7953,
0.6026,
0.5088,
0.8237,
0.7032,
0.5558,
0.8028,
0.6833,
0.5642,
0.7212,
0.6303,
0.8256,
0.9127,
0.6909,
0.8008,
0.7686,
0.6418,
0.6737,
0.5884,
0.5629,
0.882,
0.8138,
0.7818,
0.9328,
0.8714,
0.7873,
0.9026,
0.7884,
0.8011,
0.7605,
0.7189,
0.631,
0.9653,
0.7751,
0.7764,
0.6824,
0.805,
0.8196,
0.8287,
0.8615,
0.8231,
0.7702,
0.9131,
0.9067,
0.9736,
0.8183,
0.8884,
0.835,
0.6652,
0.9596,
0.9154,
0.8402,
0.8105,
0.7534,
0.6795,
0.9847,
0.7797,
0.7543,
0.6447,
0.7131,
0.7597,
0.8549,
0.7611,
0.8761,
0.8125,
0.751,
0.8772,
0.8983,
0.959,
0.7846,
0.7414,
0.7811,
0.7706,
0.8876,
0.7545,
0.7812,
0.771,
0.7165,
0.6397,
0.9194,
0.8148,
0.7582,
0.7046,
0.7617,
0.8687,
0.8141,
0.8432,
0.7619,
0.7714,
0.8328,
0.9282,
0.9399,
0.8041,
0.8103,
0.6155,
0.6673,
0.8757,
0.8608,
0.8264,
0.8702,
0.6345,
0.6861,
1.0,
0.799,
0.7498,
0.7166,
0.7034,
0.6686,
0.8077,
0.7925,
0.7918,
0.7843,
0.7761,
0.7735,
0.968,
0.9108,
0.7553,
0.9403,
0.8424,
0.9137,
0.724,
0.8414,
0.7856,
0.6662,
0.7181,
0.9691,
0.7757,
0.775,
0.8066,
0.8247,
0.8408,
0.8314,
0.782,
0.8599,
0.7253,
0.7682,
0.9178,
0.8371,
0.8179,
0.8617,
0.9229,
0.7482,
0.6692,
0.9591,
0.8222,
0.8435,
0.8112,
0.6778,
0.5459,
0.9916,
0.7795,
0.7525,
0.8727,
0.7636,
0.8241,
0.7312,
0.7907,
0.7891,
0.7299,
0.7706,
0.6894,
0.8278,
0.7694,
0.836,
0.8563,
0.852,
0.912,
0.8939,
0.8649,
0.9093,
0.7239,
0.7089,
0.8664,
0.7609,
0.7361,
0.6921,
0.7242,
0.6156,
0.8655,
0.7999,
0.8331,
0.7573,
0.8089,
0.7675,
0.7565,
0.7176,
0.7155,
0.6998,
0.6354,
0.5834,
0.5692,
0.4462,
0.5018,
0.5423,
0.6909,
0.4909,
0.4233,
0.3859,
0.5817,
0.5165,
0.7074,
0.4761,
0.3727,
0.3888,
0.4079,
0.3444,
0.3467,
0.3608,
0.345,
0.3116,
0.3233,
0.2999,
0.2884,
0.2561,
0.4671,
0.4339,
0.343,
0.8217,
0.5921,
0.4025,
0.447,
0.316,
0.336,
0.8662,
0.61,
0.3333,
0.4918,
0.5542,
0.4646,
0.7887,
0.4932,
0.2515,
0.5508,
0.3639,
0.3709,
0.9135,
0.5382,
0.2536,
0.5504,
0.3957,
0.7766,
0.9134,
0.532,
0.4803,
0.4784,
0.3446,
0.8787,
0.7651,
0.3931,
0.482,
0.4335,
0.5619,
0.7454,
0.6061,
0.4336,
0.3423,
0.3069,
0.375,
0.3492,
0.2932,
0.2744,
0.3968,
0.3128,
0.3199,
0.7247,
0.5124,
0.4105,
0.3594,
0.2875,
0.3069,
0.7624,
0.4278,
0.2487,
0.407,
0.4479,
0.4541,
0.4386,
0.4369,
0.3324,
0.2808,
0.4592,
0.481,
0.4668,
0.3082,
0.4231,
0.4309,
0.3422,
0.72,
0.6486,
0.4022,
0.3215,
0.2547,
0.2843,
0.694,
0.5054,
0.2794,
0.3567,
0.3474,
0.3999,
0.3688,
0.3292,
0.3726,
0.2249,
0.3233,
0.3079,
0.237,
0.2553,
0.1969,
0.354,
0.2723,
0.2781,
0.595,
0.3765,
0.2926,
0.2997,
0.2661,
0.6542,
0.5907,
0.3062,
0.3515,
0.321,
0.4241,
0.5158,
0.4754,
0.2719,
0.3895,
0.2931,
0.2972,
0.6376,
0.514,
0.2647,
0.382,
0.2373,
0.2937,
0.6233,
0.4669,
0.3652,
0.3096,
0.2556,
0.2725,
0.6659,
0.4128,
0.2304,
0.3564,
0.5013,
0.4531,
0.5741,
0.4593,
0.3252,
0.2966,
0.3474,
0.3893,
0.3219,
0.2603,
0.343,
0.3502,
0.2785,
0.5808,
0.5421,
0.3417,
0.2826,
0.2257,
0.2599,
0.6227,
0.4766,
0.2625,
0.328,
0.2899,
0.375,
0.3327,
0.3165,
0.3522,
0.2121,
0.3455,
0.3712,
0.3595,
0.2859,
0.1852,
0.3385,
0.2601,
0.2736,
0.5458,
0.3493,
0.2371,
0.2648,
0.2487,
0.5132,
0.5865,
0.345,
0.477,
0.5034,
0.68,
0.4515,
0.4802,
0.6373,
0.5594,
0.5537,
0.8069,
0.6497,
0.5213,
0.2295,
0.3085,
0.2282,
0.2656,
0.5342,
0.4127,
0.3241,
0.2634,
0.23,
0.2476,
0.5855,
0.37,
0.1993,
0.3084,
0.3209,
0.2572,
0.251,
0.2467,
0.3006,
0.3381,
0.2668,
0.5487,
0.5265,
0.2958,
0.3365,
0.3443,
0.2776,
0.5715,
0.5517,
0.3573,
0.2975,
0.2398,
0.2869,
0.6913,
0.5353,
0.2984,
0.3792,
0.3751,
0.3695,
0.3413,
0.3356,
0.4478,
0.3424,
0.3963,
0.3452,
0.3153,
0.3128,
0.2422,
0.4125,
0.3252,
0.3297,
0.6739,
0.4381,
0.2914,
0.3246,
0.3066,
0.6322,
0.7245,
0.3404,
0.3556,
0.3806,
0.3836,
0.2972,
0.2053,
0.3995,
0.2456,
0.2002,
0.4379,
0.4232,
0.3977,
0.2965,
0.4061,
0.2753,
0.3302,
0.6593,
0.505,
0.3946,
0.3195,
0.2806,
0.3023,
0.7132,
0.4507,
0.2505,
0.3777,
0.3793,
0.3229,
0.2272,
0.3913,
0.3518,
0.2503,
0.3685,
0.3327,
0.2834,
0.2977,
0.3979,
0.403,
0.3225,
0.6572,
0.628,
0.3983,
0.3239,
0.261,
0.3061,
0.7251,
0.5584,
0.308,
0.3833,
0.357,
0.3982,
0.3118,
0.2116,
0.3059,
0.418,
0.3143,
0.3289,
0.6737,
0.4748,
0.224,
0.4146,
0.3207,
0.3275,
0.6658,
0.4395,
0.296,
0.3303,
0.3135,
0.6508,
0.7442,
0.3512,
0.3686,
0.3999,
0.3935,
0.3581,
0.309,
0.4718,
0.2999,
0.3022,
0.4081,
0.3782,
0.3081,
0.2959,
0.3854,
0.276,
0.282,
0.5675,
0.4353,
0.3443,
0.2812,
0.2426,
0.2619,
0.5805,
0.358,
0.2013,
0.2962,
0.3003,
0.2517,
0.1716,
0.2734,
0.2128,
0.1626,
0.2958,
0.2883,
0.2889,
0.2041,
0.2737,
0.2823,
0.2302,
0.4624,
0.4376,
0.254,
0.2045,
0.1658,
0.2702,
0.1675,
0.1741,
0.1544,
0.1653,
0.1325,
0.1866,
0.2791,
0.2375,
0.225,
0.2255,
0.2215,
0.2267,
0.2244,
0.2416,
0.2587,
0.2801,
0.3001,
0.3367,
0.3314,
0.3224,
0.2736,
0.2398,
0.2115,
0.2541,
0.2268,
0.2543,
0.2574,
0.2733,
0.2702,
0.2303,
0.2496,
0.2477,
0.2273,
0.2389,
0.2381,
0.2471,
0.2697,
0.3615,
0.4487,
0.4565,
0.4692,
0.4337,
0.3625,
0.3468,
0.3587,
0.3737,
0.4083,
0.4163,
0.4215,
0.3536,
0.2144,
0.1555,
0.1088,
0.0657,
0.0304,
0.014,
0.0089,
0.0074,
0.0088,
0.0167,
0.0204,
0.0439,
0.0599,
0.0835,
0.1126,
0.1212,
0.1382,
0.1563,
0.1741,
0.1913,
0.1838,
0.2082,
0.2523,
0.2885,
0.2895,
0.2691,
0.2869,
0.3032,
0.3079,
0.3009,
0.2996,
0.2953,
0.3005,
0.3231,
0.3302,
0.3437,
0.317,
0.28,
0.3087,
0.3565,
0.3528,
0.3529,
0.3521,
0.3167,
0.3271,
0.3262,
0.3435,
0.2643,
0.2661,
0.2499,
0.227,
0.2263,
0.22,
0.2282,
0.2172,
0.2172,
0.2134,
0.2443,
0.2534,
0.2629,
0.2795,
0.2954,
0.2962,
0.2764,
0.2378,
0.2007,
0.2467,
0.2227,
0.2385,
0.2394,
0.2654,
0.2711,
0.2817,
0.2333,
0.2475,
0.2449,
0.241,
0.2473,
0.2353,
0.2373,
0.2503,
0.2747,
0.2874,
0.3038,
0.2992,
0.2465,
0.2251,
0.2339,
0.2372,
0.2553,
0.2681,
0.283,
0.2938,
0.2903,
0.2718,
0.2674,
0.2559,
0.2646,
0.268,
0.2946,
0.2822,
0.2898,
0.2671,
0.252,
0.2634,
0.2709,
0.2773,
0.2764,
0.2742,
0.2689,
0.2851,
0.2367,
0.2332,
0.2633,
0.2874,
0.3013,
0.3005,
0.2627,
0.2586,
0.2444,
0.2457,
0.247,
0.2461,
0.2488,
0.2465,
0.2784,
0.254,
0.2189,
0.2276,
0.2407,
0.2473,
0.2511,
0.2493,
0.2388,
0.2595,
0.2391,
0.2158,
0.2457,
0.2991,
0.2833,
0.3065,
0.3184,
0.2639,
0.224,
0.2227,
0.2267,
0.2195,
0.2566,
0.2286,
0.2294,
0.2824,
0.2925,
0.2954,
0.2807,
0.3145,
0.3203,
0.3047,
0.3073,
0.2517,
0.2925,
0.2967,
0.3134,
0.3127,
0.3233,
0.3501,
0.3589,
0.2308,
0.2447,
0.2606,
0.2746,
0.3271,
0.2957,
0.3186,
0.353,
0.3437,
0.3321,
0.3261,
0.3304,
0.3141,
0.2826,
0.268,
0.2602,
0.2592,
0.3255,
0.3586,
0.389,
0.3224,
0.3678,
0.2717,
0.1876,
0.1766,
0.1684,
0.1872,
0.2304,
0.2094,
0.2416,
0.2279,
0.1839,
0.1908,
0.2302,
0.1978,
0.1983,
0.2299,
0.1895,
0.1039,
0.1129,
0.1391,
0.1748,
0.196,
0.1812,
0.1784,
0.1413,
0.1148,
0.0357,
0.0398,
0.0821,
0.1288,
0.1913,
0.1695,
0.1861,
0.1609,
0.176,
0.1474,
0.0681,
0.1145,
0.1211,
0.1375,
0.1288,
0.1045,
0.0783,
0.1435,
0.1688,
0.1647,
0.0787,
0.1164,
0.0849,
0.0163,
0.0091,
0.0048,
0.0082,
0.0163,
0.0488,
0.045,
0.0686,
0.1478,
0.1139,
0.1199,
0.11,
0.1169,
0.0695,
0.0447,
0.0328,
0.025,
0.0763,
0.1009,
0.1182,
0.1612,
0.1313,
0.1675,
0.0894,
0.0335,
0.0259,
0.0342,
0.0544,
0.0706,
0.1887,
0.13,
0.167,
0.1466,
0.1353,
0.1326,
0.1475,
0.0939,
0.076,
0.0841,
0.0469,
0.0562,
0.1177,
0.1383,
0.1612,
0.1816,
0.1687,
0.1474,
0.0451,
0.0325,
0.0504,
0.0432,
0.0503,
0.1379,
0.1185,
0.1423,
0.1875,
0.1584,
0.1447,
0.1448,
0.1208,
0.0749,
0.1047,
0.1321,
0.0675,
0.1039,
0.1323,
0.1494,
0.2291,
0.1484,
0.1576,
0.0622,
0.0291,
0.1471,
0.1191,
0.0491,
0.1039,
0.1367,
0.1433,
0.2451,
0.1824,
0.1572,
0.1518,
0.1283,
0.1091,
0.2183,
0.2228,
0.2139,
0.0812,
0.1265,
0.1577,
0.3532,
0.3071,
0.2046,
0.4082,
0.4405,
0.4321,
0.4519,
0.1379,
0.0927,
0.2179,
0.2447,
0.2115,
0.2442,
0.1701,
0.2328,
0.1943,
0.1841,
0.34,
0.4927,
0.5136,
0.428,
0.1775,
0.1177,
0.2969,
0.2014,
0.1705,
0.4127,
0.4256,
0.4044,
0.4165,
0.1312,
0.1132,
0.1932,
0.1736,
0.1511,
0.2369,
0.1631,
0.1282,
0.2172,
0.1768,
0.1417,
0.4508,
0.4787,
0.4175,
0.2485,
0.122,
0.1495,
0.2212,
0.1999,
0.2589,
0.3573,
0.3674,
0.3717,
0.2456,
0.0805,
0.1317,
0.1504,
0.1485,
0.1923,
0.1627,
0.137,
0.1903,
0.1442,
0.1413,
0.4251,
0.4478,
0.4493,
0.34,
0.099,
0.1146,
0.2054,
0.1494,
0.1399,
0.3515,
0.3411,
0.3418,
0.3629,
0.0717,
0.0827,
0.1416,
0.1606,
0.1488,
0.182,
0.1593,
0.1949,
0.1392,
0.1491,
0.3577,
0.4484,
0.4071,
0.3631,
0.1244,
0.0935,
0.3798,
0.3515,
0.4312,
0.3946,
0.3226,
0.3445,
0.3366,
0.1351,
0.0788,
0.1447,
0.1336,
0.1082,
0.1408,
0.1012,
0.0618,
0.131,
0.0717,
0.0548,
0.4103,
0.4337,
0.3874,
0.2496,
0.0767,
0.0876,
0.1935,
0.2386,
0.2372,
0.344,
0.3348,
0.3541,
0.2422,
0.0882,
0.183,
0.1627,
0.1366,
0.1932,
0.1237,
0.0625,
0.1525,
0.1015,
0.0526,
0.4393,
0.3868,
0.4767,
0.383,
0.0741,
0.096,
0.1507,
0.2198,
0.2266,
0.4246,
0.3629,
0.392,
0.3791,
0.1115,
0.1053,
0.1754,
0.1418,
0.1254,
0.2069,
0.1097,
0.1771,
0.1471,
0.1183,
0.3161,
0.4615,
0.4673,
0.4021,
0.1547,
0.0947,
0.1556,
0.1531,
0.2317,
0.3543,
0.3214,
0.3028,
0.2862,
0.1174,
0.086,
0.3273,
0.2768,
0.2521,
0.2468,
0.0801,
0.0693,
0.353,
0.3554,
0.2878,
0.4115,
0.3434,
0.3307,
0.3178,
0.3061,
0.31,
0.367,
0.372,
0.406,
0.3311,
0.3333,
0.3281,
0.2172,
0.11,
0.3079,
0.3747,
0.3511,
0.3625,
0.1623,
0.0629,
0.4412,
0.3993,
0.3811,
0.3498,
0.183,
0.1826,
0.3898,
0.3683,
0.3827,
0.3511,
0.1407,
0.1433,
0.364,
0.3671,
0.3634,
0.3643,
0.0953,
0.1118,
0.4025,
0.3963,
0.3822,
0.3439,
0.0788,
0.1452,
0.4533,
0.3696,
0.3804,
0.2717,
0.1723,
0.368,
0.3766,
0.3878,
0.373,
0.2033,
0.1603,
0.3621,
0.3554,
0.3678,
0.3622,
0.1146,
0.0965,
0.3984,
0.3912,
0.3826,
0.3563,
0.1135,
0.0552,
0.4368,
0.3785,
0.3774,
0.3436,
0.1487,
0.1689,
0.3902,
0.3872,
0.3815,
0.3537,
0.1299,
0.166,
0.3745,
0.3769,
0.3791,
0.2694,
0.0996,
0.4003,
0.3905,
0.3926,
0.3758,
0.1776,
0.0876,
0.4736,
0.386,
0.3884,
0.4434,
0.1471,
0.1576,
0.3923,
0.3574,
0.3858,
0.3499,
0.1864,
0.124,
0.3733,
0.3603,
0.3704,
0.3411,
0.0816,
0.0568,
0.414,
0.3613,
0.383,
0.3505,
0.1005,
0.1878,
0.4472,
0.3798,
0.3688,
0.233,
0.1657,
0.3787,
0.3805,
0.3898,
0.3814,
0.1892,
0.1147,
0.3658,
0.366,
0.3543,
0.3417,
0.0733,
0.0458,
0.1548,
0.1171,
0.1135,
0.111,
0.0871,
0.097,
0.092,
0.0957,
0.0591,
0.1449,
0.1095,
0.0887,
0.1012,
0.1198,
0.107,
0.1628,
0.2152,
0.1777,
0.2036,
0.2213,
0.1266,
0.2524,
0.249,
0.1997,
0.3904,
0.3676,
0.208,
0.4198,
0.394,
0.2272,
0.4057,
0.3786,
0.1788,
0.1622,
0.1421,
0.1159,
0.1243,
0.1415,
0.2136,
0.1612,
0.1582,
0.2372,
0.1949,
0.1564,
0.1476,
0.1539,
0.1276,
0.2643,
0.2967,
0.1409,
0.3081,
0.2983,
0.2567,
0.4214,
0.3935,
0.4041,
0.247,
0.1164,
0.3623,
0.5193,
0.52,
0.5073,
0.4244,
0.4626,
0.4298,
0.4114,
0.4021,
0.3452,
0.1737,
0.1407
],
"marks": [
{
"key": "takeover",
"label": "takeover",
"t": 3557.2
},
{
"key": "mid",
"label": "mid",
"t": 3554.2
},
{
"key": "onset",
"label": "onset",
"t": 3515.7
}
],
"settled": false,
"noCandidate": false,
"note": "all five fractions inside 3.5 s => SHORT crossfade; nominal looks ~3 s late"
},
{
"track": 12,
"title": "Mafia",
"fromTitle": "Ghosts in the Toilets",
"nominal": 3681.6,
"clipStart": 3636.6,
"clipEnd": 3726.6,
"url": "/audio/bounds/12.flac",
"peaks": [
0.2591,
0.2928,
0.576,
0.442,
0.3923,
0.2965,
0.2041,
0.2335,
0.2601,
0.2331,
0.1988,
0.5598,
0.4288,
0.3565,
0.2858,
0.2843,
0.2182,
0.4585,
0.3986,
0.3195,
0.6887,
0.4625,
0.3266,
0.3198,
0.3048,
0.3055,
0.412,
0.3212,
0.2819,
0.2445,
0.2663,
0.2581,
0.3651,
0.2955,
0.2421,
0.2445,
0.2389,
0.3443,
0.3554,
0.261,
0.2462,
0.2148,
0.2145,
0.5748,
0.4936,
0.333,
0.2785,
0.2365,
0.2182,
0.3734,
0.3211,
0.2403,
0.2078,
0.1957,
0.1943,
0.332,
0.2519,
0.2281,
0.1772,
0.1845,
0.1694,
0.2879,
0.2031,
0.1697,
0.5445,
0.3601,
0.3969,
0.369,
0.3549,
0.2921,
0.2287,
0.2416,
0.5409,
0.7595,
0.2985,
0.3695,
0.1862,
0.2114,
0.4081,
0.3148,
0.4411,
0.3626,
0.3771,
0.3205,
0.7967,
0.4732,
0.345,
0.2938,
0.2923,
0.3252,
0.4932,
0.3476,
0.2814,
0.2665,
0.2355,
0.232,
0.7205,
0.3933,
0.3119,
0.2403,
0.3048,
0.2432,
0.367,
0.394,
0.3417,
0.3169,
0.33,
0.3986,
0.5993,
0.3179,
0.266,
0.2575,
0.2901,
0.3273,
0.3374,
0.2728,
0.2458,
0.1712,
0.2186,
0.5866,
0.4085,
0.1723,
0.1342,
0.1791,
0.2025,
0.3283,
0.2911,
0.2982,
0.3092,
0.3052,
0.31,
0.5364,
0.2951,
0.1906,
0.27,
0.2804,
0.2589,
0.3762,
0.2445,
0.1472,
0.1684,
0.182,
0.6237,
0.6792,
0.3107,
0.2097,
0.1469,
0.2056,
0.3494,
0.3158,
0.2583,
0.2704,
0.2914,
0.3414,
0.6007,
0.4345,
0.209,
0.3195,
0.3104,
0.289,
0.4231,
0.317,
0.2219,
0.2723,
0.2643,
0.2799,
0.7885,
0.335,
0.2651,
0.2184,
0.2266,
0.2864,
0.4471,
0.345,
0.2558,
0.3689,
0.4935,
0.3441,
0.2112,
0.235,
0.5509,
0.4196,
0.3861,
0.4597,
0.4135,
0.3451,
0.2975,
0.2774,
0.3454,
0.5723,
0.4356,
0.3895,
0.3017,
0.233,
0.2863,
0.3159,
0.274,
0.2411,
0.5734,
0.4412,
0.3643,
0.2756,
0.3202,
0.2324,
0.4873,
0.3659,
0.319,
0.4698,
0.2698,
0.1952,
0.2306,
0.2391,
0.3542,
0.3977,
0.3061,
0.2729,
0.1559,
0.1961,
0.3644,
0.3707,
0.2979,
0.2616,
0.2639,
0.221,
0.3718,
0.3259,
0.2758,
0.2396,
0.2403,
0.238,
0.5826,
0.4161,
0.3162,
0.2559,
0.2309,
0.2194,
0.3811,
0.3022,
0.2363,
0.2098,
0.215,
0.2291,
0.3453,
0.2558,
0.2633,
0.2058,
0.2018,
0.2952,
0.6244,
0.2891,
0.354,
0.516,
0.2923,
0.3743,
0.3174,
0.2719,
0.2711,
0.2588,
0.2771,
0.7699,
0.4128,
0.3036,
0.1945,
0.2133,
0.2166,
0.4132,
0.3008,
0.3355,
0.3341,
0.452,
0.3634,
0.6164,
0.3516,
0.2501,
0.3287,
0.3639,
0.3667,
0.5024,
0.3224,
0.2547,
0.233,
0.249,
0.4392,
0.6899,
0.3179,
0.2136,
0.1746,
0.2954,
0.356,
0.309,
0.3054,
0.3089,
0.3662,
0.3744,
0.6171,
0.4538,
0.2243,
0.2954,
0.3413,
0.3426,
0.4124,
0.3917,
0.2503,
0.2084,
0.2266,
0.2373,
0.6895,
0.2756,
0.1845,
0.163,
0.2417,
0.1946,
0.3563,
0.3323,
0.2621,
0.3414,
0.3416,
0.4012,
0.607,
0.3081,
0.2373,
0.2924,
0.2833,
0.3969,
0.4054,
0.1924,
0.1458,
0.1833,
0.1991,
0.7465,
0.4889,
0.2972,
0.2078,
0.1698,
0.2232,
0.3546,
0.3004,
0.2788,
0.306,
0.3517,
0.2625,
0.6457,
0.3883,
0.1956,
0.3277,
0.3129,
0.3055,
0.4288,
0.3099,
0.2157,
0.268,
0.2548,
0.4172,
0.8123,
0.3277,
0.1955,
0.1902,
0.2162,
0.4249,
0.3095,
0.3402,
0.2704,
0.4673,
0.3754,
0.6407,
0.4485,
0.2621,
0.2907,
0.3846,
0.3872,
0.5185,
0.3425,
0.2674,
0.2334,
0.245,
0.2943,
0.7177,
0.4699,
0.2164,
0.2236,
0.3073,
0.2756,
0.3702,
0.3311,
0.2719,
0.3967,
0.4107,
0.3453,
0.61,
0.1902,
0.1929,
0.3648,
0.3801,
0.3218,
0.2793,
0.132,
0.2063,
0.2068,
0.2569,
0.4769,
0.2855,
0.1585,
0.1682,
0.2144,
0.2738,
0.2018,
0.0932,
0.2141,
0.3733,
0.3533,
0.3455,
0.4712,
0.1948,
0.1315,
0.2796,
0.2101,
0.2355,
0.2788,
0.1677,
0.1638,
0.2313,
0.2158,
0.7455,
0.4477,
0.1569,
0.1607,
0.1251,
0.2107,
0.1662,
0.0976,
0.155,
0.2418,
0.27,
0.2467,
0.6593,
0.3561,
0.49,
0.8799,
0.6368,
0.4669,
0.7309,
0.5802,
0.4407,
0.32,
0.2046,
0.2215,
0.7941,
0.5805,
0.3701,
0.1866,
0.1802,
0.1977,
0.6524,
0.4986,
0.5487,
0.6504,
0.4695,
0.7936,
0.6897,
0.3938,
0.3045,
0.3334,
0.3406,
0.6781,
0.7931,
0.5556,
0.3117,
0.2109,
0.2739,
0.5751,
0.459,
0.3238,
0.1917,
0.1728,
0.2071,
0.3463,
0.3105,
0.3525,
0.354,
0.4372,
0.362,
0.5868,
0.31,
0.2935,
0.3082,
0.3354,
0.2632,
0.5984,
0.3199,
0.2226,
0.1772,
0.1999,
0.4458,
0.5206,
0.2063,
0.1924,
0.1457,
0.1716,
0.3191,
0.3086,
0.2301,
0.3591,
0.4415,
0.2841,
0.4762,
0.3484,
0.1929,
0.2117,
0.2407,
0.2317,
0.522,
0.484,
0.1824,
0.1236,
0.15,
0.1569,
0.5134,
0.3132,
0.2572,
0.155,
0.1498,
0.1636,
0.3511,
0.307,
0.2449,
0.3789,
0.4283,
0.4099,
0.5596,
0.2552,
0.2387,
0.2453,
0.2428,
0.3715,
0.6544,
0.2969,
0.2721,
0.3399,
0.3577,
0.6519,
0.3666,
0.2944,
0.1904,
0.2402,
0.2582,
0.3996,
0.3421,
0.273,
0.3548,
0.4064,
0.2944,
0.5444,
0.3215,
0.2172,
0.2666,
0.3361,
0.296,
0.622,
0.4733,
0.2305,
0.2535,
0.2995,
0.4839,
0.4681,
0.2855,
0.214,
0.2075,
0.2854,
0.3474,
0.3454,
0.3238,
0.2936,
0.3255,
0.4229,
0.5667,
0.3969,
0.2813,
0.27,
0.3047,
0.291,
0.7115,
0.4814,
0.2771,
0.1631,
0.1507,
0.1694,
0.5573,
0.3298,
0.2315,
0.1369,
0.1266,
0.1497,
0.3422,
0.2524,
0.3284,
0.385,
0.3134,
0.4813,
0.3698,
0.2657,
0.403,
0.4987,
0.271,
0.4087,
0.5167,
0.3932,
0.179,
0.1374,
0.1396,
0.4652,
0.4249,
0.2705,
0.2001,
0.1965,
0.2185,
0.3786,
0.3372,
0.3966,
0.339,
0.4461,
0.3394,
0.538,
0.3145,
0.3405,
0.5431,
0.4596,
0.2568,
0.6647,
0.3788,
0.2125,
0.1994,
0.2065,
0.2252,
0.4405,
0.3491,
0.2952,
0.2492,
0.182,
0.2044,
0.2212,
0.2158,
0.3598,
0.4725,
0.3425,
0.315,
0.1926,
0.196,
0.5389,
0.4163,
0.3884,
0.3376,
0.2214,
0.1386,
0.1387,
0.1417,
0.1682,
0.0783,
0.0761,
0.1342,
0.1419,
0.1245,
0.2004,
0.15,
0.1827,
0.2137,
0.2664,
0.1875,
0.2757,
0.2387,
0.2269,
0.1733,
0.2189,
0.1618,
0.2086,
0.1207,
0.0751,
0.0867,
0.1031,
0.1774,
0.1547,
0.0513,
0.0632,
0.0814,
0.12,
0.0971,
0.1073,
0.0591,
0.0606,
0.0836,
0.1118,
0.1026,
0.0627,
0.053,
0.0714,
0.0724,
0.1058,
0.1042,
0.0579,
0.0253,
0.0244,
0.0252,
0.0179,
0.0138,
0.0105,
0.0076,
0.1712,
0.2398,
0.2072,
0.0954,
0.0444,
0.0503,
0.4248,
0.5342,
0.462,
0.1844,
0.0778,
0.1188,
0.3597,
0.3171,
0.3564,
0.3177,
0.0186,
0.0295,
0.5428,
0.5623,
0.2536,
0.168,
0.0796,
0.1239,
0.4708,
0.4667,
0.4127,
0.3801,
0.3302,
0.3832,
0.471,
0.413,
0.3828,
0.2064,
0.2738,
0.3485,
0.3661,
0.3542,
0.3809,
0.1788,
0.2587,
0.3265,
0.3055,
0.2872,
0.2682,
0.1446,
0.196,
0.2651,
0.2544,
0.3304,
0.4096,
0.189,
0.2721,
0.3346,
0.2797,
0.4191,
0.4063,
0.3025,
0.2591,
0.2972,
0.247,
0.3404,
0.1755,
0.3155,
0.4823,
0.4314,
0.4165,
0.4626,
0.3036,
0.29,
0.357,
0.2301,
0.2927,
0.3079,
0.2011,
0.2753,
0.2418,
0.2562,
0.2538,
0.3761,
0.2014,
0.2985,
0.6058,
0.5629,
0.4178,
0.3715,
0.241,
0.254,
0.3461,
0.2516,
0.2285,
0.2256,
0.1533,
0.2552,
0.2741,
0.253,
0.255,
0.1772,
0.0917,
0.1002,
0.1079,
0.1086,
0.1318,
0.1554,
0.4296,
0.5166,
0.1875,
0.312,
0.3868,
0.3177,
0.1454,
0.1728,
0.1321,
0.1071,
0.1208,
0.0934,
0.1772,
0.3728,
0.2758,
0.2323,
0.1937,
0.0763,
0.1276,
0.1333,
0.1363,
0.1496,
0.4111,
0.322,
0.3854,
0.4116,
0.2947,
0.3057,
0.3894,
0.208,
0.3428,
0.2934,
0.3498,
0.3137,
0.1488,
0.2818,
0.3904,
0.6541,
0.3014,
0.2947,
0.1214,
0.1215,
0.1248,
0.1139,
0.1289,
0.6341,
0.7445,
0.4413,
0.2021,
0.0919,
0.1367,
0.5133,
0.3595,
0.173,
0.2056,
0.2024,
0.3345,
0.3495,
0.3213,
0.3528,
0.5185,
0.2866,
0.3104,
0.1572,
0.1295,
0.1551,
0.1612,
0.1663,
0.1997,
0.4299,
0.3946,
0.4421,
0.4596,
0.3307,
0.5553,
0.5634,
0.375,
0.3054,
0.3222,
0.3779,
0.1647,
0.164,
0.299,
0.7638,
0.5899,
0.3162,
0.1513,
0.1258,
0.1284,
0.1131,
0.1183,
0.1617,
0.5811,
0.3685,
0.2966,
0.1329,
0.0935,
0.1588,
0.6523,
0.1684,
0.1568,
0.18,
0.1724,
0.2935,
0.1977,
0.2722,
0.4822,
0.6551,
0.2764,
0.2078,
0.0841,
0.1244,
0.1333,
0.1401,
0.1497,
0.5963,
0.5561,
0.388,
0.4154,
0.2953,
0.2841,
0.5308,
0.4409,
0.3382,
0.2767,
0.3358,
0.3403,
0.132,
0.1881,
0.2829,
0.7131,
0.3518,
0.2774,
0.1133,
0.1146,
0.1178,
0.1066,
0.1094,
0.2501,
0.7317,
0.4169,
0.2176,
0.0749,
0.0944,
0.5766,
0.4209,
0.1472,
0.1719,
0.1636,
0.2707,
0.2846,
0.1998,
0.2917,
0.5987,
0.485,
0.2696,
0.2037,
0.0827,
0.1329,
0.139,
0.1458,
0.1565,
0.5521,
0.3726,
0.3895,
0.4166,
0.2973,
0.307,
0.4681,
0.2158,
0.3429,
0.2938,
0.3467,
0.3088,
0.1464,
0.2756,
0.3905,
0.6514,
0.29,
0.2824,
0.1149,
0.1144,
0.1174,
0.1069,
0.1209,
0.6917,
0.5313,
0.7104,
0.1981,
0.0768,
0.5563,
0.6084,
0.4626,
0.1517,
0.1647,
0.1581,
0.2609,
0.5702,
0.3821,
0.2708,
0.4913,
0.3266,
0.3286,
0.2825,
0.205,
0.2488,
0.2332,
0.1905,
0.2067,
0.6929,
0.3206,
0.4647,
0.2675,
0.2893,
0.6018,
0.4985,
0.2684,
0.2453,
0.2965,
0.2945,
0.5091,
0.491,
0.2146,
0.511,
0.4699,
0.1583,
0.1221,
0.0765,
0.1255,
0.1309,
0.1342,
0.2422,
0.9824,
0.5623,
0.4431,
0.1851,
0.1058,
0.0927,
0.2852,
0.0977,
0.1299,
0.1294,
0.1561,
0.1502,
0.6115,
0.2819,
0.276,
0.3287,
0.329,
0.3492,
0.1683,
0.2461,
0.2743,
0.2143,
0.2192,
0.6926,
0.4826,
0.4484,
0.5301,
0.2999,
0.3312,
0.2495,
0.2005,
0.3013,
0.3247,
0.3421,
0.3445,
0.5698,
0.3695,
0.2853,
0.357,
0.2705,
0.1652,
0.1446,
0.0998,
0.1493,
0.1541,
0.1579,
0.4085,
0.9255,
0.6966,
0.4149,
0.1231,
0.1205,
0.17,
0.1644,
0.1192,
0.1426,
0.1458,
0.1797,
0.6965,
0.5098,
0.3182,
0.3051,
0.3237,
0.3706,
0.3862,
0.1979,
0.2738,
0.2994,
0.2329,
0.2442,
0.714,
0.3824,
0.4885,
0.5642,
0.3173,
0.3475,
0.3288,
0.2608,
0.313,
0.3325,
0.349,
0.2864,
0.5898,
0.2295,
0.2813,
0.4117,
0.2156,
0.1535,
0.1016,
0.1266,
0.1431,
0.1504,
0.1664,
1.0,
0.6074,
0.6623,
0.3198,
0.1109,
0.1145,
0.3413,
0.2702,
0.1183,
0.1413,
0.1672,
0.1606,
0.6468,
0.3764,
0.2955,
0.3985,
0.3419,
0.3625,
0.312,
0.2335,
0.2802,
0.2616,
0.2213,
0.2342,
0.7427,
0.362,
0.5305,
0.302,
0.3304,
0.2867,
0.3013,
0.2978,
0.2709,
0.3348,
0.3371,
0.5543,
0.5498,
0.25,
0.3987,
0.3835,
0.187,
0.1398,
0.0843,
0.14,
0.1482,
0.1521,
0.2126,
0.9195,
0.601,
0.6759,
0.2121,
0.7314,
0.506,
0.1969,
0.1037,
0.1481,
0.1481,
0.1829,
0.1736,
0.7194,
0.3338,
0.3265,
0.2951,
0.2043,
0.1549,
0.0753,
0.1329,
0.145,
0.1463,
0.141,
0.7632,
0.6067,
0.1415,
0.2779,
0.2616,
0.2788,
0.3335,
0.2649,
0.3206,
0.3512,
0.3287,
0.2956,
0.6759,
0.4169,
0.4316,
0.6896,
0.432,
0.3778,
0.3498,
0.2269,
0.3674,
0.4318,
0.3299,
0.3197,
0.7637,
0.7964,
0.5059,
0.1566,
0.2424,
0.2398,
0.2457,
0.0913,
0.1188,
0.0956,
0.0769,
0.6994,
0.5153,
0.1295,
0.3362,
0.3002,
0.157,
0.143,
0.0893,
0.1345,
0.1307,
0.1374,
0.1342,
0.7667,
0.4052,
0.1444,
0.271,
0.2571,
0.2778,
0.2534,
0.1132,
0.3415,
0.3581,
0.3363,
0.2052,
0.7061,
0.2221,
0.4638,
0.5331,
0.4332,
0.4139,
0.2583,
0.2741,
0.4035,
0.4705,
0.3684,
0.7316,
0.5214,
0.8072,
0.4065,
0.1594,
0.2514,
0.2558,
0.0924,
0.0935,
0.1366,
0.1158,
0.0832,
0.7192,
0.3554,
0.1398,
0.2433,
0.2102,
0.1612,
0.121,
0.1092,
0.1413,
0.1896,
0.1552,
0.1484,
0.5925,
0.1536,
0.1515,
0.3203,
0.275,
0.2983,
0.0921,
0.1245,
0.4266,
0.3643,
0.3559,
0.5735,
0.459,
0.4845,
0.5232,
0.599,
0.4589,
0.4096,
0.2732,
0.3125,
0.5152,
0.4393,
0.3745,
0.5967,
0.5726,
0.698,
0.2667,
0.2672,
0.2594,
0.1758,
0.1223,
0.1274,
0.193,
0.1878,
0.1959,
0.5595,
0.1989,
0.1984,
0.4549,
0.4501,
0.4379,
0.1567,
0.1313,
0.163,
0.1902,
0.1492,
0.5795,
0.4538,
0.1465,
0.3171,
0.2963,
0.2821,
0.2828,
0.0811,
0.3128,
0.3858,
0.3191,
0.2695,
0.4619,
0.291,
0.3602,
0.4133,
0.3121,
0.2927,
0.2551,
0.157,
0.2364,
0.2999,
0.2295,
0.2121,
0.3446,
0.4683,
0.2858,
0.9723,
0.8779,
0.8921,
0.8794,
0.5102,
0.0701,
0.0682,
0.0453,
0.345,
0.2671,
0.0946,
0.1135,
0.1362,
0.1559,
0.1668,
0.179,
0.1758,
0.1899,
0.2163,
0.1095,
0.356,
0.1877,
0.1022,
0.1869,
0.1807,
0.1554,
0.4626,
0.4361,
0.2419,
0.2302
],
"marks": [
{
"key": "takeover",
"label": "takeover",
"t": 3688.1
},
{
"key": "mid",
"label": "mid",
"t": 3680.1
},
{
"key": "onset",
"label": "onset",
"t": 3667.1
}
],
"settled": false,
"noCandidate": false,
"note": "span 21 s => LONG crossfade; nominal sits mid-span, so the takeover is later than the current cut"
},
{
"track": 13,
"title": "Vague de CRIME",
"fromTitle": "Mafia",
"nominal": 3889.1,
"clipStart": 3844.1,
"clipEnd": 3934.1,
"url": "/audio/bounds/13.flac",
"peaks": [
0.9987,
0.8566,
0.5246,
0.4166,
0.5394,
0.4235,
0.9775,
0.7875,
0.501,
0.896,
0.7837,
0.5879,
0.9731,
0.7982,
0.3481,
0.3873,
0.4682,
0.489,
0.9822,
0.7872,
0.3645,
0.4174,
0.4478,
0.5516,
0.9429,
0.6443,
0.3854,
0.505,
0.4438,
0.4415,
0.9931,
0.6576,
0.491,
0.4754,
0.4626,
0.9751,
0.8017,
0.4708,
0.3897,
0.4461,
0.4601,
1.0,
0.8632,
0.504,
0.4476,
0.406,
0.5203,
0.9723,
0.7585,
0.2599,
0.5853,
0.5577,
0.4733,
0.6529,
0.4764,
0.5523,
0.9082,
0.6367,
0.2886,
0.883,
0.6889,
0.3271,
0.4589,
0.4623,
0.4536,
0.9757,
0.6749,
0.4177,
0.3868,
0.4423,
0.48,
0.444,
0.4439,
0.4269,
0.4162,
0.4349,
0.419,
0.4448,
0.524,
0.496,
0.4413,
0.3926,
0.3669,
0.4616,
0.4257,
0.3987,
0.4527,
0.3771,
0.3981,
0.3971,
0.4236,
0.4229,
0.4003,
0.3908,
0.4317,
0.4249,
0.4102,
0.4252,
0.4201,
0.4801,
0.364,
0.3758,
0.4058,
0.9243,
0.5364,
0.8481,
0.8679,
0.4435,
0.3978,
0.4496,
0.4276,
0.5126,
0.5197,
0.4804,
0.4611,
0.4443,
0.389,
0.4839,
0.4279,
0.4588,
0.4342,
0.4417,
0.4331,
0.4361,
0.5492,
0.5278,
0.5189,
0.4591,
0.4111,
0.3711,
0.4778,
0.4386,
0.4054,
0.4692,
0.3691,
0.4168,
0.418,
0.4412,
0.4223,
0.377,
0.4336,
0.4393,
0.432,
0.4253,
0.4335,
0.4172,
0.5034,
0.397,
0.3542,
0.8673,
0.7191,
0.2541,
0.9855,
0.7157,
0.3122,
0.3885,
0.4102,
0.4261,
0.5192,
0.521,
0.4907,
0.465,
0.4606,
0.3991,
0.4986,
0.4277,
0.4612,
0.4333,
0.4436,
0.4351,
0.4384,
0.5493,
0.5192,
0.4398,
0.4587,
0.4091,
0.405,
0.4755,
0.4409,
0.4579,
0.4696,
0.39,
0.4191,
0.4034,
0.445,
0.4221,
0.3792,
0.4369,
0.4434,
0.4361,
0.4268,
0.4315,
0.4226,
0.98,
0.5849,
0.2359,
0.2716,
0.1314,
0.166,
0.2445,
0.1813,
0.2806,
0.4301,
0.4663,
0.4937,
0.2644,
0.1763,
0.3209,
0.3667,
0.3498,
0.3492,
0.2156,
0.2722,
0.2982,
0.408,
0.3855,
0.425,
0.2265,
0.3021,
0.357,
0.3925,
0.3998,
0.5918,
0.248,
0.2704,
0.4614,
0.5726,
0.4842,
0.45,
0.2259,
0.3145,
0.4363,
0.4224,
0.4625,
0.4419,
0.1734,
0.2633,
0.3809,
0.4043,
0.3996,
0.4052,
0.4545,
0.4637,
0.2467,
0.358,
0.4037,
0.1857,
0.2113,
0.3558,
0.4397,
0.5336,
0.5314,
0.1561,
0.1838,
0.3916,
0.4072,
0.3783,
0.3801,
0.2148,
0.281,
0.3934,
0.4251,
0.3867,
0.4267,
0.2309,
0.3015,
0.367,
0.3936,
0.5919,
0.5904,
0.2293,
0.2909,
0.5746,
0.4234,
0.4843,
0.4401,
0.232,
0.3817,
0.4398,
0.4232,
0.4625,
0.2201,
0.2134,
0.3429,
0.4049,
0.4095,
0.3978,
0.1743,
0.249,
0.3496,
0.2448,
0.3407,
0.3991,
0.1671,
0.2158,
0.441,
0.4511,
0.5412,
0.5093,
0.1499,
0.2175,
0.3943,
0.377,
0.3802,
0.3752,
0.2503,
0.2804,
0.4173,
0.4072,
0.426,
0.3699,
0.2693,
0.3023,
0.3942,
0.3757,
0.5905,
0.2478,
0.2404,
0.3529,
0.5612,
0.4523,
0.4861,
0.2088,
0.2717,
0.4274,
0.4276,
0.461,
0.4449,
0.1726,
0.2261,
0.3755,
0.4159,
0.4064,
0.3962,
0.4527,
0.4664,
0.4625,
0.2492,
0.3472,
0.4041,
0.1758,
0.2886,
0.4512,
0.4967,
0.5391,
0.2736,
0.1657,
0.2459,
0.3955,
0.3829,
0.386,
0.2307,
0.253,
0.3138,
0.4215,
0.4095,
0.4231,
0.2265,
0.3049,
0.3392,
0.3975,
0.3999,
0.5934,
0.2467,
0.2729,
0.4634,
0.5786,
0.4833,
0.4552,
0.2086,
0.3126,
0.4241,
0.426,
0.46,
0.442,
0.175,
0.2663,
0.3839,
0.4058,
0.422,
0.3778,
0.1777,
0.351,
0.2412,
0.3692,
0.3969,
0.1687,
0.2208,
0.3248,
0.4799,
0.4803,
0.5126,
0.2195,
0.261,
0.4128,
0.4777,
0.4672,
0.4705,
0.2545,
0.235,
0.4234,
0.4887,
0.5355,
0.4873,
0.2583,
0.2848,
0.5981,
0.5519,
0.4681,
0.4836,
0.2411,
0.2748,
0.368,
0.4603,
0.4631,
0.3333,
0.2465,
0.3573,
0.463,
0.4384,
0.4583,
0.2021,
0.228,
0.3171,
0.404,
0.408,
0.4307,
0.3025,
0.4211,
0.4622,
0.1753,
0.2414,
0.32,
0.3051,
0.1763,
0.3499,
0.4424,
0.4312,
0.4978,
0.2484,
0.3204,
0.3728,
0.4344,
0.423,
0.4391,
0.2405,
0.2512,
0.3878,
0.4519,
0.5025,
0.3556,
0.2024,
0.4396,
0.5805,
0.5345,
0.4666,
0.3435,
0.2328,
0.3492,
0.4624,
0.4666,
0.4033,
0.1865,
0.2487,
0.38,
0.4677,
0.4456,
0.4654,
0.1862,
0.2314,
0.322,
0.411,
0.4146,
0.4381,
0.1947,
0.3231,
0.4318,
0.1992,
0.3278,
0.3214,
0.2098,
0.2736,
0.376,
0.451,
0.5078,
0.3017,
0.2298,
0.327,
0.442,
0.4327,
0.4481,
0.229,
0.2459,
0.3572,
0.396,
0.4628,
0.5172,
0.2728,
0.2049,
0.4864,
0.5916,
0.462,
0.4735,
0.2362,
0.2697,
0.3497,
0.4665,
0.4682,
0.3927,
0.1596,
0.2915,
0.4444,
0.4617,
0.4594,
0.4093,
0.1618,
0.2565,
0.4039,
0.4089,
0.4298,
0.2973,
0.4219,
0.4207,
0.4605,
0.2286,
0.3213,
0.313,
0.1783,
0.3504,
0.4426,
0.4325,
0.4962,
0.2498,
0.323,
0.3419,
0.4345,
0.4249,
0.4387,
0.5158,
0.4204,
0.4452,
0.4499,
0.5084,
0.5047,
0.4769,
0.44,
0.5969,
0.5344,
0.4508,
0.4672,
0.5012,
0.4282,
0.4182,
0.4607,
0.4624,
0.4094,
0.4461,
0.4357,
0.4761,
0.451,
0.463,
0.4254,
0.4308,
0.4031,
0.4157,
0.4114,
0.4294,
0.2883,
0.4158,
0.462,
0.4023,
0.3845,
0.42,
0.4356,
0.4298,
0.4508,
0.4458,
0.497,
0.449,
0.4127,
0.3962,
0.4584,
0.3614,
0.3606,
0.4184,
0.4256,
0.4502,
0.3247,
0.3668,
0.4068,
0.4833,
0.4813,
0.4844,
0.5323,
0.4631,
0.4489,
0.5168,
0.5107,
0.4802,
0.4164,
0.4812,
0.4986,
0.4881,
0.5649,
0.4907,
0.501,
0.4603,
0.3605,
0.3468,
0.4177,
0.4452,
0.4774,
0.5746,
0.4461,
0.4295,
0.4215,
0.4301,
0.4606,
0.377,
0.3127,
0.2876,
0.2937,
0.3097,
0.2624,
0.2831,
0.2939,
0.3119,
0.3222,
0.3378,
0.36,
0.3751,
0.3784,
0.3762,
0.3477,
0.2857,
0.2353,
0.2503,
0.2594,
0.2642,
0.2664,
0.2827,
0.3036,
0.3113,
0.3141,
0.3111,
0.3173,
0.3221,
0.3319,
0.3391,
0.3384,
0.3306,
0.3054,
0.295,
0.2933,
0.2776,
0.281,
0.3127,
0.3176,
0.3143,
0.2766,
0.1966,
0.1715,
0.1534,
0.1414,
0.1033,
0.0789,
0.078,
0.0844,
0.0991,
0.1095,
0.1066,
0.1021,
0.1022,
0.1043,
0.1246,
0.1338,
0.1481,
0.1511,
0.1494,
0.1477,
0.06,
0.0683,
0.1576,
0.2964,
0.3424,
0.3465,
0.357,
0.2228,
0.0983,
0.118,
0.2041,
0.3409,
0.3261,
0.3009,
0.3721,
0.159,
0.0717,
0.0551,
0.081,
0.0837,
0.076,
0.053,
0.0638,
0.0422,
0.0507,
0.0821,
0.2028,
0.2224,
0.2006,
0.2218,
0.2114,
0.0631,
0.0739,
0.0812,
0.2503,
0.2598,
0.2055,
0.1975,
0.1827,
0.108,
0.1251,
0.1634,
0.3566,
0.3577,
0.3319,
0.318,
0.0659,
0.1395,
0.1356,
0.1906,
0.3835,
0.3433,
0.3062,
0.2632,
0.0676,
0.1165,
0.1297,
0.1938,
0.3654,
0.2739,
0.2768,
0.2346,
0.0637,
0.1054,
0.1162,
0.3412,
0.387,
0.2981,
0.2831,
0.2796,
0.0708,
0.103,
0.1198,
0.4413,
0.405,
0.2612,
0.2682,
0.2511,
0.0942,
0.1131,
0.1444,
0.4402,
0.3644,
0.2965,
0.2811,
0.2307,
0.0989,
0.1029,
0.1668,
0.4093,
0.3118,
0.2817,
0.2808,
0.163,
0.1056,
0.1375,
0.2149,
0.4641,
0.3493,
0.3365,
0.3284,
0.0638,
0.1109,
0.1303,
0.2709,
0.4649,
0.3282,
0.3425,
0.3244,
0.0937,
0.1193,
0.1532,
0.4889,
0.4503,
0.3507,
0.3656,
0.3539,
0.1052,
0.1282,
0.182,
0.2296,
0.1883,
0.1475,
0.2316,
0.2159,
0.1051,
0.1174,
0.1932,
0.4478,
0.3521,
0.3139,
0.3153,
0.2919,
0.2136,
0.262,
0.2729,
0.44,
0.3559,
0.3228,
0.3411,
0.1596,
0.2297,
0.2633,
0.2664,
0.183,
0.1489,
0.2039,
0.3347,
0.1814,
0.2532,
0.2983,
0.4976,
0.484,
0.365,
0.3744,
0.3745,
0.1395,
0.1355,
0.2543,
0.2859,
0.306,
0.2889,
0.1327,
0.2591,
0.3028,
0.3186,
0.4545,
0.329,
0.331,
0.3141,
0.1628,
0.3072,
0.3423,
0.3556,
0.5005,
0.3404,
0.3516,
0.3318,
0.2431,
0.3155,
0.3371,
0.4608,
0.4962,
0.3125,
0.3308,
0.243,
0.1623,
0.1451,
0.1364,
0.2914,
0.3524,
0.3239,
0.3485,
0.3384,
0.2579,
0.3134,
0.3026,
0.4863,
0.4093,
0.3478,
0.3473,
0.3363,
0.2876,
0.3211,
0.3133,
0.5514,
0.4274,
0.3461,
0.3577,
0.1523,
0.2897,
0.3308,
0.3233,
0.5269,
0.3839,
0.3669,
0.3756,
0.1982,
0.1532,
0.155,
0.2083,
0.5195,
0.4789,
0.4391,
0.4269,
0.255,
0.3317,
0.3488,
0.5708,
0.581,
0.4534,
0.4462,
0.416,
0.2472,
0.2968,
0.3243,
0.5614,
0.5175,
0.4195,
0.4283,
0.4054,
0.2501,
0.3276,
0.3328,
0.5276,
0.4826,
0.4051,
0.4111,
0.1997,
0.2945,
0.3229,
0.333,
0.5099,
0.3704,
0.3445,
0.3385,
0.1402,
0.1996,
0.2547,
0.2639,
0.3973,
0.276,
0.2853,
0.3074,
0.141,
0.2399,
0.2435,
0.3682,
0.3339,
0.2589,
0.2727,
0.2743,
0.1739,
0.2397,
0.2537,
0.3723,
0.3314,
0.2817,
0.3048,
0.2961,
0.0901,
0.1044,
0.1152,
0.2489,
0.2421,
0.1837,
0.1777,
0.129,
0.1875,
0.2228,
0.2377,
0.2825,
0.243,
0.2369,
0.2271,
0.1112,
0.2,
0.2205,
0.223,
0.2977,
0.2308,
0.2292,
0.2022,
0.1512,
0.2103,
0.2409,
0.2447,
0.343,
0.2431,
0.2571,
0.2134,
0.0451,
0.0571,
0.0668,
0.1998,
0.2053,
0.1586,
0.1965,
0.1877,
0.1029,
0.1675,
0.169,
0.3266,
0.2783,
0.1845,
0.1869,
0.1773,
0.142,
0.1815,
0.1733,
0.3251,
0.2511,
0.2065,
0.2007,
0.1639,
0.1652,
0.1778,
0.1836,
0.3017,
0.2259,
0.2004,
0.2045,
0.1177,
0.0802,
0.0995,
0.1543,
0.3274,
0.2517,
0.2327,
0.2455,
0.0893,
0.1881,
0.2043,
0.3298,
0.3008,
0.244,
0.2358,
0.2264,
0.1459,
0.2165,
0.2158,
0.3479,
0.3282,
0.278,
0.2889,
0.2961,
0.1702,
0.2119,
0.2124,
0.3465,
0.3283,
0.2629,
0.2474,
0.1374,
0.0819,
0.0811,
0.1332,
0.324,
0.2595,
0.2185,
0.2573,
0.2078,
0.1537,
0.1811,
0.2067,
0.3221,
0.2813,
0.2342,
0.2579,
0.115,
0.1703,
0.1918,
0.1941,
0.1339,
0.1605,
0.1474,
0.2342,
0.1182,
0.1717,
0.2101,
0.3639,
0.3492,
0.3186,
0.2718,
0.2721,
0.0295,
0.027,
0.0796,
0.1211,
0.1689,
0.1284,
0.0891,
0.0544,
0.056,
0.0446,
0.1233,
0.0963,
0.1684,
0.0932,
0.0677,
0.0397,
0.0681,
0.089,
0.1345,
0.1883,
0.1784,
0.1272,
0.1383,
0.0721,
0.053,
0.1368,
0.1198,
0.1689,
0.1742,
0.0549,
0.0168,
0.0772,
0.018,
0.0517,
0.0776,
0.1726,
0.0978,
0.1252,
0.0239,
0.0658,
0.0415,
0.1131,
0.0728,
0.1563,
0.098,
0.0773,
0.0321,
0.0594,
0.0471,
0.0939,
0.1012,
0.1448,
0.1107,
0.132,
0.0313,
0.0651,
0.0899,
0.1256,
0.1173,
0.1476,
0.1106,
0.0331,
0.0687,
0.0257,
0.08,
0.1446,
0.2105,
0.2092,
0.2147,
0.1396,
0.0573,
0.1293,
0.1482,
0.1628,
0.214,
0.1323,
0.0946,
0.0722,
0.1247,
0.085,
0.0677,
0.1549,
0.2091,
0.1388,
0.1496,
0.0521,
0.0725,
0.216,
0.2599,
0.0688,
0.1764,
0.0818,
0.0333,
0.0401,
0.1106,
0.1328,
0.2029,
0.1853,
0.2359,
0.2666,
0.2177,
0.1017,
0.147,
0.1961,
0.2394,
0.2846,
0.2474,
0.2131,
0.1361,
0.1407,
0.1793,
0.1893,
0.2697,
0.3382,
0.1937,
0.201,
0.123,
0.0988,
0.1019,
0.0842,
0.0887,
0.1409,
0.0908,
0.0684,
0.0851,
0.0831,
0.0917,
0.1111,
0.1147,
0.1336,
0.1187,
0.0974,
0.0522,
0.0464,
0.0612,
0.1649,
0.2155,
0.2294,
0.2087,
0.1026,
0.0845,
0.05,
0.0951,
0.2112,
0.1857,
0.1332,
0.1356,
0.0303,
0.0607,
0.0878,
0.1469,
0.1612,
0.2322,
0.1372,
0.0677,
0.034,
0.0683,
0.0463,
0.0744,
0.0726,
0.1548,
0.1033,
0.142,
0.0653,
0.0501,
0.0664,
0.0851,
0.1555,
0.1477,
0.0936,
0.0727,
0.0757,
0.0527,
0.0571,
0.1214,
0.1736,
0.1435,
0.1485,
0.0846,
0.0706,
0.0537,
0.0791,
0.1005,
0.1749,
0.1043,
0.1427,
0.0377,
0.0997,
0.0696,
0.0581,
0.1335,
0.1929,
0.0981,
0.2757,
0.1188,
0.2666,
0.2741,
0.4701,
0.5254,
0.441,
0.3902,
0.3721,
0.146,
0.214,
0.1948,
0.4998,
0.4445,
0.359,
0.3533,
0.3242,
0.1263,
0.1704,
0.1663,
0.4266,
0.3721,
0.2922,
0.2784,
0.2127,
0.1585,
0.1774,
0.1797,
0.3876,
0.3252,
0.3047,
0.2951,
0.1711,
0.1399,
0.1603,
0.1903,
0.3834,
0.3235,
0.3071,
0.3041,
0.125,
0.1904,
0.2062,
0.3029,
0.315,
0.2762,
0.2306,
0.2782,
0.1583,
0.2066,
0.1976,
0.1293,
0.1018,
0.0315,
0.0096,
0.0246,
0.0485,
0.0599,
0.0911,
0.09,
0.0458,
0.0422,
0.0244,
0.0096,
0.0047,
0.0067,
0.0116,
0.0928,
0.2163,
0.2313,
0.2318,
0.0901,
0.081,
0.0464,
0.1081,
0.1872,
0.2138,
0.1751,
0.0642,
0.0068,
0.0082,
0.062,
0.1336,
0.1746,
0.109,
0.057,
0.0794,
0.095,
0.1906,
0.217,
0.2191,
0.223,
0.1887,
0.0925,
0.2224,
0.3822,
0.4846,
0.4987,
0.4981,
0.4493,
0.4352,
0.361,
0.1793,
0.2907,
0.3661,
0.3183,
0.1724,
0.1022,
0.0868,
0.051,
0.0739,
0.1007,
0.1086,
0.1177,
0.0902,
0.0979,
0.1007,
0.0821,
0.1697,
0.3475,
0.3416,
0.3099,
0.3024
],
"marks": [],
"settled": false,
"noCandidate": true,
"note": "NO CANDIDATE \u2014 timbral lens structurally blind here (shared sounds across the switch). Ear-only."
},
{
"track": 15,
"title": "REVOLUTION",
"fromTitle": "Desire",
"nominal": 4547.45,
"clipStart": 4502.45,
"clipEnd": 4592.45,
"url": "/audio/bounds/15.flac",
"peaks": [
0.5188,
0.485,
0.313,
0.3171,
0.6287,
0.7878,
0.7358,
0.7553,
0.4799,
0.5398,
0.5469,
0.4619,
0.4264,
0.3381,
0.3442,
0.3675,
0.3426,
0.3327,
0.348,
0.5385,
0.5118,
0.4422,
0.6848,
0.6632,
0.5464,
0.4606,
0.5289,
0.4426,
0.3552,
0.3728,
0.5331,
0.3759,
0.3954,
0.5414,
0.5673,
0.4179,
0.4848,
0.4779,
0.3841,
0.2984,
0.3452,
0.3224,
0.3467,
0.3328,
0.3107,
0.3718,
0.2867,
0.2876,
0.5356,
0.37,
0.3132,
0.6437,
0.5921,
0.429,
0.4098,
0.6118,
0.5194,
0.3637,
0.3266,
0.5609,
0.4079,
0.3384,
0.4309,
0.6401,
0.5662,
0.5077,
0.4858,
0.5598,
0.4412,
0.4451,
0.4259,
0.368,
0.3313,
0.3316,
0.3709,
0.3314,
0.3229,
0.6128,
0.5084,
0.3685,
0.5828,
0.7073,
0.5217,
0.468,
0.5228,
0.4589,
0.3657,
0.3142,
0.5314,
0.4304,
0.3144,
0.6589,
0.6975,
0.4664,
0.4898,
0.4491,
0.4464,
0.3584,
0.3575,
0.3144,
0.3027,
0.3111,
0.3753,
0.3726,
0.3098,
0.2835,
0.536,
0.4698,
0.3922,
0.3159,
0.6094,
0.4812,
0.4155,
0.5482,
0.5289,
0.3641,
0.3391,
0.5001,
0.4347,
0.3325,
0.6276,
1.0,
0.8194,
0.7452,
0.8071,
0.7861,
0.6141,
0.4476,
0.3531,
0.3715,
0.4478,
0.3329,
0.3254,
0.3196,
0.2706,
0.5307,
0.4854,
0.5004,
0.4911,
0.7011,
0.5779,
0.5202,
0.3896,
0.5545,
0.4081,
0.3981,
0.4514,
0.446,
0.4495,
0.3851,
0.4348,
0.4074,
0.4348,
0.4123,
0.3317,
0.229,
0.25,
0.4841,
0.4615,
0.4987,
0.3579,
0.3351,
0.4162,
0.5854,
0.4928,
0.5065,
0.3652,
0.3001,
0.736,
0.6188,
0.4433,
0.3696,
0.5937,
0.4519,
0.3528,
0.5642,
0.5887,
0.4419,
0.5036,
0.462,
0.4662,
0.4965,
0.4203,
0.4964,
0.2651,
0.2629,
0.4128,
0.4898,
0.4885,
0.4289,
0.3074,
0.5122,
0.4894,
0.4714,
0.476,
0.3347,
0.3187,
0.6648,
0.8558,
0.5276,
0.503,
0.594,
0.5056,
0.2759,
0.4639,
0.5793,
0.4588,
0.3035,
0.5948,
0.5972,
0.4158,
0.4158,
0.3976,
0.2923,
0.2313,
0.4366,
0.4372,
0.4538,
0.3846,
0.2839,
0.3181,
0.4075,
0.4011,
0.5292,
0.3759,
0.2739,
0.6775,
0.7052,
0.5307,
0.4537,
0.5164,
0.4995,
0.3248,
0.4862,
0.5615,
0.4418,
0.2896,
0.6106,
0.6237,
0.4408,
0.3946,
0.4756,
0.3735,
0.3727,
0.4273,
0.494,
0.4616,
0.5782,
0.4057,
0.4934,
0.6787,
0.4544,
0.5661,
0.4939,
0.3789,
0.566,
0.7344,
0.5863,
0.4798,
0.5395,
0.4489,
0.344,
0.4894,
0.6726,
0.6939,
0.4112,
0.3712,
0.3581,
0.3355,
0.3697,
0.3503,
0.2205,
0.2098,
0.2755,
0.3951,
0.3937,
0.3895,
0.2741,
0.2605,
0.4749,
0.4646,
0.4369,
0.3465,
0.2998,
0.3858,
0.6437,
0.4871,
0.4016,
0.4074,
0.5307,
0.3586,
0.3248,
0.5393,
0.5312,
0.3535,
0.4787,
0.4576,
0.476,
0.4285,
0.4838,
0.3171,
0.2583,
0.4039,
0.4566,
0.4866,
0.4859,
0.4265,
0.2444,
0.51,
0.4892,
0.4928,
0.4488,
0.3315,
0.3824,
0.8515,
0.752,
0.5346,
0.427,
0.5922,
0.3731,
0.2749,
0.5749,
0.4943,
0.4562,
0.3897,
0.5955,
0.5219,
0.4162,
0.398,
0.2909,
0.2896,
0.2322,
0.4364,
0.4523,
0.4257,
0.3847,
0.2904,
0.3349,
0.4083,
0.4199,
0.5253,
0.3719,
0.2883,
0.6994,
0.599,
0.394,
0.4529,
0.5186,
0.4485,
0.3269,
0.568,
0.5392,
0.3321,
0.3424,
0.7897,
0.6999,
0.6315,
0.6917,
0.3656,
0.4563,
0.4985,
0.5123,
0.3626,
0.2889,
0.2369,
0.3009,
0.3913,
0.3221,
0.2528,
0.1771,
0.3022,
0.264,
0.2352,
0.2145,
0.1809,
0.1384,
0.1875,
0.1794,
0.1214,
0.0825,
0.0721,
0.1127,
0.4144,
0.5905,
0.0578,
0.1056,
0.1147,
0.1156,
0.0937,
0.0785,
0.0597,
0.0608,
0.0669,
0.0627,
0.0446,
0.0257,
0.0301,
0.0407,
0.2225,
0.0173,
0.0212,
0.0364,
0.033,
0.0285,
0.0265,
0.0201,
0.0157,
0.0188,
0.0196,
0.011,
0.0134,
0.008,
0.0109,
0.0414,
0.0674,
0.0047,
0.0097,
0.0108,
0.0102,
0.0085,
0.0069,
0.006,
0.006,
0.0058,
0.0054,
0.0038,
0.0021,
0.0023,
0.004,
0.0201,
0.0025,
0.0019,
0.003,
0.0033,
0.0024,
0.0025,
0.0018,
0.0013,
0.0017,
0.0017,
0.0011,
0.0012,
0.0008,
0.0009,
0.0017,
0.0061,
0.0004,
0.0009,
0.001,
0.0009,
0.0008,
0.0006,
0.0006,
0.0005,
0.0005,
0.0005,
0.0003,
0.0002,
0.0002,
0.0003,
0.0014,
0.0018,
0.0002,
0.0003,
0.0003,
0.0002,
0.0002,
0.0002,
0.0001,
0.0001,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0001,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0001,
0.0001,
0.0,
0.0,
0.0,
0.0008,
0.012,
0.0131,
0.0326,
0.0243,
0.0127,
0.0152,
0.0119,
0.0258,
0.0051,
0.0371,
0.0364,
0.0355,
0.0157,
0.0065,
0.0027,
0.0512,
0.0215,
0.0013,
0.0398,
0.0297,
0.0086,
0.0218,
0.0214,
0.0142,
0.0023,
0.027,
0.0322,
0.0319,
0.0238,
0.0062,
0.0001,
0.0001,
0.0138,
0.0263,
0.0396,
0.035,
0.0335,
0.0113,
0.0002,
0.0,
0.0005,
0.0153,
0.0299,
0.0291,
0.0264,
0.0079,
0.0204,
0.0133,
0.0462,
0.0162,
0.0151,
0.0463,
0.0421,
0.0218,
0.0268,
0.0215,
0.0342,
0.004,
0.0325,
0.0322,
0.0306,
0.0127,
0.0057,
0.0003,
0.0044,
0.0134,
0.0144,
0.0326,
0.0294,
0.0177,
0.0152,
0.014,
0.0258,
0.0159,
0.0284,
0.0371,
0.0364,
0.0329,
0.0075,
0.0065,
0.0175,
0.047,
0.0015,
0.0013,
0.0398,
0.0187,
0.0184,
0.0217,
0.0181,
0.0119,
0.0149,
0.0307,
0.0322,
0.0285,
0.0083,
0.0012,
0.0001,
0.0007,
0.0204,
0.0327,
0.0397,
0.035,
0.0178,
0.0026,
0.0,
0.0001,
0.0006,
0.0281,
0.0299,
0.029,
0.0126,
0.0204,
0.0192,
0.0482,
0.0599,
0.0199,
0.0241,
0.0668,
0.0552,
0.0314,
0.0386,
0.0493,
0.0168,
0.0335,
0.0468,
0.0453,
0.0399,
0.0103,
0.0022,
0.0017,
0.0063,
0.0207,
0.02,
0.0525,
0.0384,
0.0245,
0.0226,
0.0411,
0.0416,
0.0055,
0.0597,
0.0586,
0.0572,
0.0194,
0.0105,
0.0019,
0.0676,
0.0255,
0.0021,
0.0642,
0.0478,
0.0443,
0.0683,
0.0673,
0.0375,
0.0067,
0.0848,
0.1012,
0.097,
0.0457,
0.0152,
0.0003,
0.0003,
0.0563,
0.0935,
0.1661,
0.1866,
0.1978,
0.0615,
0.0009,
0.0003,
0.0045,
0.1211,
0.237,
0.2306,
0.2003,
0.1412,
0.1615,
0.0996,
0.2867,
0.0806,
0.1255,
0.3677,
0.3342,
0.1731,
0.2123,
0.1705,
0.2716,
0.0243,
0.0215,
0.0008,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0021,
0.0057,
0.0371,
0.0523,
0.2084,
0.1001,
0.4989,
0.5462,
0.5964,
0.4701,
0.1051,
0.0725,
0.2753,
0.6443,
0.0237,
0.1559,
0.6041,
0.2787,
0.2681,
0.3104,
0.2451,
0.1671,
0.2049,
0.4226,
0.4335,
0.3527,
0.1066,
0.0151,
0.0007,
0.0165,
0.2497,
0.3946,
0.4921,
0.4558,
0.2542,
0.0285,
0.0005,
0.001,
0.0089,
0.4666,
0.454,
0.4304,
0.1931,
0.2922,
0.2454,
0.376,
0.1638,
0.1197,
0.1602,
0.3004,
0.1707,
0.1422,
0.1316,
0.182,
0.0438,
0.1137,
0.1527,
0.1473,
0.1305,
0.033,
0.0038,
0.0093,
0.0205,
0.0677,
0.0622,
0.1536,
0.1122,
0.0716,
0.066,
0.1217,
0.1066,
0.0162,
0.1742,
0.1713,
0.1654,
0.0566,
0.0306,
0.0041,
0.1921,
0.0453,
0.0061,
0.1876,
0.1398,
0.0747,
0.1022,
0.0958,
0.0561,
0.0059,
0.1405,
0.1514,
0.1452,
0.0697,
0.0169,
0.0005,
0.0006,
0.0842,
0.1401,
0.1868,
0.1646,
0.1577,
0.0374,
0.0006,
0.0002,
0.0027,
0.0718,
0.1406,
0.1369,
0.1189,
0.0928,
0.0956,
0.0654,
0.1676,
0.0546,
0.0745,
0.2182,
0.1983,
0.1027,
0.1259,
0.1488,
0.1612,
0.0265,
0.1527,
0.1518,
0.1394,
0.0498,
0.0214,
0.0012,
0.0205,
0.063,
0.0677,
0.1536,
0.1383,
0.06,
0.0716,
0.0649,
0.1217,
0.0464,
0.174,
0.1682,
0.1713,
0.1336,
0.0306,
0.0211,
0.1221,
0.192,
0.0061,
0.051,
0.1876,
0.0882,
0.0918,
0.1022,
0.0807,
0.0529,
0.0702,
0.1476,
0.1514,
0.1257,
0.0386,
0.0054,
0.0004,
0.0111,
0.0528,
0.094,
0.1004,
0.0964,
0.0477,
0.0544,
0.1486,
0.1355,
0.0562,
0.0696,
0.0941,
0.0998,
0.0702,
0.1491,
0.1142,
0.0715,
0.0317,
0.071,
0.1231,
0.1081,
0.1443,
0.1245,
0.1243,
0.0292,
0.0419,
0.0982,
0.1204,
0.1152,
0.1147,
0.0482,
0.0271,
0.0218,
0.0751,
0.099,
0.1382,
0.1389,
0.1338,
0.0556,
0.2571,
0.2646,
0.2031,
0.0765,
0.1414,
0.1566,
0.1475,
0.1332,
0.1833,
0.0892,
0.0671,
0.0325,
0.1081,
0.1345,
0.2022,
0.1166,
0.1389,
0.1424,
0.0869,
0.0467,
0.0407,
0.049,
0.0413,
0.0672,
0.0584,
0.0439,
0.0314,
0.0339,
0.0686,
0.1004,
0.093,
0.0964,
0.0533,
0.0543,
0.1486,
0.1132,
0.0631,
0.0857,
0.0993,
0.0998,
0.1336,
0.1489,
0.0938,
0.0551,
0.0163,
0.1133,
0.1231,
0.1447,
0.0901,
0.1245,
0.0821,
0.0275,
0.0638,
0.1134,
0.1203,
0.1152,
0.053,
0.0177,
0.0272,
0.0741,
0.0602,
0.1255,
0.1383,
0.1389,
0.0764,
0.1443,
0.2633,
0.217,
0.1207,
0.1229,
0.1661,
0.1616,
0.1508,
0.1853,
0.1794,
0.0662,
0.092,
0.0977,
0.1611,
0.2124,
0.2313,
0.2413,
0.1979,
0.2291,
0.141,
0.0611,
0.0501,
0.1636,
0.2085,
0.1822,
0.1751,
0.1826,
0.146,
0.1506,
0.1901,
0.1872,
0.2516,
0.2148,
0.0608,
0.0107,
0.0046,
0.0095,
0.1401,
0.137,
0.2326,
0.1778,
0.1175,
0.0906,
0.1684,
0.0764,
0.0661,
0.1443,
0.2182,
0.2329,
0.1343,
0.1245,
0.1601,
0.0424,
0.1131,
0.1526,
0.1471,
0.2201,
0.0837,
0.0219,
0.0184,
0.0283,
0.0707,
0.0568,
0.1535,
0.1418,
0.0761,
0.0978,
0.1187,
0.1157,
0.0118,
0.1953,
0.1875,
0.1764,
0.1547,
0.0503,
0.0163,
0.1988,
0.0944,
0.0612,
0.2249,
0.1348,
0.1977,
0.1814,
0.1971,
0.1614,
0.0506,
0.1409,
0.1509,
0.2249,
0.2094,
0.0661,
0.0116,
0.1591,
0.1435,
0.1473,
0.1874,
0.1649,
0.158,
0.0491,
0.1364,
0.0838,
0.0319,
0.0712,
0.1898,
0.189,
0.2367,
0.1944,
0.1262,
0.0717,
0.1679,
0.1636,
0.1394,
0.2406,
0.192,
0.2804,
0.1508,
0.2966,
0.2765,
0.0558,
0.1518,
0.1546,
0.33,
0.1576,
0.1049,
0.0577,
0.2637,
0.1414,
0.1697,
0.1525,
0.1384,
0.0956,
0.2638,
0.3267,
0.1552,
0.0732,
0.1722,
0.3975,
0.3164,
0.4405,
0.3117,
0.0889,
0.1124,
0.3677,
0.2851,
0.1852,
0.4721,
0.2198,
0.5172,
0.2646,
0.5098,
0.3223,
0.0909,
0.1443,
0.4768,
0.3913,
0.3005,
0.2303,
0.2485,
0.4733,
0.3076,
0.252,
0.1887,
0.1937,
0.1659,
0.4239,
0.2854,
0.1291,
0.0213,
0.1454,
0.3523,
0.282,
0.5343,
0.2692,
0.1728,
0.1685,
0.3614,
0.2693,
0.5056,
0.3616,
0.5388,
0.5142,
0.2181,
0.5264,
0.3545,
0.1213,
0.1566,
0.5142,
0.3722,
0.2601,
0.0689,
0.4555,
0.3753,
0.2508,
0.12,
0.1517,
0.1317,
0.0723,
0.4592,
0.2962,
0.2802,
0.1414,
0.4414,
0.3406,
0.2823,
0.4996,
0.1688,
0.0662,
0.201,
0.3693,
0.2242,
0.478,
0.3109,
0.5181,
0.3879,
0.5077,
0.4012,
0.4462,
0.1456,
0.1503,
0.4729,
0.3741,
0.1822,
0.046,
0.4565,
0.3521,
0.1921,
0.0767,
0.0033,
0.1292,
0.0236,
0.4356,
0.2611,
0.3491,
0.0775,
0.4053,
0.2745,
0.5644,
0.4664,
0.16,
0.0352,
0.0066,
0.5091,
0.2933,
0.7051,
0.3331,
0.7396,
0.3881,
0.7031,
0.5191,
0.4115,
0.0292,
0.1338,
0.6584,
0.3064,
0.1574,
0.0833,
0.6545,
0.3459,
0.2696,
0.0811,
0.1136,
0.0843,
0.1991,
0.5578,
0.2969,
0.3014,
0.0369,
0.4105,
0.2736,
0.5855,
0.3783,
0.1087,
0.0138,
0.4722,
0.3782,
0.2609,
0.6866,
0.296,
0.7435,
0.3218,
0.6931,
0.3894,
0.155,
0.0208,
0.1283,
0.4609,
0.4769,
0.4949,
0.3658,
0.3771,
0.5615,
0.6912,
0.818,
0.723,
0.6674,
0.0687,
0.0144,
0.5312,
0.4007,
0.191,
0.189,
0.3739,
0.37,
0.269,
0.4205,
0.332,
0.3763,
0.1771,
0.1317,
0.2573,
0.4658,
0.2312,
0.1872,
0.291,
0.5368,
0.2119,
0.2832,
0.2748,
0.5714,
0.2908,
0.4057,
0.2098,
0.4047,
0.3288,
0.1809,
0.4371,
0.5814,
0.3238,
0.2121,
0.3587,
0.6944,
0.4587,
0.5324,
0.5052,
0.6592,
0.4146,
0.4451,
0.3792,
0.6126,
0.4372,
0.0961,
0.5577,
0.5509,
0.5121,
0.37,
0.3093,
0.4158,
0.6064,
0.4047,
0.4792,
0.457,
0.6133,
0.1454,
0.3937,
0.1742,
0.4309,
0.4214,
0.5762,
0.5043,
0.6167,
0.305,
0.0272,
0.0042,
0.5758,
0.2231,
0.4051,
0.4119,
0.4757,
0.3234,
0.5353,
0.2758,
0.5129,
0.3391,
0.2264,
0.5456,
0.522,
0.4483,
0.4922,
0.3738,
0.6555,
0.4243,
0.4041,
0.4212,
0.6527,
0.5103,
0.3843,
0.4513,
0.3476,
0.387,
0.225,
0.4602,
0.3556,
0.5961,
0.2906,
0.2134,
0.3986,
0.6028,
0.473,
0.4881,
0.4768,
0.7168,
0.3001,
0.3973,
0.3154,
0.6435,
0.2178,
0.0363,
0.5206,
0.5563,
0.2909,
0.2705,
0.2102,
0.6345,
0.3662,
0.2239,
0.2248,
0.7563,
0.7218,
0.6154,
0.4954,
0.4907,
0.4621,
0.1972,
0.1956,
0.5307,
0.5841,
0.1774,
0.1741,
0.4723,
0.4478,
0.1687,
0.1404,
0.6484,
0.7348,
0.4376,
0.5623
],
"marks": [],
"settled": false,
"noCandidate": true,
"note": "NO CANDIDATE \u2014 same as #13. Ear-only."
}
]
}
\ No newline at end of file
/**
* End-to-end smoke for the boundary lab.
*
* Same discipline as judge-smoke: a green `tsc -b` proved nothing there — the
* transport was dead because `peaks` + `url` made wavesurfer think the clip was
* pre-decoded. So every assertion here is POSITIVE and phrased as what a human
* would see: markers drawn, the clock moving, the call changing.
*
* The two failures specific to THIS tool would both pass a compiler:
* - the /audio/bounds mount resolving to the wrong directory (a clip plays,
* but it is the wrong 90 seconds — inaudible to a test that only checks
* "audio played"), so we assert the clip's DURATION matches the ±pad window;
* - clip-time vs master-time confusion, which would silently shift every
* candidate — so we assert a marker click yields a call in MASTER seconds
* inside the boundary's own clip range.
*
* npx vite --port 5199 --strictPort &
* node scripts/bounds-smoke.mjs
*/
import { chromium } from 'playwright'
import { existsSync, readFileSync } from 'node:fs'
// NB: do not name this `URL` — it shadows the global used just below
const PAGE = process.env.BOUNDS_URL ?? 'http://localhost:5199/bounds.html'
const DATA = new URL('../public/bounds-opal-festival-2026.json', import.meta.url)
const fail = (m) => { console.error(`✗ ${m}`); process.exitCode = 1 }
const ok = (m) => console.log(`✓ ${m}`)
const doc = JSON.parse(readFileSync(DATA, 'utf8'))
const SYSTEM_CHROME = ['/usr/bin/chromium', '/usr/bin/chromium-browser', '/usr/bin/google-chrome-stable']
.find((p) => existsSync(p))
const b = await chromium.launch(SYSTEM_CHROME ? { executablePath: SYSTEM_CHROME } : {})
const p = await b.newPage({ viewport: { width: 1280, height: 1000 } })
const bad = []
p.on('console', (m) => m.type() === 'error' && bad.push(m.text()))
p.on('pageerror', (e) => bad.push(String(e)))
await p.goto(PAGE, { waitUntil: 'networkidle' })
// 1 — the boundaries loaded and every one is navigable
await p.waitForSelector('nav button', { timeout: 15000 })
const navs = await p.$$eval('nav button', (n) => n.map((x) => x.textContent))
navs.length === doc.boundaries.length
? ok(`${navs.length} boundaries in the nav (${navs.join(' ')})`)
: fail(`expected ${doc.boundaries.length} nav buttons, got ${navs.length}`)
// 2 — the waveform painted (canvas lives in wavesurfer's shadow root;
// Playwright selectors pierce it, document.querySelector does NOT)
await p.waitForSelector('canvas', { timeout: 20000 })
ok('waveform canvas rendered')
// 3 — the clip is the RIGHT clip. Two independent checks, because either alone
// lies: the mount could 404 (caught by the fetch) or fall through to the
// wrong directory and serve *a* file (caught by the displayed duration).
const first = doc.boundaries[0]
const head = await p.request.get(new global.URL(first.url, PAGE).href)
const bytes = Number(head.headers()['content-length'] ?? 0)
head.ok() && bytes > 500_000
? ok(`clip served: ${head.status()} ${(bytes / 1e6).toFixed(1)} MB`)
: fail(`clip fetch ${head.status()} ${bytes} B — /audio/bounds mount wrong?`)
const want = first.clipEnd - first.clipStart
const shownDur = await p.textContent('.tnum span.w-14, span.w-14')
const mm = /(\d+):(\d+)/.exec(shownDur ?? '')
const shownS = mm ? Number(mm[1]) * 60 + Number(mm[2]) : -1
Math.abs(shownS - want) <= 1
? ok(`transport shows ${shownDur?.trim()} = the ±${doc.pad}s window`)
: fail(`transport shows ${shownDur} (${shownS}s), expected ${want}s`)
// 4 — markers are drawn, one per candidate plus the nominal
const wantMarks = first.marks.length + 1
const marks = await p.$$eval('[part="region-content"], .wavesurfer-region', (n) => n.length)
marks >= wantMarks
? ok(`${marks} markers drawn (>= ${wantMarks} expected)`)
: fail(`expected >= ${wantMarks} markers, saw ${marks}`)
// 5 — clicking a candidate button records a call IN MASTER TIME, inside the clip
await p.getByRole('button', { name: /^takeover · / }).first().click()
await p.waitForTimeout(400)
const callTxt = await p.textContent('section:has(textarea) .tnum')
const secs = Number(/\(([\d.]+)s/.exec(callTxt ?? '')?.[1])
Number.isFinite(secs) && secs >= first.clipStart && secs <= first.clipEnd
? ok(`call recorded at ${secs}s master (inside [${first.clipStart}, ${first.clipEnd}])`)
: fail(`call "${callTxt}" is not a master time inside this clip`)
Math.abs(secs - first.marks.find((m) => m.key === 'takeover').t) < 0.01
? ok('call equals the takeover candidate exactly — no clip/master drift')
: fail(`call ${secs} != takeover ${first.marks.find((m) => m.key === 'takeover').t}`)
// 6 — audio actually PLAYS. The media element is owned by wavesurfer and never
// enters the DOM, so querySelector('audio') is null by construction — that
// probe would "pass" forever on a dead transport. Assert what a HUMAN sees:
// the transport clock advances and the button offers Pause.
await p.waitForTimeout(1500)
const clock = async () => {
const txt = await p.textContent('[data-testid="transport-clock"]')
const m = /(\d+):(\d+)/.exec(txt ?? '')
return m ? Number(m[1]) * 60 + Number(m[2]) : -1
}
const t1 = await clock()
await p.waitForTimeout(1600)
const t2 = await clock()
t2 > t1
? ok(`clock advanced ${t1}s -> ${t2}s (playing)`)
: fail(`clock stuck at ${t1}s — transport never started`)
const pauseVisible = await p.getByRole('button', { name: 'Pause' }).count()
pauseVisible > 0 ? ok('button offers Pause') : fail('button never flipped to Pause')
// 7 — a no-candidate boundary tells the user it is theirs, rather than showing nothing
const blindIdx = doc.boundaries.findIndex((x) => x.noCandidate)
if (blindIdx >= 0) {
await p.$$eval('nav button', (n, i) => n[i].click(), blindIdx)
await p.waitForTimeout(500)
const warn = (await p.textContent('body')) ?? ''
warn.includes('No machine candidate exists')
? ok(`#${doc.boundaries[blindIdx].track} shows the ear-only notice`)
: fail('no-candidate boundary renders without its notice')
}
// 8 — no console errors anywhere in that flow
bad.length === 0 ? ok('no console errors') : fail(`console errors: ${bad.slice(0, 3).join(' | ')}`)
await b.close()
console.log(process.exitCode ? '\nSMOKE FAILED' : '\nsmoke passed')
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import '@fontsource-variable/geist'
import '@fontsource-variable/geist-mono'
import './index.css'
import { BoundaryLab } from './bounds/BoundaryLab.tsx'
const gig = new URLSearchParams(location.search).get('set') ?? 'opal-festival-2026'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<BoundaryLab gig={gig} />
</StrictMode>,
)
import { useCallback, useEffect, useMemo, useState } from 'react'
import { Download, Check, HelpCircle, AlertTriangle } from 'lucide-react'
import { WaveformPlayer } from '@/components/WaveformPlayer'
import { mmss } from '@/lib/format'
/**
* Boundary lab — decide exactly where one track becomes the next.
*
* The set-judge answers "is this a keeper"; this answers "where is the cut",
* which turned out to be the harder question. Three automated attempts failed
* (see cut_lens3.py): on transitions where both tracks share their sounds, a
* timbral lens is blind by construction, so some cuts are ear-only by nature.
* The tool's whole job is therefore to make one ear call cost seconds.
*
* Times: the clip is a ±45 s excerpt, so everything on screen is CLIP time and
* everything exported is MASTER time. The conversion lives in exactly two
* places (`toClip`, `toMaster`) because doing it ad hoc is how every marker
* ends up silently shifted.
*/
interface Mark { key: string; label: string; t: number }
interface Boundary {
track: number
title: string
fromTitle: string
nominal: number
clipStart: number
clipEnd: number
url: string
peaks: number[]
marks: Mark[]
settled: boolean
noCandidate: boolean
note: string
}
interface BoundsDoc { gig: string; title: string; pad: number; boundaries: Boundary[] }
const MARK_COLOR: Record<string, string> = {
takeover: 'rgba(217, 0, 255, 0.9)', // brand magenta — the one to try first
mid: 'rgba(120, 170, 255, 0.75)',
onset: 'rgba(120, 170, 255, 0.45)',
ear: 'rgba(80, 220, 140, 0.9)',
alt: 'rgba(80, 220, 140, 0.5)',
nominal: 'rgba(255, 255, 255, 0.35)',
}
type Call = { t: number | null; comment: string; source: string }
export function BoundaryLab({ gig }: { gig: string }) {
const [doc, setDoc] = useState<BoundsDoc | null>(null)
const [err, setErr] = useState<string | null>(null)
const [i, setI] = useState(0)
const [now, setNow] = useState(0)
const [seek, setSeek] = useState<{ t: number; n: number } | undefined>()
const [calls, setCalls] = useState<Record<number, Call>>(() => {
try { return JSON.parse(localStorage.getItem(`bounds:${gig}`) || '{}') } catch { return {} }
})
useEffect(() => {
fetch(`/bounds-${gig}.json`)
.then((r) => (r.ok ? r.json() : Promise.reject(new Error(`${r.status} ${r.statusText}`))))
.then(setDoc)
.catch((e) => setErr(String(e)))
}, [gig])
useEffect(() => {
localStorage.setItem(`bounds:${gig}`, JSON.stringify(calls))
}, [calls, gig])
const b = doc?.boundaries[i]
const toClip = useCallback((t: number) => (b ? t - b.clipStart : 0), [b])
const toMaster = useCallback((t: number) => (b ? t + b.clipStart : 0), [b])
const markers = useMemo(() => {
if (!b) return []
const all = [...b.marks, { key: 'nominal', label: 'nominal', t: b.nominal }]
return all.map((m) => ({
t: toClip(m.t),
label: m.label,
color: MARK_COLOR[m.key] ?? MARK_COLOR.nominal,
}))
}, [b, toClip])
const jump = useCallback((tMaster: number, lead = 4) => {
// land a few seconds BEFORE the candidate: a cut is judged by what leads
// into it, and starting exactly on it gives you nothing to judge against.
setSeek({ t: Math.max(0, toClip(tMaster) - lead), n: Date.now() })
}, [toClip])
const setCall = useCallback((t: number | null, source: string) => {
if (!b) return
setCalls((c) => ({ ...c, [b.track]: { t, source, comment: c[b.track]?.comment ?? '' } }))
}, [b])
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return
if (!doc) return
if (e.key === 'j') setI((v) => Math.min(doc.boundaries.length - 1, v + 1))
else if (e.key === 'k') setI((v) => Math.max(0, v - 1))
else if (e.key === 'Enter' && b) setCall(toMaster(now), 'playhead')
}
window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey)
}, [doc, b, now, toMaster, setCall])
const download = () => {
if (!doc) return
const out = {
gig: doc.gig,
_comment: [
'Ear calls from the boundary lab. Times are absolute MASTER seconds.',
'These are PLN\'s answers — merge them into the `verified` block of',
'judge_specs/<gig>_boundaries_ear.json, which is the ground truth.',
],
decided: Object.fromEntries(
Object.entries(calls)
.filter(([, c]) => c.t !== null)
.map(([k, c]) => [k, { start: Number(c.t!.toFixed(2)), source: c.source, note: c.comment }]),
),
skipped: Object.entries(calls).filter(([, c]) => c.t === null).map(([k]) => Number(k)),
}
const url = URL.createObjectURL(new Blob([JSON.stringify(out, null, 1)], { type: 'application/json' }))
const a = document.createElement('a')
a.href = url
a.download = `boundaries-${doc.gig}.json`
a.click()
URL.revokeObjectURL(url)
}
if (err) return <p className="p-6 font-mono text-sm text-red-400">bounds data not loaded: {err}</p>
if (!doc || !b) return <p className="p-6 font-mono text-sm text-ink-faint">loading…</p>
const decided = Object.values(calls).filter((c) => c.t !== null).length
const call = calls[b.track]
return (
<div className="mx-auto max-w-4xl px-4 pb-24">
<header className="sticky top-0 z-20 -mx-4 flex flex-col gap-3 border-b border-hairline
bg-surface/95 px-4 pb-3 pt-5 backdrop-blur-sm">
<div className="flex items-baseline justify-between gap-4">
<h1 className="text-lg font-semibold">
Boundary lab <span className="text-ink-faint">· {doc.title}</span>
</h1>
<div className="flex items-center gap-3 font-mono text-xs text-ink-muted tnum">
<span>{decided}/{doc.boundaries.length} decided</span>
<button
onClick={download}
className="flex items-center gap-1 rounded bg-ink px-2 py-1 text-surface hover:opacity-90"
>
<Download size={12} /> boundaries.json
</button>
</div>
</div>
<nav className="flex flex-wrap gap-1">
{doc.boundaries.map((x, n) => (
<button
key={x.track}
onClick={() => setI(n)}
className={`rounded px-2 py-0.5 font-mono text-xs transition-colors ${
n === i ? 'bg-magenta text-white'
: calls[x.track]?.t != null ? 'bg-emerald-500/20 text-emerald-300'
: 'text-ink-faint hover:text-ink'
}`}
>
#{x.track}
</button>
))}
</nav>
</header>
<section className="mt-5">
<p className="font-mono text-xs text-ink-faint">
{b.fromTitle} <span className="text-magenta"></span> {b.title}
</p>
<h2 className="text-xl font-semibold">#{b.track} {b.title}</h2>
{b.noCandidate && (
<p className="mt-2 flex items-start gap-2 rounded border border-amber-500/30 bg-amber-500/10
px-3 py-2 text-xs text-amber-200">
<AlertTriangle size={14} className="mt-0.5 shrink-0" />
<span>
No machine candidate exists for this cut, and none ever will from the timbral
lens — both tracks share their sounds across the switch, so it is blind here by
construction. This one is yours: scrub, then press <kbd>Enter</kbd> at the moment
the incoming track takes over.
</span>
</p>
)}
{b.note && <p className="mt-2 text-xs text-ink-muted">{b.note}</p>}
</section>
<section className="mt-4">
<WaveformPlayer
url={b.url}
dur={b.clipEnd - b.clipStart}
peaks={b.peaks}
markers={markers}
seek={seek}
onTime={setNow}
/>
</section>
<section className="mt-4 flex flex-wrap items-center gap-2">
{[...b.marks, { key: 'nominal', label: 'nominal', t: b.nominal }].map((m) => (
<button
key={m.key}
onClick={() => { jump(m.t); setCall(m.t, m.key) }}
className={`rounded border px-2.5 py-1 font-mono text-xs transition-colors ${
call?.source === m.key
? 'border-magenta bg-magenta/15 text-magenta'
: 'border-hairline text-ink-muted hover:text-ink'
}`}
title="Audition from 4 s before, and take it as the call"
>
{m.label} · {mmss(m.t)}
</button>
))}
<span className="mx-1 text-hairline">|</span>
<button
onClick={() => setCall(toMaster(now), 'playhead')}
className="rounded border border-hairline px-2.5 py-1 font-mono text-xs
text-ink-muted hover:text-ink"
>
<Check size={12} className="mr-1 inline" /> use playhead · {mmss(toMaster(now))}
</button>
<button
onClick={() => setCall(null, 'skip')}
className="rounded border border-hairline px-2.5 py-1 font-mono text-xs
text-ink-faint hover:text-ink"
>
<HelpCircle size={12} className="mr-1 inline" /> undecided
</button>
</section>
<section className="mt-4">
<div className="flex items-baseline gap-3">
<span className="font-mono text-xs text-ink-faint">call</span>
<span className="font-mono text-lg tnum">
{call?.t != null
? <span className="text-emerald-300">{mmss(call.t)} <span className="text-xs text-ink-faint">({call.t.toFixed(2)}s · {call.source})</span></span>
: <span className="text-ink-faint"></span>}
</span>
</div>
<textarea
value={call?.comment ?? ''}
onChange={(e) =>
setCalls((c) => ({
...c,
[b.track]: { t: c[b.track]?.t ?? null, source: c[b.track]?.source ?? 'comment', comment: e.target.value },
}))
}
placeholder="what you heard — feeds performance_notes.md"
className="mt-2 h-16 w-full resize-y rounded border border-hairline bg-transparent
px-2 py-1.5 text-sm outline-none focus:border-magenta/60"
/>
</section>
<p className="mt-6 font-mono text-[10px] text-ink-faint/70">
j/k next-prev · space play/pause · Enter = take the playhead · drag to loop a span ·
clip is ±{doc.pad}s around the current cut, all exported times are master time
</p>
</div>
)
}
......@@ -20,8 +20,23 @@ interface Props {
onFinish?: () => void
/** start playing as soon as the transport is ready (auto-advance landing) */
autoPlay?: boolean
/**
* Fixed labelled points, in CLIP seconds — candidate cut times, the ear's
* answer, the nominal. Drawn as zero-width regions rather than absolutely
* positioned divs on purpose: the waveform zooms and scrolls, and an overlay
* positioned in page pixels drifts off the audio the moment it does.
*/
markers?: { t: number; label: string; color: string }[]
/**
* Imperative seek. `n` is a nonce: clicking the same marker twice must seek
* twice, which a bare `t` cannot express because the value doesn't change.
*/
seek?: { t: number; n: number }
}
/** marker regions are identified by id prefix so drag-selection never eats them */
const MARK_ID = 'mark-'
const MIN_ZOOM = 0 // 0 = fit-to-width
const MAX_ZOOM = 600 // px per second
const ZOOM_STEP = 1.6
......@@ -32,7 +47,7 @@ const ZOOM_STEP = 1.6
* on the waveform, toggle Repeat to loop it (great for auditing a transition or
* a single moment while iterating on the sound). Space = play/pause.
*/
export function WaveformPlayer({ url, dur, peaks, onTime, onFinish, autoPlay }: Props) {
export function WaveformPlayer({ url, dur, peaks, onTime, onFinish, autoPlay, markers, seek }: Props) {
const elRef = useRef<HTMLDivElement>(null)
const wsRef = useRef<WaveSurfer | null>(null)
const regionsRef = useRef<ReturnType<typeof RegionsPlugin.create> | null>(null)
......@@ -43,6 +58,8 @@ export function WaveformPlayer({ url, dur, peaks, onTime, onFinish, autoPlay }:
onFinishRef.current = onFinish
const autoPlayRef = useRef(autoPlay)
autoPlayRef.current = autoPlay
const markersRef = useRef(markers)
markersRef.current = markers
const loopRef = useRef(true)
const [playing, setPlaying] = useState(false)
......@@ -100,6 +117,17 @@ export function WaveformPlayer({ url, dur, peaks, onTime, onFinish, autoPlay }:
}
ws.on('ready', () => {
setReady(true)
for (const m of markersRef.current ?? []) {
if (m.t < 0 || m.t > dur) continue // outside this clip: don't fake it
regions.addRegion({
id: MARK_ID + m.label,
start: m.t,
content: m.label,
color: m.color,
drag: false,
resize: false,
})
}
// Auto-advance lands here. Browsers only allow this because the user
// already gestured on the previous track; if they haven't, play()
// rejects and we simply stay paused rather than throwing.
......@@ -120,12 +148,17 @@ export function WaveformPlayer({ url, dur, peaks, onTime, onFinish, autoPlay }:
// single drag-selection that can be looped
regions.enableDragSelection({ color: 'rgba(217, 0, 255, 0.12)' })
regions.on('region-created', (r) => {
for (const other of regions.getRegions()) if (other.id !== r.id) other.remove()
if (r.id.startsWith(MARK_ID)) return // a marker, not a user selection
for (const other of regions.getRegions())
if (other.id !== r.id && !other.id.startsWith(MARK_ID)) other.remove()
activeRegionRef.current = r
setRegion({ start: r.start, end: r.end })
})
regions.on('region-updated', (r) => setRegion({ start: r.start, end: r.end }))
regions.on('region-updated', (r) => {
if (!r.id.startsWith(MARK_ID)) setRegion({ start: r.start, end: r.end })
})
regions.on('region-in', (r) => {
if (r.id.startsWith(MARK_ID)) return
activeRegionRef.current = r
})
regions.on('region-out', (r) => {
......@@ -133,6 +166,13 @@ export function WaveformPlayer({ url, dur, peaks, onTime, onFinish, autoPlay }:
})
regions.on('region-clicked', (r, e) => {
e.stopPropagation()
// Clicking a marker jumps to it. It has zero width, so `play(true)` would
// start and immediately hit region-out; seek explicitly instead.
if (r.id.startsWith(MARK_ID)) {
ws.setTime(r.start)
ws.play().catch(() => {})
return
}
activeRegionRef.current = r
r.play(true)
})
......@@ -147,11 +187,30 @@ export function WaveformPlayer({ url, dur, peaks, onTime, onFinish, autoPlay }:
}
}, [url, peaks, dur])
// apply zoom when it changes (and once ready)
// Apply zoom when it changes (and once ready).
//
// `zoom()` needs a DECODED buffer to rescale, and on the peaks path there
// isn't one — wavesurfer throws "No audio loaded", the same message the
// peaks+url trap produced but from a completely different cause. It fires
// from an effect, so it escapes as an unhandled React error and blanks the
// whole page. Zoom is a convenience; never let it take the tool down.
useEffect(() => {
if (ready && wsRef.current) wsRef.current.zoom(zoom)
if (!ready || !wsRef.current || zoom <= MIN_ZOOM) return
try {
wsRef.current.zoom(zoom)
} catch {
/* peaks-only render: nothing to rescale, keep the fit-to-width view */
}
}, [zoom, ready])
// imperative seek from the parent (marker buttons / keyboard)
useEffect(() => {
const ws = wsRef.current
if (!ready || !ws || !seek) return
ws.setTime(Math.max(0, Math.min(dur, seek.t)))
ws.play().catch(() => {})
}, [seek?.n, ready]) // eslint-disable-line react-hooks/exhaustive-deps
const toggle = () => wsRef.current?.playPause()
const zoomBy = (factor: number) =>
setZoom((z) => {
......@@ -159,7 +218,9 @@ export function WaveformPlayer({ url, dur, peaks, onTime, onFinish, autoPlay }:
return Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, factor > 1 ? base * factor : z <= 40 ? 0 : z / ZOOM_STEP))
})
const clearRegion = () => {
regionsRef.current?.clearRegions()
// clear the SELECTION only — clearRegions() would wipe the markers too
for (const r of regionsRef.current?.getRegions() ?? [])
if (!r.id.startsWith(MARK_ID)) r.remove()
activeRegionRef.current = null
setRegion(null)
}
......@@ -202,7 +263,10 @@ export function WaveformPlayer({ url, dur, peaks, onTime, onFinish, autoPlay }:
</div>
<div className="mt-1 flex items-center justify-between gap-2 font-mono text-xs text-ink-muted tnum">
<span>{ready ? mmss(t) : 'loading…'}</span>
{/* testid: the smoke test reads the CLOCK to prove playback; several
other elements share `tnum`, and a positional selector silently
picked the header's counter instead. */}
<span data-testid="transport-clock">{ready ? mmss(t) : 'loading…'}</span>
<div className="flex items-center gap-1">
{region && (
......
......@@ -74,6 +74,8 @@ export default defineConfig({
main: path.resolve(__dirname, 'index.html'),
sextant: path.resolve(__dirname, 'sextant.html'),
judge: path.resolve(__dirname, 'judge.html'),
// and the boundary lab — where exactly does one track become the next
bounds: path.resolve(__dirname, 'bounds.html'),
},
},
},
......
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