- 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
5.1 KiB
Notes for AI agents
This repo is designed to be driven by agents. Everything is deterministic, self-verifying, and dependency-light.
Fastest path: generate a tune from a JSON spec
python3 tools/make_tune.py tunes/specs/04_tea_break_at_the_terminal.json --render
Copy that spec, change name, bpm, key, chords, melody, loop.
The style preset (dos_adventure, n64_ambient, chiptune_racing) handles
arrangement: instruments, accompaniment patterns, drums, vibrato, mix levels.
You supply the music theory: a per-bar chord chart and a melody.
Spec schema
{
"name": "file_name_no_extension",
"style": "dos_adventure | n64_ambient | chiptune_racing",
"bpm": 112,
"key": ["A", "minor"], // "major" or "minor"
"chords": [["A","m"], ["F",""], ...], // one per bar; qualities:
// maj m dim aug 7 maj7 m7 m7b5 6 m6 9 add9 sus2 sus4 sus
"melody_start_bar": 4,
"melody": "A4-8 C5-8 E5-4 A5-2 | ...", // see score grammar below
"loop": {"start_bar": 4, "end_bar": 20}, // optional; writes LOOP_START/END,
// RPR LOOP START/END, LoopBegin/End markers
"counter": "...", // optional 2nd line (dos_adventure only)
"fills_at": [11, 19] // optional drum-fill bars (styles with drums)
}
Indexing convention (read this once, then never guess)
- Bars are 0-based everywhere: bar 0 is the first bar of the chord chart.
loop.end_baris EXCLUSIVE:{"start_bar": 4, "end_bar": 20}loops bars 4..19 — 16 bars.start_baris inclusive.- The melody must fit the chart:
melody_start_bar + (number of '|' bars) <= len(chords). Bars past the melody (e.g. chords 20–23 above) are an outro the engine plays before jumping back tostart_bar. fills_atentries are 0-based bar numbers,0 <= bar < len(chords).
Validation behaviour
make_tune.py validates the whole spec up front and reports every
problem in one go (missing keys, unknown style, bad chord quality,
bpm <= 0, out-of-range loop/fill bars, melody longer than the chart,
unsafe name) — no raw KeyErrors, no silently wrong output. Fields a
style can't use (e.g. counter for n64_ambient, fills_at for styles
without drums) print WARNING: to stderr and are skipped, never dropped
in silence. name must match [A-Za-z0-9][A-Za-z0-9._-]* — it becomes
midi/<name>.mid, so no /, no ...
Score grammar (melody strings)
- Tokens:
PITCH-dur-vel—A4-8-90(A4 eighth, vel 90). Durations:1whole,2half,4quarter (default),8eighth,16sixteenth. - Rests:
r4,r8,r16,r2,r1. |separates bars. If you use|, every bar — including the final one — must total exactly 4/4 — the parser raisesValueError: bar N totals X ticksand tells you which bar is wrong. Use it; it is the single most useful bug-catcher here.- Pitches: letter + optional
#/b+ octave, e.g.Bb3,F#5.
Verification loop (do this, don't skip it)
python3 tools/make_tune.py spec.json --render— writes MIDI, re-parses it, renders WAV, checks for clipping / dead air / wrong duration.- If a check fails, fix the spec and re-run. Never declare success without a passing run.
- Optional theory audit: parse the lead track with
midikit.read(), flag notes outside the key scale and outside the current bar's chord tones, and treat each flag as either intentional colour or a bug.
Looping for game background music
loop in the spec (or MidiFile.set_loop(start_tick, end_tick) in Python)
writes markers in three conventions so any engine/DAW finds them:
LOOP_START/LOOP_END, RPR LOOP START/RPR LOOP END (Reaper),
LoopBegin/LoopEnd. Convention: intro bars sit before the loop start so
the engine plays the intro once, then jumps back to loop start and returns
to loop end.
Library use (Python)
lib/midikit.py and lib/songkit.py are stdlib-only — drop them into any
project. Key entry points: MidiFile, Track, MPQN.from_bpm,
set_loop, verify; songkit: bar, melody, arp, bass_drive,
drums, vibrato, parse_score. Times are ticks; bar(n) converts bars
to ticks at 480 tpq.
Rendering
Two routes, both verified:
- Sample-based:
timidity -Ow -s 44 in.mid -o out.wav+ a GM soundfont. FluidR3 is MIT-licensed and one command away:apt install fluid-soundfont-gm(148MB; lighter:timgm6mb-soundfont, ~6MB). Not bundled in-repo because of size, not licence. Debian's timidity: use-Ow -s 44(no-O2mode). - OPL3 FM (authentic DOS sound, no soundfont):
./tools/opl/build.shonce (needs cmake+g++, fetches libADLMIDI), then./tools/render_opl.sh.OPL_MODEL=env picks a game-driver volume model: 3 DMX/Doom (default), 4 Apogee/Duke Nukem 3D, 2 id-style, 8 AIL, 10 HMI. Output is normalised.
Known gotchas
- Pitch bends extend file duration (vibrato tails count).
- No comments inside melody strings —
#gets parsed as a note. - GM2 patches (ocarina etc.) are unreliable on GM1 synths; use pan flute.
- Five hot tracks sum to clipping; lower CC7 volumes, not velocities.