Skip to content

Instantly share code, notes, and snippets.

@RavuAlHemio
Created June 19, 2024 14:54
Show Gist options
  • Save RavuAlHemio/ce156c80f15acc5eab408470bf8f7990 to your computer and use it in GitHub Desktop.
Save RavuAlHemio/ce156c80f15acc5eab408470bf8f7990 to your computer and use it in GitHub Desktop.
LittleWing MIDI resource to Standard MIDI Format file converter
#!/usr/bin/env python3
import binascii
import io
import struct
import sys
from typing import NamedTuple
class MidiEvent(NamedTuple):
time: int
command: bytes
file_name: str
offset: int
def int_to_vlq(number: int) -> bytes:
if number == 0x00:
return bytes((0,))
bs = bytearray()
while number > 0:
b = (number & 0b0111_1111)
number = number >> 7
bs.append(b)
bs.reverse()
for i in range(len(bs)-1):
bs[i] = bs[i] | 0b1000_0000
return bytes(bs)
def main():
if len(sys.argv) < 3:
print("Usage: INFILE... OUTFILE", file=sys.stderr)
sys.exit(1)
events = []
for name in sys.argv[1:-1]:
with open(name, "rb") as f:
offset = 0
while True:
chunk = f.read(8)
if not chunk:
break
if len(chunk) != 8:
raise ValueError("short read")
(time, command) = struct.unpack("<L4s", chunk)
event = MidiEvent(time, command, name, offset)
events.append(event)
offset += 8
with io.BytesIO() as f:
f.write(b"MThd")
f.write(struct.pack(">LHHH", 6, 0, 1, 480))
track_bytes = bytearray()
# initial SysEx message (GS RESET)
track_bytes.extend(b"\x00\xF0\x7E\x7F\x09\x01\xF7")
cur_time = 0
last_cmd = 0x00
for event in events:
if event.time == 0:
# new file
cur_time = 0
delta_time = event.time - cur_time
assert delta_time >= 0
cur_time = event.time
delta_time_vlq = int_to_vlq(delta_time)
if event.command[0] & 0b1000_0000 == 0x00:
if last_cmd == 0x00:
# never mind then
continue
full_event_command = bytes((last_cmd,)) + event.command
else:
full_event_command = event.command
event_nibble = ((full_event_command[0] >> 4) & 0x0F)
if event_nibble in (0x8, 0x9, 0xA, 0xB, 0xE):
# note off, note on, aftertouch, control change, pitch wheel
event_length = 3
elif event_nibble in (0xC, 0xD):
# program change, channel pressure
event_length = 2
elif event_nibble == 0xF:
print(f"skipping variable length event in {event.file_name} at 0x{event.offset:08X}")
continue
else:
raise ValueError("unknown event nibble: " + hex(event_nibble))
event_bytes = full_event_command[:event_length]
print(binascii.hexlify(delta_time_vlq) + b" " + binascii.hexlify(event_bytes))
track_bytes.extend(delta_time_vlq)
track_bytes.extend(event_bytes)
last_cmd = full_event_command[0]
# end of track
track_bytes.extend(b"\x00\xFF\x2F\x00")
f.write(b"MTrk")
f.write(struct.pack(">L", len(track_bytes)))
f.write(track_bytes)
f.seek(0)
with open(sys.argv[-1], "wb") as f2:
f2.write(f.read())
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment