Rebuild as a code-first MIDI composition toolkit
Replaces the April wreckage (fifteen 70-byte 'working' MIDIs, archived) with a proper pipeline: - lib/midikit.py: stdlib-only SMF format-1 writer + reader/verifier - lib/songkit.py: composition helpers (bar-grid score DSL with a 4/4 bar linter, OPL arps, driving bass, vibrato, drum patterns) - tunes/: three original tunes - DOS adventure theme (Dm/132), N64 ambient (G/72), chiptune racing (Em/152) - all with LOOP_START/ LOOP_END markers for game engines - tools/build_all.sh + tools/check_wav.py: generate, verify, render via timidity+FluidR3, fail on clipping/dead air - tests/smoke_test.py: round-trip verified against mido All three tunes render clean: no clipping, no dead air, durations match, chromaticism audited against the chord chart.
This commit is contained in:
1 parent
7ddabaff2a
commit
95480abdab
31 files changed
+1222
-31
No files matched your search
@@ -1,40 +1,81 @@
|
||||
# DOS MIDI Playlist
|
||||
# MIDI Playlist — retro game music, built from code
|
||||
|
||||
Repository for my early 90's DOS gaming MIDI music collection.
|
||||
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.
|
||||
|
||||
## MIDI Files
|
||||
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.
|
||||
|
||||
### Castles_Proper.mid
|
||||
**A proper, working DOS Castle theme!**
|
||||
## The tunes
|
||||
|
||||
- **Style**: Medieval Castle Corridor (95 BPM)
|
||||
- **Key**: D minor
|
||||
- **Duration**: ~1 minute
|
||||
- **Format**: Standard MIDI Type 0 with proper header
|
||||
- **Features**:
|
||||
- Correct MIDI binary structure
|
||||
- Variable-length delta time encoding
|
||||
- Tempo metadata events
|
||||
- Note on/off events with velocities
|
||||
- 480 ticks per quarter note
|
||||
- **Playable in**: QuickTime Player, web browsers, any MIDI player
|
||||
| # | 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 |
|
||||
|
||||
**Download**: [midi/Castles_Proper.mid](midi/Castles_Proper.mid)
|
||||
**View Online**: https://repos.retroweb.dev/hermes-playground/midi-playlist/midi/Castles_Proper.mid
|
||||
All files are Standard MIDI File **format 1**, 480 tpq, General MIDI
|
||||
instruments, with `LOOP_START` / `LOOP_END` markers so a game engine can
|
||||
skip the intro and loop the gameplay section.
|
||||
|
||||
## Technical Details
|
||||
## Quick start
|
||||
|
||||
### MIDI Format Specification
|
||||
- Header: MThd ("MThd") + 6 bytes + format 0 + 1 track
|
||||
- Tempo: 600000 microseconds/quarter (100 BPM)
|
||||
- Delta time: Encoded as variable-length quantities
|
||||
- Events: Note on (0x90) / off (0x80) with velocities
|
||||
```bash
|
||||
./tools/build_all.sh # regenerate, verify, render WAVs, audio checks
|
||||
python3 tests/smoke_test.py # library self-test
|
||||
```
|
||||
|
||||
### Notes in the Castle Theme
|
||||
D4, E4, G4, A4 - D minor arpeggio pattern repeated
|
||||
Each note lasts a full quarter note (480 ticks)
|
||||
Rendering needs `timidity` + a GM soundfont (`FluidR3_GM.sf2`). Note:
|
||||
Debian's timidity build has no `-O2` mode — use `-Ow -s 44`.
|
||||
|
||||
## Generated Using
|
||||
- Pure Python with struct module
|
||||
- Correct MIDI binary protocol encoding
|
||||
- Valid MIDI Type 0 file format
|
||||
## 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
|
||||
tools/build_all.sh full pipeline: generate -> verify -> render -> check
|
||||
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)
|
||||
```
|
||||
|
||||
## 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.
|
||||
Reference in new issue
Block a user