Commit 708c2aef by PLN (Algolia)

fix(probe): a decay crosses its midpoint ONCE — stop calling gated risers dying

probe-chain reported piment_bresilien's d10 as "DECAYING, almost certainly an
xfade tail" in two consecutive runs. It is nothing of the kind: d10 is a `mask
"<t f!7>"` riser — one cycle sounding, seven silent — and the sparkline showed it
plainly, dipping and coming back (|▇▇▇▇▇▅▃▇▇▇▆▄|). The classifier was reading
head-quartile vs tail-quartile, which is a monotonicity question answered with two
averages: exactly the missing-information mistake that made an aggregate unable to
tell a fade from a sparse part, one level up.

First attempt asked "does it recover after its lowest bin?" — which fails when the
dip lands at the END of the window, since there is no 'after' to recover into, and
that is precisely the real d10's shape. The right evidence is REPETITION: count
how many times the level crosses its own midpoint downward. **A monotonic fade
crosses once, by definition. Two or more is a cycle, and a cycle is a part.**

Validated against seven synthetic shapes before trusting it on real audio, and
they now separate cleanly: gated-riser-with-a-dip-at-the-end -> GATED/SPARSE,
spiky-sparse -> GATED/SPARSE (this one was misclassified as DECAYING even in my
own self-test and shipped that way), monotonic decay -> DECAYING, decay after a
steady head -> DECAYING, silent tail -> DIED, plus STEADY and RISING.

Why it matters more than the numbers suggest: a tool that cries wolf on a healthy
riser gets ignored on the night it is right. J-7 is the wrong week to teach PLN to
distrust the instrument.

Also lands tools/check-tracks.sh — the empirical pre-gig gate. There is no static
typechecker for a .tidal file (it is a GHCi fragment, not a module), so the only
honest question is "evaluate it and does sound come out". Three times this week a
silent rig was really a compile error, and each time the damage was TOTAL rather
than partial because a blank line is Tidal's block separator and these tracks have
almost none. All-orbits-silent is therefore diagnostic: the track did not compile.
Run it the day before a gig and after ANY BootTidal.hs edit, since a broken helper
silences the whole corpus at once. It distinguishes Tidal faults from Ardour faders
and refuses to let a mixer problem read as a track bug.
parent 60de8a2d
#!/usr/bin/env zsh
# check-tracks — boot every track in the setlist and PROVE each declared orbit sounds.
#
# Why this exists (2026-07-28, J-7 to OPAL)
# -----------------------------------------
# Three separate times this week "the rig is silent" turned out to be "the code
# did not compile":
# * a `crackles` that was never defined (vague_de_crime)
# * a DJF helper written with nested `#` (BootTidal.hs, took EVERY helper)
# * `|+ note 12 . (|* gain 0.6)` (desire.tidal, took all 9 orbits)
# In each case the file looked fine, the rig looked healthy, and nothing said
# "error" out loud. And each time the damage was TOTAL rather than partial,
# because a blank line is Tidal's block separator and these tracks have almost
# none — desire.tidal and the_revolution have ZERO, so the whole file is ONE
# block, and a block either compiles entirely or not at all.
#
# There is no static typechecker for a .tidal file (it is a fragment of a GHCi
# session, not a module), so the only honest gate is EMPIRICAL: evaluate the
# track and measure whether sound comes out. A track whose every orbit reads -inf
# did not compile. That makes this both a smoke test and a compile check.
#
# Run it the day before the gig, and after ANY edit to BootTidal.hs — a broken
# helper silences the entire corpus at once, which is the worst failure this rig
# has.
#
# Usage
# -----
# tools/check-tracks.sh # the whole OPAL setlist
# tools/check-tracks.sh live/techno/foo.tidal # just these
# KEEP=1 tools/check-tracks.sh # keep the full probe output
#
# Exit 0 = every declared orbit of every track made sound. 1 = at least one did
# not. The per-track detail lands in a log whose path is printed at the end.
#
# TIMING IS NOT NEGOTIABLE: one track at a time, >=20s settle. A tight loop
# rebooting every ~45s lands evals on a half-loaded interpreter (BootTidal.hs
# needs its 6s) and reports false failures — burned 2026-07-28.
#
# CAVEAT, stated plainly: this measures the orbit at SuperCollider AND after
# Ardour's fader. A "SILENT AT SOURCE" verdict is Tidal's fault; an "Ardour eats
# it" verdict is a fader/mute and is NOT a track bug. Read the log, don't just
# read the exit code.
set -u
cd "${0:A:h}/.." || exit 2
SETLIST=${SETLIST:-setlist_opal2026.txt}
LOG=$(mktemp -t check-tracks-XXXXXX.log)
if (( $# )); then
TRACKS=("$@")
else
TRACKS=(${(f)"$(grep -vE '^[[:space:]]*(#|$)' $SETLIST)"})
fi
print "check-tracks: ${#TRACKS} track(s), one at a time — this takes ~45s each\n"
fails=0
for T in $TRACKS; do
if [[ ! -f $T ]]; then
print " ${T:t:r}: FAIL — no such file"
(( fails++ )); continue
fi
# Declared orbits, from the track itself. NB: zsh does NOT word-split unquoted
# variables, so ${=ORB} is mandatory below — without it probe-chain gets ONE
# bogus argument, exits 2 on a usage error, and a grep'd loop shows empty
# results that read exactly like "nothing is playing" (burned 2026-07-28).
ORB=$(grep -oE '^[[:space:]]*d[0-9]+' $T | tr -d ' d' | sort -un | tr '\n' ' ')
print "########## $T (orbits: $ORB)" >>$LOG
python3 tools/tidal-remote.py boot $T >>$LOG 2>&1
sleep 20
OUT=$(python3 tools/probe-chain.py -d ${=ORB} -s 18 -t 1.5 2>&1)
print -r -- "$OUT" >>$LOG
# An orbit passes if it is not silent at SOURCE. DIED/DECAYING is called out
# separately: it means the eval did not take and what you hear is the xfade
# tail of the OUTGOING pattern (dN == xfade N).
n=$(print -r -- "$OUT" | grep -cE '^ d[0-9]+ ')
silent=$(print -r -- "$OUT" | grep -c 'SILENT AT SOURCE')
dying=$(print -r -- "$OUT" | grep -cE 'DIED|DECAYING')
eaten=$(print -r -- "$OUT" | grep -c 'Ardour eats it')
alive=$(( n - silent ))
note=""
(( dying )) && note+=" ${dying} DIED/DECAYING (eval may not have taken)"
(( eaten )) && note+=" ${eaten} eaten by an Ardour fader (mixer, not Tidal)"
if (( silent == n && n > 0 )); then
print " ${T:t:r}: FAIL — ALL $n orbits silent at source. This track almost"
print " certainly DID NOT COMPILE (one block, one error). Check the"
print " Pulsar notification for EVAL ERROR."
(( fails++ ))
elif (( silent )); then
print " ${T:t:r}: FAIL — $alive/$n orbits alive, $silent silent at source.$note"
(( fails++ ))
else
print " ${T:t:r}: ok — $n/$n orbits alive.$note"
fi
done
python3 tools/tidal-remote.py hush >/dev/null 2>&1
print "\ncheck-tracks: detail in $LOG"
if (( fails )); then
print "check-tracks: FAIL — $fails track(s) would not play as written." >&2
exit 1
fi
print "check-tracks: OK — every declared orbit of every track made sound."
...@@ -238,6 +238,11 @@ def sparkline(series: list[float], floor: float = -80.0) -> str: ...@@ -238,6 +238,11 @@ def sparkline(series: list[float], floor: float = -80.0) -> str:
return "".join(out) return "".join(out)
def _lvl(db: float) -> float:
"""-inf as a comparable number, so min()/max() over a series with silence works."""
return -120.0 if db == -math.inf else db
def shape_of(series: list[float], bin_s: float) -> str: def shape_of(series: list[float], bin_s: float) -> str:
"""Classify the level's behaviour over time. """Classify the level's behaviour over time.
...@@ -263,9 +268,37 @@ def shape_of(series: list[float], bin_s: float) -> str: ...@@ -263,9 +268,37 @@ def shape_of(series: list[float], bin_s: float) -> str:
f"dying; the new one is silent (eval did not take)") f"dying; the new one is silent (eval did not take)")
tail_avg = sum(tail) / len(tail) tail_avg = sum(tail) / len(tail)
drop = head_avg - tail_avg drop = head_avg - tail_avg
# GATED before DECAYING. A decay only ever goes DOWN; a gated or sparse part
# goes down and COMES BACK. Comparing head-quartile to tail-quartile cannot
# tell them apart, and on 2026-07-28 it cried wolf: piment_bresilien's d10 is
# a `mask "<t f!7>"` riser — one cycle on, seven off — and it was reported
# "DECAYING, almost certainly an xfade tail" in two consecutive runs. That is
# the same missing-information mistake as judging a fade by an aggregate, one
# level up: a monotonicity question answered with two averages.
#
# So ask the question directly: after the level's lowest point, does it ever
# recover most of the way back? If yes it is not decaying, whatever the
# quartiles say. A tool that cries wolf on a healthy riser gets ignored on the
# night it is right.
# Evidence for "gated" is REPETITION, not the position of the single lowest
# bin: if the dip happens to land at the end of the window there is no
# "after" to recover into, and a real gated riser gets called a decay anyway.
# So count how many times the level crosses its own midpoint downward. A
# monotonic fade crosses ONCE, by definition. Two or more crossings is a
# cycle, and a cycle is a part.
if drop > 12 and len(live) >= 4:
lvls = [_lvl(d) for d in series]
mid = (max(lvls) + min(lvls)) / 2.0
crossings = sum(1 for a, b in zip(lvls, lvls[1:]) if a > mid >= b)
if crossings >= 2:
return (f"GATED/SPARSE — {crossings} dips below its own midpoint "
f"({min(lvls):+.1f} to {max(lvls):+.1f} dB), so it RECURS rather than "
f"fades: a masked/gated part (e.g. mask \"<t f!7>\") or a sparse one. "
f"A decay crosses once, by definition")
if drop > 12: if drop > 12:
return (f"DECAYING — {head_avg:+.1f} -> {tail_avg:+.1f} dB ({drop:.0f} dB down). " return (f"DECAYING — {head_avg:+.1f} -> {tail_avg:+.1f} dB ({drop:.0f} dB down), "
f"Almost certainly an xfade tail, not a part") f"never recovering. Almost certainly an xfade tail, not a part")
if drop < -12: if drop < -12:
return f"RISING — {head_avg:+.1f} -> {tail_avg:+.1f} dB (fading IN)" return f"RISING — {head_avg:+.1f} -> {tail_avg:+.1f} dB (fading IN)"
return f"STEADY — {head_avg:+.1f} -> {tail_avg:+.1f} dB (a real, sustained part)" return f"STEADY — {head_avg:+.1f} -> {tail_avg:+.1f} dB (a real, sustained part)"
......
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