Files
Hermes Agent 810f127d01 Harden for agent users: spec validation, key-sig accidentals, N64 chord qualities
- make_tune: validate_spec() reports every spec problem in one run
  (missing keys, unknown style, bad qualities, bpm<=0, out-of-range
  loop/fills, melody longer than chart, unsafe name) — no raw KeyErrors
- make_tune: key_sig_for handles accidentals in minor and major keys,
  any case (Bb minor, F# minor, bb major); real spellings kept (C# major)
- make_tune: n64 pad uses QUALITIES table (dim/aug/sus now correct)
- make_tune: WARNING on stderr for fields a style can't use (counter
  for non-DOS, fills_at for n64) instead of silent drops
- make_tune: name sanitised ([A-Za-z0-9][A-Za-z0-9._-]*), no traversal
- make_tune: render errors actionable (timidity presence check, stderr
  surfaced, sys.executable instead of bare python3)
- songkit: parse_score lints the FINAL bar too (was unchecked); this
  caught a real 5-beat bar in 01_service_station_2am COUNTER_A (fixed)
- build_all.sh: dependency + soundfont checks, removes stale midi/wav
  outputs with no current source, fails if nothing was produced
- smoke_test: runs every spec, checks loop marker tick positions,
  feeds malformed bars to the linter, covers validation + key sigs
- AGENTS.md: documents 0-based bars and exclusive end_bar, validation
  behaviour; SKILL.md added so the repo loads as an agent skill
2026-10-17 15:59:46 +00:00

113 lines
5.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)
}
```
## Indexing convention (read this once, then never guess)
- **Bars are 0-based everywhere**: bar 0 is the first bar of the chord chart.
- **`loop.end_bar` is EXCLUSIVE**: `{"start_bar": 4, "end_bar": 20}` loops
bars 4..19 — 16 bars. `start_bar` is inclusive.
- The melody must fit the chart: `melody_start_bar + (number of '|' bars)
<= len(chords)`. Bars past the melody (e.g. chords 20–23 above) are an
outro the engine plays before jumping back to `start_bar`.
- `fills_at` entries are 0-based bar numbers, `0 <= bar < len(chords)`.
## Validation behaviour
`make_tune.py` validates the whole spec up front and reports **every**
problem in one go (missing keys, unknown style, bad chord quality,
`bpm <= 0`, out-of-range loop/fill bars, melody longer than the chart,
unsafe `name`) — no raw `KeyError`s, no silently wrong output. Fields a
style can't use (e.g. `counter` for `n64_ambient`, `fills_at` for styles
without drums) print `WARNING:` to stderr and are skipped, never dropped
in silence. `name` must match `[A-Za-z0-9][A-Za-z0-9._-]*` — it becomes
`midi/<name>.mid`, so no `/`, no `..`.
## 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 — including the final
one — 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.