Files
midi-playlist/AGENTS.md
T

4.0 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)
}

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

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 -O2 mode).
  • OPL3 FM (authentic DOS sound, no soundfont): ./tools/opl/build.sh once (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.