Commit c35c9020 by PLN (Algolia)

feat(gig): auto-launch is now perf-aware + preloads the set's samples

Two live-play problems, both auto-handled by gig-up.sh so nothing lives in
PLN's head at gig time:

1. MIDI-burst CRACKS — root cause was NOT samples or RT priority (rtprio 95 is
   fine): the CPU was capped at max_perf_pct=55 (the quiet/cool desk regime),
   so a control sweep's CPU burst starved the audio thread -> xrun. gig-up now
   ASSERTS Standard (uncapped, governor performance) on launch via
   perf.set_mode('standard') — which also writes the desired-state file so the
   Bridge watcher holds it. Revert to Cool in the tray when done; skip with
   GIG_PERF=off. Confirmed live: toggling to Standard killed the cracks.

2. LAZY-LOAD cracks — gen_preload() runs setlist_samples.py --last N --emit-sc
   into preload.scd before boot; start_and_midi.scd loads it after
   SCLOrkSynths.load, warming just the recent set's samples (last 15 tracks =
   55 folders vs the whole library). setlist_samples.py also gained bare-name
   track resolution (find under live/). preload.scd + gig-sclang.log gitignored.
parent a4a76e03
......@@ -18,3 +18,7 @@ __pycache__/
# Local archives & bulky derived assets (kept out of git; back up via fbk)
*.tar.bz2
# Generated at launch by gig-up.sh (set-specific sample preload + sclang log)
preload.scd
gig-sclang.log
......@@ -19,6 +19,7 @@ set -u
DIR="$(cd "$(dirname "$0")" && pwd)" # repo root (this script lives here)
ARDOUR_SESSION="$HOME/Work/Sound/Ardour/Tidal Live/Tidal Live.ardour" # lean per-gig session (Tidal Multi = fat archive, retiring)
READY_TIMEOUT="${READY_TIMEOUT:-120}" # s to wait for SuperDirt (samples can be slow)
PRELOAD_TRACKS="${PRELOAD_TRACKS:-20}" # warm samples from the last N edited tracks (0 = off)
C_OK=$'\e[32m'; C_WARN=$'\e[33m'; C_INFO=$'\e[36m'; C_DIM=$'\e[2m'; C_RST=$'\e[0m'
ok(){ printf '%s✓ %s%s\n' "$C_OK" "$*" "$C_RST"; }
......@@ -35,6 +36,40 @@ midi_claimed(){ aconnect -l 2>/dev/null | grep -qiE "client [0-9]+: 'SuperCollid
ardour_up(){ pgrep -f 'ardour-[0-9]' >/dev/null 2>&1; }
pulsar_up(){ pgrep -x pulsar >/dev/null 2>&1; }
# --- perf-aware gear: performing needs CPU HEADROOM. A capped CPU (low
# max_perf_pct, the quiet/cool desk regime) can't absorb a MIDI-sweep burst →
# audio thread starves → xrun/crack. So launching the rig ASSERTS Standard
# (uncapped, governor performance) and lets the Bridge watcher hold it. Revert
# to Cool in the tray when you're done. RT priority is already fine (rtprio 95).
# Skip with GIG_PERF=off. ---
ensure_perf(){
[ "${GIG_PERF:-standard}" = off ] && { info "perf: auto-assert disabled (GIG_PERF=off)."; return; }
local maxp gov
maxp=$(cat /sys/devices/system/cpu/intel_pstate/max_perf_pct 2>/dev/null || echo 100)
gov=$(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor 2>/dev/null)
if [ "${maxp:-100}" -ge 100 ] 2>/dev/null && [ "$gov" = performance ]; then
ok "perf: CPU uncapped + performance — headroom for MIDI bursts."; return
fi
info "perf: CPU capped at ${maxp}% ('$gov') → asserting Standard for the set…"
# set_mode writes the desired-state file too, so the watcher won't revert it.
if PYTHONPATH="$DIR/tools/bridge" python3 -c "import perf,sys; ok,m=perf.set_mode('standard'); print(m); sys.exit(0 if ok else 1)" 2>/dev/null; then
ok "perf: Standard set + held by the watcher (fans may rise under load — fine on stage; tray → Cool to revert)."
else
warn "perf: auto-set failed — switch to STANDARD in the perf tray/Bridge yourself."
fi
}
# --- generate the set-specific sample preload the boot will warm ---
gen_preload(){
[ "${PRELOAD_TRACKS}" = 0 ] && { info "preload: disabled (PRELOAD_TRACKS=0)."; return; }
if python3 "$DIR/tools/setlist_samples.py" --last "$PRELOAD_TRACKS" --emit-sc > "$DIR/preload.scd" 2>/dev/null; then
ok "preload: $(grep -c loadSoundFiles "$DIR/preload.scd" 2>/dev/null || echo 0) sample folders queued from your last $PRELOAD_TRACKS tracks."
else
warn "preload: generation failed (python3 / tool?) — samples will lazy-load."
rm -f "$DIR/preload.scd"
fi
}
# --- terminal for the sclang post window ---
pick_term(){
if [ -n "${TERMINAL:-}" ] && command -v "$TERMINAL" >/dev/null 2>&1; then echo "$TERMINAL"; return; fi
......@@ -73,11 +108,13 @@ launch_bin(){ # launch_bin "<label>" <binary names...> — first found, detache
# ---------------------------------------------------------------------------
echo
info "gig-up — ordered launch from $DIR"
ensure_perf
# 1) SuperDirt (skip if already running)
if scsynth_up; then
ok "SuperDirt already running — skipping boot."
ok "SuperDirt already running — skipping boot (preload only applies to a fresh boot)."
else
gen_preload # write preload.scd so start_and_midi.scd warms the set's samples
info "starting SuperDirt (sclang start_and_midi.scd, headless)…"
open_term
fi
......
......@@ -132,6 +132,16 @@ s.waitForBoot {
// Setup SCLOrK Synths (installed via Quark)
SCLOrkSynths.load;
// Set-specific sample PRELOAD — generated by gig-up.sh (tools/setlist_samples.py)
// from the recent set's tracks, so those samples are warm instead of
// lazy-loading (crackle + xruns) on first play mid-gig. Absent = pure lazy.
if(File.exists("/home/pln/Work/Sound/Tidal/preload.scd"), {
"preload: warming the set's samples…".postln;
thisProcess.interpreter.executeFile("/home/pln/Work/Sound/Tidal/preload.scd");
}, {
"preload: no preload.scd — lazy-loading samples on demand.".postln;
});
// =============================================
// Mutable Instruments Global Effects
// https://club.tidalcycles.org/t/mutable-instruments-ugens/2730/22
......
......@@ -71,6 +71,24 @@ def last_n_tracks(n):
return tracks[:n]
REPO = HOME / "Work/Sound/Tidal"
def resolve_track(arg):
"""Accept a full path OR a bare track name; find it under the repo."""
p = Path(arg)
if p.is_file():
return [p]
stem = p.name if p.name.endswith('.tidal') else p.name + '.tidal'
matches = list(LIVE_DIR.rglob(stem)) or list(REPO.rglob(stem))
if not matches:
print(f'! no track named {stem!r} under live/ or the repo', file=sys.stderr)
elif len(matches) > 1:
print(f'! {stem!r} is ambiguous ({len(matches)} matches) — using {matches[0]}',
file=sys.stderr)
return matches[:1]
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
......@@ -81,11 +99,11 @@ def main():
help='print a SuperCollider preload snippet for the resolved folders')
args = ap.parse_args()
tracks = list(args.tracks)
tracks = [p for arg in args.tracks for p in resolve_track(arg)]
if args.last:
tracks += last_n_tracks(args.last)
if not tracks:
ap.error('give track paths or --last N')
ap.error('give track names/paths or --last N')
idx = build_index()
names, per_track = set(), {}
......
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