Created
June 19, 2024 14:54
-
-
Save RavuAlHemio/ce156c80f15acc5eab408470bf8f7990 to your computer and use it in GitHub Desktop.
LittleWing MIDI resource to Standard MIDI Format file converter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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