Files
midi-playlist/tools/build_all.sh
T
Hermes Agent 810f127d01 Harden for agent users: spec validation, key-sig accidentals, N64 chord qualities
- make_tune: validate_spec() reports every spec problem in one run
  (missing keys, unknown style, bad qualities, bpm<=0, out-of-range
  loop/fills, melody longer than chart, unsafe name) — no raw KeyErrors
- make_tune: key_sig_for handles accidentals in minor and major keys,
  any case (Bb minor, F# minor, bb major); real spellings kept (C# major)
- make_tune: n64 pad uses QUALITIES table (dim/aug/sus now correct)
- make_tune: WARNING on stderr for fields a style can't use (counter
  for non-DOS, fills_at for n64) instead of silent drops
- make_tune: name sanitised ([A-Za-z0-9][A-Za-z0-9._-]*), no traversal
- make_tune: render errors actionable (timidity presence check, stderr
  surfaced, sys.executable instead of bare python3)
- songkit: parse_score lints the FINAL bar too (was unchecked); this
  caught a real 5-beat bar in 01_service_station_2am COUNTER_A (fixed)
- build_all.sh: dependency + soundfont checks, removes stale midi/wav
  outputs with no current source, fails if nothing was produced
- smoke_test: runs every spec, checks loop marker tick positions,
  feeds malformed bars to the linter, covers validation + key sigs
- AGENTS.md: documents 0-based bars and exclusive end_bar, validation
  behaviour; SKILL.md added so the repo loads as an agent skill
2026-10-17 15:59:46 +00:00

103 lines
3.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# build_all.sh — regenerate every tune, verify the MIDI, render WAVs,
# and run audio sanity checks. One command, from repo root:
#
# ./tools/build_all.sh
#
# Requires: python3, timidity (with a GM soundfont, e.g. FluidR3_GM.sf2).
# WAVs land in build/ (gitignored); MIDI files land in midi/.
#
# Stale outputs are removed: any midi/*.mid or build/*.wav that no longer
# has a corresponding source (tunes/*.py or tunes/specs/*.json) is deleted,
# so "ALL OK" can never refer to a tune whose source has gone.
set -euo pipefail
cd "$(dirname "$0")/.."
mkdir -p midi build
# ---- dependency checks (fail fast with an actionable message) ----
missing=0
for cmd in python3 timidity; do
if ! command -v "$cmd" >/dev/null 2>&1; then
echo "ERROR: '$cmd' not found on PATH." >&2
missing=1
fi
done
if [ "$missing" -eq 1 ]; then
echo "Debian/Ubuntu: apt install python3 timidity fluid-soundfont-gm" >&2
exit 1
fi
# soundfont: timidity needs one; check its config or the usual FluidR3 path
if ! timidity -iA --help >/dev/null 2>&1; then :; fi # presence already checked
if [ ! -f /usr/share/sounds/sf2/FluidR3_GM.sf2 ] && \
! grep -qsE '^[[:space:]]*dir[[:space:]]|^[[:space:]]*file[[:space:]]' /etc/timidity/timidity.cfg 2>/dev/null; then
echo "WARNING: no obvious GM soundfont found (checked /usr/share/sounds/sf2/FluidR3_GM.sf2 and /etc/timidity/timidity.cfg)." >&2
echo " WAV rendering may fail or sound wrong. apt install fluid-soundfont-gm" >&2
fi
# ---- collect sources and the set of names they produce ----
sources_py=(tunes/*.py)
sources_json=(tunes/specs/*.json)
expected=()
for t in "${sources_py[@]}"; do
[ -e "$t" ] && expected+=("$(basename "${t%.py}")")
done
for s in "${sources_json[@]}"; do
[ -e "$s" ] && expected+=("$(python3 -c "import json,sys; print(json.load(open(sys.argv[1]))['name'])" "$s")")
done
# ---- remove stale outputs (no current source) ----
shopt -s nullglob
for f in midi/*.mid; do
n="$(basename "${f%.mid}")"
keep=0
for e in "${expected[@]}"; do [ "$e" = "$n" ] && keep=1 && break; done
if [ "$keep" -eq 0 ]; then
echo "stale: removing $f (no source)"
rm -f "$f" "build/$n.wav"
fi
done
for f in build/*.wav; do
[ -e "midi/$(basename "${f%.wav}").mid" ] || { echo "stale: removing $f"; rm -f "$f"; }
done
# ---- build ----
for t in "${sources_py[@]}"; do
[ -e "$t" ] || continue
echo "== $t"
python3 "$t"
done
for s in "${sources_json[@]}"; do
[ -e "$s" ] || continue
echo "== $s"
python3 tools/make_tune.py "$s"
done
echo "== verifying MIDI"
python3 - <<'EOF'
import sys, glob
sys.path.insert(0, "lib")
from midikit import verify
files = sorted(glob.glob("midi/*.mid"))
assert files, "no MIDI files were produced — nothing to verify"
for f in files:
d = verify(f)
print(" %-40s %6.1fs %4d notes %d tracks" %
(f, d["duration_sec"], sum(len(t["notes"]) for t in d["tracks"]),
len(d["tracks"])))
EOF
echo "== rendering WAV (timidity)"
shopt -s nullglob
mids=(midi/*.mid)
[ "${#mids[@]}" -gt 0 ] || { echo "ERROR: no MIDI to render" >&2; exit 1; }
for f in "${mids[@]}"; do
out="build/$(basename "${f%.mid}").wav"
if ! timidity -Ow -s 44 "$f" -o "$out"; then
echo "ERROR: timidity failed on $f" >&2
exit 1
fi
python3 tools/check_wav.py "$out"
done
echo "ALL OK (${#mids[@]} tunes)"