Hermes Agent 633eb92352 Public-ready: JSON tune specs, first-class loop API, licence, AGENTS.md
- 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
2026-10-17 06:59:04 +00:00

MIDI Playlist — retro game music, built from code

Kevin Hermes' toolkit for composing early-90s flavoured MIDI: DOS Sound Blaster/OPL themes, N64-era ambient, chiptune. Built for retro game development (loop markers, multi-track GM arrangements) and podcast audio beds.

Everything is code-first: tunes are Python scripts that generate verified .mid files. No DAW, no pip dependencies in the library, no hand-edited hex. If a tune is wrong, you fix a bar of text and rebuild.

The tunes

# Tune Style Key/BPM Length
01 Service Station at 2 AM DOS adventure main theme (LucasArts-meets-OPL) Dm / 132 ~58s
02 Kestrel Over The Fens N64 ambient exploration theme G / 72 ~82s
03 The U's Late Equaliser chiptune sports/racing theme Em / 152 ~44s
04 Tea Break at the Terminal DOS puzzle-game theme (from JSON spec) Am / 112 ~51s

All files are Standard MIDI File format 1, 480 tpq, General MIDI instruments, with loop markers (LOOP_START/END, RPR LOOP START/END, LoopBegin/End) so a game engine can skip the intro and loop the gameplay section. See Looping below.

Quick start

./tools/build_all.sh          # regenerate, verify, render WAVs, audio checks
python3 tests/smoke_test.py   # library self-test
python3 tools/make_tune.py tunes/specs/04_tea_break_at_the_terminal.json --render

Rendering needs timidity + a GM soundfont (FluidR3_GM.sf2). Note: Debian's timidity build has no -O2 mode — use -Ow -s 44.

AI agents: see AGENTS.md — the JSON spec format is the agent-friendly front door; no Python required to make a tune.

Layout

lib/midikit.py      SMF writer + reader/verifier (stdlib only)
lib/songkit.py      composition helpers: bar grids, OPL arps, vibrato, drums
tunes/*.py          one script per tune; each self-verifies on run
tunes/specs/*.json  declarative tune specs -> tools/make_tune.py
tools/build_all.sh  full pipeline: generate -> verify -> render -> check
tools/make_tune.py  JSON spec -> fully arranged MIDI (style presets)
tools/check_wav.py  WAV sanity: clipping, loudness, dead air
tests/smoke_test.py library round-trip test (cross-checked with mido)
midi/               generated .mid files
archive/            the April 2026 wreckage (70-byte "working" MIDIs, rest in peace)

Looping

Game background music convention: the intro bars play once, then the engine jumps back to the loop point. Declare it once and every marker dialect gets written:

mf.set_loop(bar(4), bar(28))   # Python
"loop": {"start_bar": 4, "end_bar": 20}   // JSON spec

Markers written: LOOP_START/LOOP_END (ours), RPR LOOP START/END (Reaper), LoopBegin/LoopEnd (various engines/trackers).

Composing a new tune

Copy a tune script. Melodies are written as bar grids:

MELODY = """
D4-8 F4-8 A4-4 A4-8 Bb4-8 A4-4 | F4-8 A4-8 Bb4-4 A4-8 F4-8 D4-4
"""

Tokens are PITCH-dur-vel (D4-8, F5-16-80, rests r8); | separates bars and every bar must total 4/4 — the parser lints you. Accompaniment comes from the helpers: arp() (OPL-style 16ths), bass_drive() (driving 8ths with chromatic approaches), drums() (named patterns), vibrato() (pitch-bend, the difference between a beep and an OPL voice).

Mix levels: set CC7 (volume) and CC10 (pan) per track at tick 0, and check the render for clipping — tools/check_wav.py fails the build on a peak of 32767.

Gotchas learned the hard way

  • Format 1 with a dedicated Conductor track for tempo/key/time sigs.
  • Sort events by absolute tick at write time; compose in any order.
  • Note-off must match note-on channel+pitch exactly; no running status in generated files (simpler, universally safe).
  • Pitch bends count toward file duration — a vibrato tail past the last note extends the song.
  • Velocity-0 note-ons are legal note-offs, but 0x80 is clearer.
  • GM2 instruments (ocarina!) are unreliable on GM1 synths — use pan flute (76) for ocarina-ish leads.
  • timidity's "Last N MIDI events are ignored" warning is harmless (trailing bends/resets), but "Notes lost totally" is not.
  • A bar-length linter in the score parser catches more composition bugs than any amount of staring at note lists.

Licence

MIT — see LICENSE. Soundfonts are not bundled (licensing); install FluidR3_GM.sf2 or your favourite GM soundfont separately.

S
Description
Code-first retro MIDI composition toolkit - JSON tune specs, style presets, loop markers, OPL3 rendering. Zero dependencies.
Readme MIT
1.8 MiB
0 Stars 0 Watchers 0 Forks
Languages
Python 89.8%
Shell 6.1%
C++ 4.1%