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:
1 parent
c3cd91d04f
commit
633eb92352
15 files changed
+387
-9
No files matched your search
+26
-1
@@ -253,6 +253,22 @@ class MidiFile:
|
||||
self.tempos = [] # list[MPQN]
|
||||
self.key_sig = None # (flats_sharps, minor_bool)
|
||||
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):
|
||||
self.tracks.append(track)
|
||||
@@ -271,6 +287,12 @@ class MidiFile:
|
||||
conductor = Track("Conductor")
|
||||
for mp in sorted(self.tempos, key=lambda m: m.tick):
|
||||
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:
|
||||
conductor.meta(0, 0x58, bytes([self.time_sig[0], self.time_sig[1], 24, 8]))
|
||||
if self.key_sig:
|
||||
@@ -303,7 +325,8 @@ def read(path_or_bytes):
|
||||
tlen = struct.unpack(">I", data[pos + 4:pos + 8])[0]
|
||||
pos += 8
|
||||
end = pos + tlen
|
||||
track = {"notes": [], "tempos": [], "name": None, "programs": {}, "cc": [], "bends": []}
|
||||
track = {"notes": [], "tempos": [], "name": None, "programs": {},
|
||||
"cc": [], "bends": [], "markers": []}
|
||||
tick = 0
|
||||
running = None
|
||||
while pos < end:
|
||||
@@ -325,6 +348,8 @@ def read(path_or_bytes):
|
||||
track["tempos"].append((tick, int.from_bytes(mdata, "big")))
|
||||
elif mtype == 0x03:
|
||||
track["name"] = mdata.decode("latin-1")
|
||||
elif mtype == 0x06:
|
||||
track["markers"].append((tick, mdata.decode("latin-1")))
|
||||
elif mtype == 0x2F:
|
||||
break
|
||||
elif status in (0xF0, 0xF7):
|
||||
|
||||
Reference in new issue
Block a user