Commit a4a76e03 by PLN (Algolia)

feat(gig): setlist_samples.py — preload just the set's samples (kill lazy-load cracks)

Live play cracked from lazy sample loading (~dirt.doNotReadYet=true): each
sample reads from disk on first play mid-set → crackle + xrun spikes
(764→1085 xruns during a track). Loading the whole library = slow boot + RAM;
lazy = cracks. This tool finds the middle path: parse a set's .tidal tracks
(or --last N by mtime), extract every token from every quoted string, and keep
only those that resolve to a real sample folder (Dirt-Samples, extra/,
tidal-drum-machines) — the library itself is the filter, robust against
ParVagues' bare-string/# dialect that defeats s"..."-based parsing.

--emit-sc prints a ~dirt.loadSoundFiles preload snippet. Validated: last 15
tracks → 55 folders (vs hundreds in the library); bombe_dj → 8. Foundation for
#26 (wire into boot) + #24 (soundcheck). t/f excluded (Tidal booleans).
parent c63dd3d3
#!/usr/bin/env python3
"""setlist_samples — what samples does a set actually use?
Parses `s "..."` / `sound "..."` calls out of .tidal tracks, resolves each
sample name to its on-disk folder across the SuperDirt sample roots, and emits
a preload plan. This feeds the smart-preload boot step (task #26): warm exactly
the set's samples before the gig instead of lazy-loading them (crackle + xruns)
on first play mid-set — without loading the whole library.
Usage:
tools/setlist_samples.py TRACK.tidal [TRACK2.tidal ...]
tools/setlist_samples.py --last 10 # 10 most-recently-edited live/ tracks
tools/setlist_samples.py --last 30 --emit-sc # print SC preload snippet
"""
import re, sys, argparse
from pathlib import Path
HOME = Path.home()
# Sample roots, in the order start_and_midi.scd loads them.
SAMPLE_ROOTS = [
HOME / ".local/share/SuperCollider/downloaded-quarks/Dirt-Samples",
HOME / "Work/Sound/Samples/extra",
]
DRUM_MACHINES = HOME / "Work/Sound/Samples/tidal-drum-machines/machines"
LIVE_DIR = HOME / "Work/Sound/Tidal/live"
# ParVagues' dialect names samples via bare strings and `# "x"`, not `s "..."`,
# and mixes them with structure/notes/MIDI-ctrl strings in the same quotes. So
# we extract EVERY word-token from EVERY quoted string and let the on-disk
# sample library be the filter (a token is a sample iff a folder exists for it).
QUOTED = re.compile(r'"([^"]*)"')
WORD = re.compile(r'[A-Za-z][A-Za-z0-9_]*')
# Tidal boolean literals — collide with real Dirt-Samples/{t,f} folders but are
# never meant as samples in the mini-notation, so never preload them.
STOP = {'t', 'f'}
LINE_COMMENT = re.compile(r'--.*$', re.MULTILINE)
BLOCK_COMMENT = re.compile(r'\{-.*?-\}', re.DOTALL)
def extract_names(text):
"""Every candidate token appearing in a quoted string (resolution filters)."""
text = BLOCK_COMMENT.sub(' ', text)
text = LINE_COMMENT.sub('', text)
names = set()
for s in QUOTED.findall(text):
names.update(WORD.findall(s))
return names
def build_index():
"""name -> folder Path, across Dirt-Samples, extra/, and drum machines."""
idx = {}
for root in SAMPLE_ROOTS:
if root.is_dir():
for d in sorted(root.iterdir()):
if d.is_dir():
idx.setdefault(d.name, d)
# drum machines: <machine>/<folder>, name = folder basename minus '-'
if DRUM_MACHINES.is_dir():
for machine in sorted(DRUM_MACHINES.iterdir()):
if machine.is_dir():
for folder in sorted(machine.iterdir()):
if folder.is_dir():
idx.setdefault(folder.name.replace('-', ''), folder)
return idx
def last_n_tracks(n):
tracks = sorted(LIVE_DIR.rglob('*.tidal'),
key=lambda p: p.stat().st_mtime, reverse=True)
return tracks[:n]
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument('tracks', nargs='*', type=Path)
ap.add_argument('--last', type=int, metavar='N',
help='use the N most-recently-edited live/ tracks')
ap.add_argument('--emit-sc', action='store_true',
help='print a SuperCollider preload snippet for the resolved folders')
args = ap.parse_args()
tracks = list(args.tracks)
if args.last:
tracks += last_n_tracks(args.last)
if not tracks:
ap.error('give track paths or --last N')
idx = build_index()
names, per_track = set(), {}
for t in tracks:
try:
got = extract_names(t.read_text(errors='replace'))
except OSError as e:
print(f'! skip {t}: {e}', file=sys.stderr)
continue
per_track[t] = got
names |= got
resolved = {n: idx[n] for n in names if n in idx and n not in STOP}
unresolved = sorted(names - resolved.keys())
folders = sorted({str(p) for p in resolved.values()})
if args.emit_sc:
print('// preload — set-specific samples (setlist_samples.py)')
print('~dirt.doNotReadYet = false;')
for f in folders:
print(f'~dirt.loadSoundFiles("{f}");')
print(f'// {len(folders)} folders, {len(resolved)} names; '
f'{len(unresolved)} unresolved (lazy fallback)')
return
print(f'tracks scanned : {len(per_track)}')
print(f'candidate toks : {len(names)} (words seen inside all quoted strings)')
print(f'SAMPLES found : {len(resolved)} -> {len(folders)} folders to warm')
print(f' (non-sample tokens = structure/notes/MIDI/synths, ignored: {len(unresolved)})')
print('\nSAMPLES to preload (name -> folder):')
for n in sorted(resolved):
print(f' {n:20s} {resolved[n]}')
if __name__ == '__main__':
main()
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