2026-10-17 00:00:20 +00:00
|
|
|
#!/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
|
|
|
|
|
#
|
2026-10-17 15:59:46 +00:00
|
|
|
# Requires: python3, timidity (with a GM soundfont, e.g. FluidR3_GM.sf2).
|
2026-10-17 00:00:20 +00:00
|
|
|
# WAVs land in build/ (gitignored); MIDI files land in midi/.
|
2026-10-17 15:59:46 +00:00
|
|
|
#
|
|
|
|
|
# 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.
|
2026-10-17 00:00:20 +00:00
|
|
|
set -euo pipefail
|
|
|
|
|
cd "$(dirname "$0")/.."
|
|
|
|
|
mkdir -p midi build
|
|
|
|
|
|
2026-10-17 15:59:46 +00:00
|
|
|
# ---- 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
|
2026-10-17 00:00:20 +00:00
|
|
|
echo "== $t"
|
|
|
|
|
python3 "$t"
|
|
|
|
|
done
|
|
|
|
|
|
2026-10-17 15:59:46 +00:00
|
|
|
for s in "${sources_json[@]}"; do
|
|
|
|
|
[ -e "$s" ] || continue
|
2026-10-17 06:59:04 +00:00
|
|
|
echo "== $s"
|
|
|
|
|
python3 tools/make_tune.py "$s"
|
|
|
|
|
done
|
|
|
|
|
|
2026-10-17 00:00:20 +00:00
|
|
|
echo "== verifying MIDI"
|
|
|
|
|
python3 - <<'EOF'
|
|
|
|
|
import sys, glob
|
|
|
|
|
sys.path.insert(0, "lib")
|
|
|
|
|
from midikit import verify
|
2026-10-17 15:59:46 +00:00
|
|
|
files = sorted(glob.glob("midi/*.mid"))
|
|
|
|
|
assert files, "no MIDI files were produced — nothing to verify"
|
|
|
|
|
for f in files:
|
2026-10-17 00:00:20 +00:00
|
|
|
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)"
|
2026-10-17 15:59:46 +00:00
|
|
|
shopt -s nullglob
|
|
|
|
|
mids=(midi/*.mid)
|
|
|
|
|
[ "${#mids[@]}" -gt 0 ] || { echo "ERROR: no MIDI to render" >&2; exit 1; }
|
|
|
|
|
for f in "${mids[@]}"; do
|
2026-10-17 00:00:20 +00:00
|
|
|
out="build/$(basename "${f%.mid}").wav"
|
2026-10-17 15:59:46 +00:00
|
|
|
if ! timidity -Ow -s 44 "$f" -o "$out"; then
|
|
|
|
|
echo "ERROR: timidity failed on $f" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
2026-10-17 00:00:20 +00:00
|
|
|
python3 tools/check_wav.py "$out"
|
|
|
|
|
done
|
2026-10-17 15:59:46 +00:00
|
|
|
echo "ALL OK (${#mids[@]} tunes)"
|