#!/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)"