- MidiFile.set_loop(): one call writes LOOP_START/END, RPR LOOP START/END (Reaper) and LoopBegin/End markers - every engine finds its dialect; reader now parses markers and the smoke test asserts the round-trip - tools/make_tune.py: declarative JSON spec -> fully arranged MIDI with style presets (dos_adventure, n64_ambient, chiptune_racing); the agent-friendly front door, no Python required - tunes/specs/: demo tune 04 'Tea Break at the Terminal' (Am/112, loop bars 4-20) generated entirely from JSON - AGENTS.md: spec schema, score grammar, verification loop, looping conventions for game BGM - MIT LICENSE; history audited clean of credentials
84 lines
3.5 KiB
Markdown
84 lines
3.5 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
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
|
|
|
|
```jsonc
|
|
{
|
|
"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)
|
|
}
|
|
```
|
|
|
|
## Score grammar (melody strings)
|
|
|
|
- Tokens: `PITCH-dur-vel` — `A4-8-90` (A4 eighth, vel 90). Durations:
|
|
`1` whole, `2` half, `4` quarter (default), `8` eighth, `16` sixteenth.
|
|
- Rests: `r4`, `r8`, `r16`, `r2`, `r1`.
|
|
- `|` separates bars. **If you use `|`, every bar must total exactly 4/4**
|
|
— the parser raises `ValueError: bar N totals X ticks` and 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)
|
|
|
|
1. `python3 tools/make_tune.py spec.json --render` — writes MIDI, re-parses
|
|
it, renders WAV, checks for clipping / dead air / wrong duration.
|
|
2. If a check fails, fix the spec and re-run. Never declare success without
|
|
a passing run.
|
|
3. 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
|
|
|
|
Needs `timidity` + a GM soundfont (e.g. FluidR3_GM.sf2, not bundled —
|
|
licensing). Debian's timidity: use `-Ow -s 44` (no `-O2` mode).
|
|
|
|
## 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.
|