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
This commit is contained in:
Hermes Agent committed 2026-10-17 06:59:04 +00:00
1 parent c3cd91d04f
commit 633eb92352
15 files changed
+387 -9

No files matched your search

+83
View File
@@ -0,0 +1,83 @@
# 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)
}
```
## 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
Needs `timidity` + a GM soundfont (e.g. FluidR3_GM.sf2, not bundled —
licensing). Debian's timidity: use `-Ow -s 44` (no `-O2` mode).
## 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.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Kevin Hermes (an AI agent) and Steve Calcott
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+31 -2
View File
@@ -16,34 +16,58 @@ hand-edited hex. If a tune is wrong, you fix a bar of text and rebuild.
| 01 | Service Station at 2 AM | DOS adventure main theme (LucasArts-meets-OPL) | Dm / 132 | ~58s | | 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 | | 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 | | 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 All files are Standard MIDI File **format 1**, 480 tpq, General MIDI
instruments, with `LOOP_START` / `LOOP_END` markers so a game engine can instruments, with loop markers (`LOOP_START/END`, `RPR LOOP START/END`,
skip the intro and loop the gameplay section. `LoopBegin/End`) so a game engine can skip the intro and loop the
gameplay section. See [Looping](#looping) below.
## Quick start ## Quick start
```bash ```bash
./tools/build_all.sh # regenerate, verify, render WAVs, audio checks ./tools/build_all.sh # regenerate, verify, render WAVs, audio checks
python3 tests/smoke_test.py # library self-test 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: Rendering needs `timidity` + a GM soundfont (`FluidR3_GM.sf2`). Note:
Debian's timidity build has no `-O2` mode — use `-Ow -s 44`. 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 ## Layout
``` ```
lib/midikit.py SMF writer + reader/verifier (stdlib only) lib/midikit.py SMF writer + reader/verifier (stdlib only)
lib/songkit.py composition helpers: bar grids, OPL arps, vibrato, drums lib/songkit.py composition helpers: bar grids, OPL arps, vibrato, drums
tunes/*.py one script per tune; each self-verifies on run 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/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 tools/check_wav.py WAV sanity: clipping, loudness, dead air
tests/smoke_test.py library round-trip test (cross-checked with mido) tests/smoke_test.py library round-trip test (cross-checked with mido)
midi/ generated .mid files midi/ generated .mid files
archive/ the April 2026 wreckage (70-byte "working" MIDIs, rest in peace) 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 ## Composing a new tune
Copy a tune script. Melodies are written as bar grids: Copy a tune script. Melodies are written as bar grids:
@@ -79,3 +103,8 @@ on a peak of 32767.
(trailing bends/resets), but "Notes lost totally" is not. (trailing bends/resets), but "Notes lost totally" is not.
- A bar-length linter in the score parser catches more composition bugs - A bar-length linter in the score parser catches more composition bugs
than any amount of staring at note lists. than any amount of staring at note lists.
## Licence
MIT — see [LICENSE](LICENSE). Soundfonts are not bundled (licensing);
install FluidR3_GM.sf2 or your favourite GM soundfont separately.
+26 -1
View File
@@ -253,6 +253,22 @@ class MidiFile:
self.tempos = [] # list[MPQN] self.tempos = [] # list[MPQN]
self.key_sig = None # (flats_sharps, minor_bool) self.key_sig = None # (flats_sharps, minor_bool)
self.time_sig = None # (num, den_power) self.time_sig = None # (num, den_power)
self.loop = None # (start_tick, end_tick)
def set_loop(self, start_tick, end_tick):
"""Declare the loopable section (game BGM: intro plays once, then
the engine jumps back). Writes markers in every convention that
matters, so whichever player/engine you point at the file, it finds
the loop point:
LOOP_START / LOOP_END - our convention, plain SMF markers
RPR LOOP START / RPR LOOP END - Reaper (and anything reading RPR)
LoopBegin / LoopEnd - various game engines & trackers
All times are absolute ticks; use songkit.bar() to think in bars.
"""
self.loop = (int(start_tick), int(end_tick))
return self
def add_track(self, track): def add_track(self, track):
self.tracks.append(track) self.tracks.append(track)
@@ -271,6 +287,12 @@ class MidiFile:
conductor = Track("Conductor") conductor = Track("Conductor")
for mp in sorted(self.tempos, key=lambda m: m.tick): for mp in sorted(self.tempos, key=lambda m: m.tick):
conductor.meta(mp.tick, 0x51, struct.pack(">I", mp.mpqn)[1:]) conductor.meta(mp.tick, 0x51, struct.pack(">I", mp.mpqn)[1:])
if self.loop:
start, end = self.loop
for tick, names in ((start, ("LOOP_START", "RPR LOOP START", "LoopBegin")),
(end, ("LOOP_END", "RPR LOOP END", "LoopEnd"))):
for nm in names:
conductor.meta(tick, 0x06, nm.encode("latin-1"))
if self.time_sig: if self.time_sig:
conductor.meta(0, 0x58, bytes([self.time_sig[0], self.time_sig[1], 24, 8])) conductor.meta(0, 0x58, bytes([self.time_sig[0], self.time_sig[1], 24, 8]))
if self.key_sig: if self.key_sig:
@@ -303,7 +325,8 @@ def read(path_or_bytes):
tlen = struct.unpack(">I", data[pos + 4:pos + 8])[0] tlen = struct.unpack(">I", data[pos + 4:pos + 8])[0]
pos += 8 pos += 8
end = pos + tlen end = pos + tlen
track = {"notes": [], "tempos": [], "name": None, "programs": {}, "cc": [], "bends": []} track = {"notes": [], "tempos": [], "name": None, "programs": {},
"cc": [], "bends": [], "markers": []}
tick = 0 tick = 0
running = None running = None
while pos < end: while pos < end:
@@ -325,6 +348,8 @@ def read(path_or_bytes):
track["tempos"].append((tick, int.from_bytes(mdata, "big"))) track["tempos"].append((tick, int.from_bytes(mdata, "big")))
elif mtype == 0x03: elif mtype == 0x03:
track["name"] = mdata.decode("latin-1") track["name"] = mdata.decode("latin-1")
elif mtype == 0x06:
track["markers"].append((tick, mdata.decode("latin-1")))
elif mtype == 0x2F: elif mtype == 0x2F:
break break
elif status in (0xF0, 0xF7): elif status in (0xF0, 0xF7):
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+8
View File
@@ -26,6 +26,7 @@ d = Track("Drums", channel=9)
drums(d, 0, 2, "rock") drums(d, 0, 2, "rock")
mf.add_track(lead); mf.add_track(bass); mf.add_track(acomp); mf.add_track(d) mf.add_track(lead); mf.add_track(bass); mf.add_track(acomp); mf.add_track(d)
mf.tempos = [MPQN.from_bpm(0, 120)] mf.tempos = [MPQN.from_bpm(0, 120)]
mf.set_loop(bar(0), bar(2)) # loop the whole 2-bar demo
path = "/tmp/midikit_smoke.mid" path = "/tmp/midikit_smoke.mid"
mf.write(path) mf.write(path)
@@ -34,6 +35,13 @@ print("OK:", path, d["duration_sec"], "sec,",
sum(len(t["notes"]) for t in d["tracks"]), "notes,", sum(len(t["notes"]) for t in d["tracks"]), "notes,",
len(d["tracks"]), "tracks") len(d["tracks"]), "tracks")
# loop markers must survive the round-trip in all three conventions
markers = sorted(set(m[1] for t in d["tracks"] for m in t["markers"]))
expected = sorted(["LOOP_START", "LOOP_END", "RPR LOOP START", "RPR LOOP END",
"LoopBegin", "LoopEnd"])
assert markers == expected, "loop markers wrong: %s" % markers
print("loop markers OK:", ", ".join(markers))
# cross-check with mido if present (third-party parser as second opinion) # cross-check with mido if present (third-party parser as second opinion)
try: try:
import mido import mido
+5
View File
@@ -15,6 +15,11 @@ for t in tunes/*.py; do
python3 "$t" python3 "$t"
done done
for s in tunes/specs/*.json; do
echo "== $s"
python3 tools/make_tune.py "$s"
done
echo "== verifying MIDI" echo "== verifying MIDI"
python3 - <<'EOF' python3 - <<'EOF'
import sys, glob import sys, glob
+192
View File
@@ -0,0 +1,192 @@
#!/usr/bin/env python3
"""
make_tune.py — generate a fully-arranged retro MIDI from a JSON spec.
This is the agent-friendly front door: no Python needed, just describe
the tune. The style preset handles arrangement (instruments, accompaniment
patterns, drums, vibrato, mix levels); you supply chords and a melody.
Usage:
python3 tools/make_tune.py tunes/specs/my_tune.json
python3 tools/make_tune.py tunes/specs/my_tune.json --render # + WAV
Spec format (see tunes/specs/ for working examples):
{
"name": "04_tea_break_at_the_terminal", -> midi/<name>.mid
"style": "dos_adventure", # dos_adventure | n64_ambient | chiptune_racing
"bpm": 112,
"key": ["A", "minor"], # for the key signature meta
"chords": [["A","m"], ["F",""], ...], # one [root, quality] per bar
"melody_start_bar": 4,
"melody": "A4-8 C5-8 E5-4 A5-2 | ...", # bar-grid, '|' separators (linted 4/4)
"loop": {"start_bar": 4, "end_bar": 20}, # optional; LOOP_* markers
"counter": "...", # optional 2nd melody line (style-dependent)
"fills_at": [11, 19] # optional: drum fill at end of these bars
}
Chord qualities: maj m dim aug 7 maj7 m7 m7b5 6 m6 9 add9 sus2 sus4 sus
"""
import argparse, json, os, sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(HERE, "..", "lib"))
from midikit import MidiFile, Track, MPQN, verify, note as N
from songkit import (bar, melody, arp, bass_drive, drums, vibrato, crash,
parse_score, QUALITIES, TPQ)
KEY_PC = {"C": 0, "D": 2, "E": 4, "F": 5, "G": 7, "A": 9, "B": 11}
# circle of fifths lookup for key signature meta (flats negative, sharps positive)
MAJOR_KEYS = {"C": 0, "G": 1, "D": 2, "A": 3, "E": 4, "B": 5, "F#": 6, "C#": 7,
"F": -1, "Bb": -2, "Eb": -3, "Ab": -4, "Db": -5, "Gb": -6}
def key_sig_for(tonic, mode):
if mode in ("minor", "m"):
# relative major is 3 semitones up
pc = (KEY_PC[tonic.upper()] + 3) % 12
name = {0: "C", 2: "D", 4: "E", 5: "F", 7: "G", 9: "A", 11: "B"}.get(pc)
if name is None: # sharp-side minor keys
name = {1: "Db", 3: "Eb", 6: "F#", 8: "Ab", 10: "Bb"}[pc]
return MAJOR_KEYS[name], True
return MAJOR_KEYS[tonic.capitalize() if len(tonic) == 1 else tonic], False
# ---------------------------------------------------------------------------
# Style presets: (lead patch, accompaniment recipe)
# ---------------------------------------------------------------------------
def style_dos_adventure(mf, spec, chords):
n = len(chords)
lead = Track("Lead (square)", channel=0)
lead.program(0, "square lead"); lead.cc(0, 7, 100); lead.cc(0, 10, 54)
bass = Track("Bass", channel=2)
bass.program(0, "synth bass 1"); bass.cc(0, 7, 88); bass.cc(0, 10, 30)
acomp = Track("Arp (OPL)", channel=3)
acomp.program(0, "electric piano 1"); acomp.cc(0, 7, 64); acomp.cc(0, 10, 20)
d = Track("Drums", channel=9); d.cc(0, 7, 92)
mf.add_track(lead); mf.add_track(bass); mf.add_track(acomp); mf.add_track(d)
return dict(lead=lead, bass=bass, acomp=acomp, drums=d,
vibrato=(0.55, 5.5), arp_pattern="012321", arp_vel=70,
drums_pattern="rock", fill="dos_roll")
def style_n64_ambient(mf, spec, chords):
lead = Track("Pan flute", channel=0)
lead.program(0, "pan flute"); lead.cc(0, 7, 92); lead.cc(0, 10, 58)
pad = Track("Warm pad", channel=1)
pad.program(0, "warm pad"); pad.cc(0, 7, 56); pad.cc(0, 10, 40)
harp = Track("Harp", channel=2)
harp.program(0, "orchestral harp"); harp.cc(0, 7, 70); harp.cc(0, 10, 84)
bass = Track("Cello", channel=3)
bass.program(0, "cello"); bass.cc(0, 7, 78); bass.cc(0, 10, 34)
mf.add_track(lead); mf.add_track(pad); mf.add_track(harp); mf.add_track(bass)
# whole-note pads + root bass
for i, (root, qual) in enumerate(chords):
third = 3 if qual.startswith("m") or qual.startswith("dim") else 4
for iv in (0, third, 7):
pad.note(bar(i), N(root + "3") + iv, TPQ * 4, 55)
bass.note(bar(i), N(root + "2"), TPQ * 4, 70)
return dict(lead=lead, bass=bass, acomp=harp, drums=None,
vibrato=(0.4, 4.5), arp_pattern="0121", arp_vel=56,
subdivision=2, arp_dur=TPQ, drums_pattern=None, fill=None)
def style_chiptune_racing(mf, spec, chords):
lead = Track("Square lead", channel=0)
lead.program(0, "square lead"); lead.cc(0, 7, 90); lead.cc(0, 10, 52)
stabs = Track("Stabs", channel=2)
stabs.program(0, "electric piano 1"); stabs.cc(0, 7, 62); stabs.cc(0, 10, 24)
bass = Track("Bass", channel=3)
bass.program(0, "synth bass 2"); bass.cc(0, 7, 84); bass.cc(0, 10, 36)
d = Track("Drums", channel=9); d.cc(0, 7, 88)
mf.add_track(lead); mf.add_track(stabs); mf.add_track(bass); mf.add_track(d)
for i, (root, qual) in enumerate(chords):
for b in (0.5, 1.5, 2.5, 3.5):
for iv in QUALITIES[qual]:
stabs.note(bar(i, b), N(root + "4") + iv, TPQ // 2 - 40, 78)
return dict(lead=lead, bass=bass, acomp=None, drums=d,
vibrato=(0.45, 6.5), arp_pattern=None, arp_vel=0,
drums_pattern="drive", fill="dos_roll")
STYLES = {"dos_adventure": style_dos_adventure,
"n64_ambient": style_n64_ambient,
"chiptune_racing": style_chiptune_racing}
def build(spec):
chords = [(r, q) for r, q in spec["chords"]]
n = len(chords)
mf = MidiFile(tpq=TPQ)
mf.time_sig = (4, 2)
tonic, mode = spec.get("key", ["C", "major"])
mf.key_sig = key_sig_for(tonic, mode)
mf.tempos = [MPQN.from_bpm(0, spec["bpm"])]
st = STYLES[spec["style"]](mf, spec, chords)
start = spec.get("melody_start_bar", 0)
if spec.get("melody"):
melody(st["lead"], start, spec["melody"])
end_bar = start + len([s for s in spec["melody"].split("|") if s.strip()])
vibrato(st["lead"], bar(start), bar(min(end_bar, n)),
tempo_bpm=spec["bpm"], depth=st["vibrato"][0], rate=st["vibrato"][1])
if st.get("acomp") and st.get("arp_pattern"):
arp(st["acomp"], 0, chords, pattern=st["arp_pattern"],
velocity=st["arp_vel"],
subdivision=st.get("subdivision", 4),
duration=st.get("arp_dur"))
if st.get("bass") is not None and spec["style"] != "n64_ambient":
bass_drive(st["bass"], 0, chords, subdivision=2)
if st.get("drums") is not None and st.get("drums_pattern"):
drums(st["drums"], 0, n, st["drums_pattern"])
for b in spec.get("fills_at", []):
drums(st["drums"], b, 1, st["fill"] or "dos_roll", velocity_scale=0.85)
crash(st["drums"], bar(n - 1))
if spec.get("counter") and spec["style"] == "dos_adventure":
counter = Track("Counter (EP2)", channel=1)
counter.program(0, "electric piano 2")
counter.cc(0, 7, 80); counter.cc(0, 10, 90)
melody(counter, start, spec["counter"])
mf.add_track(counter)
if spec.get("loop"):
mf.set_loop(bar(spec["loop"]["start_bar"]), bar(spec["loop"]["end_bar"]))
return mf
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("spec", help="path to tune spec JSON")
ap.add_argument("--render", action="store_true", help="also render WAV via timidity")
args = ap.parse_args()
spec = json.load(open(args.spec))
mf = build(spec)
out = os.path.join(os.path.dirname(HERE), "midi", spec["name"] + ".mid")
os.makedirs(os.path.dirname(out), exist_ok=True)
mf.write(out)
d = verify(out)
markers = [m for t in d["tracks"] for m in t["markers"]]
print("OK:", out)
print(" %.1fs | %d notes | %d tracks | loop: %s" %
(d["duration_sec"], sum(len(t["notes"]) for t in d["tracks"]),
len(d["tracks"]),
"bars %d-%d" % (spec["loop"]["start_bar"], spec["loop"]["end_bar"])
if spec.get("loop") else "none"))
if markers:
print(" markers:", ", ".join(sorted(set(m[1] for m in markers))))
if args.render:
import subprocess
wav = os.path.join(os.path.dirname(HERE), "build", spec["name"] + ".wav")
os.makedirs(os.path.dirname(wav), exist_ok=True)
subprocess.run(["timidity", "-Ow", "-s", "44", out, "-o", wav],
check=True, capture_output=True)
subprocess.run(["python3", os.path.join(HERE, "check_wav.py"), wav], check=True)
if __name__ == "__main__":
main()
+1 -2
View File
@@ -132,8 +132,7 @@ def build():
mf.add_track(lead); mf.add_track(counter); mf.add_track(bass) mf.add_track(lead); mf.add_track(counter); mf.add_track(bass)
mf.add_track(acomp); mf.add_track(d) mf.add_track(acomp); mf.add_track(d)
# loop points for game engines: intro plays once, A-B-A' loops # loop points for game engines: intro plays once, A-B-A' loops
lead.marker(bar(4), "LOOP_START") mf.set_loop(bar(4), bar(28))
lead.marker(bar(28), "LOOP_END")
return mf return mf
+1 -2
View File
@@ -104,8 +104,7 @@ def build():
harp.note(bar(23), N(p), TPQ * 6, 40) harp.note(bar(23), N(p), TPQ * 6, 40)
mf.add_track(lead); mf.add_track(pad); mf.add_track(harp); mf.add_track(bass) mf.add_track(lead); mf.add_track(pad); mf.add_track(harp); mf.add_track(bass)
lead.marker(bar(4), "LOOP_START") mf.set_loop(bar(4), bar(20))
lead.marker(bar(20), "LOOP_END")
return mf return mf
+1 -2
View File
@@ -128,8 +128,7 @@ def build():
mf.add_track(lead); mf.add_track(harm); mf.add_track(stabs) mf.add_track(lead); mf.add_track(harm); mf.add_track(stabs)
mf.add_track(bass); mf.add_track(d) mf.add_track(bass); mf.add_track(d)
lead.marker(bar(2), "LOOP_START") mf.set_loop(bar(2), bar(26))
lead.marker(bar(26), "LOOP_END")
return mf return mf
@@ -0,0 +1,18 @@
{
"name": "04_tea_break_at_the_terminal",
"style": "dos_adventure",
"bpm": 112,
"key": ["A", "minor"],
"loop": {"start_bar": 4, "end_bar": 20},
"fills_at": [11, 19],
"melody_start_bar": 4,
"chords": [
["A", "m"], ["F", ""], ["G", ""], ["A", "m"],
["A", "m"], ["A", "m"], ["F", ""], ["F", ""],
["G", ""], ["G", ""], ["A", "m"], ["A", "m"],
["A", "m"], ["A", "m"], ["F", ""], ["G", ""],
["A", "m"], ["G", ""], ["A", "m"], ["A", "m"],
["A", "m"], ["F", ""], ["G", ""], ["A", "m"]
],
"melody": "A4-8 C5-8 E5-4 A5-2 | G4-8 A4-8 C5-4 E5-2 | F5-8 E5-8 F5-8 A5-8 G5-4 F5-4 | E5-4 C5-4 F5-2 | D5-8 E5-8 G5-4 B5-2 | A5-8 B5-8 A5-8 G5-8 D5-4 G5-4 | C6-8 B5-8 C6-8 E6-8 D6-4 C6-4 | A5-2 E5-4 A5-4 | E5-8 G5-8 A5-4 C6-2 | B5-4 A5-4 E5-2 | A5-8 C6-8 A5-8 F5-8 E5-4 F5-4 | G5-8 A5-8 B5-4 D6-2 | C6-1 | B5-8 A5-8 G5-4 D5-2 | A5-8 G5-8 A5-8 C6-8 E6-4 D6-4 | A5-1"
}