# 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](#looping) below. ## Quick start ```bash ./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`, MIT — `apt install fluid-soundfont-gm`), or skip soundfonts entirely and render through OPL3 emulation — see [OPL3 rendering](#opl3-rendering-the-authentic-one). Note: Debian's timidity build has no `-O2` mode — use `-Ow -s 44`. **AI agents**: see [AGENTS.md](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: ```python mf.set_loop(bar(4), bar(28)) # Python ``` ```json "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: ```python 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. ## OPL3 rendering (the authentic one) For real Sound Blaster flavour — FM synthesis, no soundfont, like the actual YMF262 chip: ```bash ./tools/opl/build.sh # one-time: fetches + builds libADLMIDI ./tools/render_opl.sh # renders all midi/*.mid through OPL3 OPL_MODEL=4 ./tools/render_opl.sh midi/03_late_equaliser.mid ``` Volume models mimic real game sound drivers: `2` NativeOPL3 (id-style), `3` DMX (Doom, default), `4` APOGEE (Duke Nukem 3D), `8` AIL, `10` HMI. Output is normalised (OPL runs quiet). Needs `cmake` + `g++` to build. ## Licence & soundfonts MIT — see [LICENSE](LICENSE). Rendering needs a General MIDI soundfont, which is **not bundled only because of size**, not licence: FluidR3_GM.sf2 is MIT-licensed and installs with one command — ```bash apt install fluid-soundfont-gm # FluidR3, 148MB, MIT — what we use apt install timgm6mb-soundfont # TimGM6mb, ~6MB, lighter fallback ``` Soundfonts are sample libraries for the synth (timidity/fluidsynth) — MIDI files themselves contain no audio. For authentic 1993 Sound Blaster flavour, OPL3 emulators (ymfmidi, libADLMIDI) need no soundfont at all: they synthesise FM like the real card. Game engines supply their own MIDI synthesis, so shipped game assets never need a soundfont bundled either.