Last active
November 8, 2016 20:24
-
-
Save nitori/81d6366e5a87a60d5bd6abd258d6aa20 to your computer and use it in GitHub Desktop.
Very basic MIDI file parser. Does nothing much.
This file contains 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
# sample output of a basic midi file https://gist.github.com/knitori/35a7d3a69189f35f655d9edb99b10b66 | |
import struct | |
import io | |
def track_reader(buf): | |
while True: | |
marker = buf.read(4) | |
if marker == b'': | |
return | |
assert marker == b'MTrk', 'Not a track chunk: {}'.format(ascii(marker)) | |
length = struct.unpack('>L', buf.read(4))[0] | |
track = io.BytesIO(buf.read(length)) | |
yield track | |
def read_var_int(buf): | |
result = 0 | |
while True: | |
byte = buf.read(1) | |
if byte == b'': | |
raise EOFError() | |
byte = byte[0] | |
result <<= 7 | |
result |= (byte & 0x7F) | |
if byte & 0x80 == 0: | |
return result | |
class Event: | |
_meta_type_map = { | |
0x00: 'Sequence number', | |
0x20: 'MIDI channel prefix assignment', | |
0x01: 'Text event', | |
0x2F: 'End of track', | |
0x02: 'Copyright notice', | |
0x51: 'Tempo setting', | |
0x03: 'Sequence or track name', | |
0x54: 'SMPTE offset', | |
0x04: 'Instrument name', | |
0x58: 'Time signature', | |
0x05: 'Lyric text', | |
0x59: 'Key signature', | |
0x06: 'Marker text', | |
0x7F: 'Sequencer specific event', | |
0x07: 'Cue point', | |
} | |
_system_map = { | |
0b0110: 'tune_request', | |
0b0111: 'end_of_exclusive', | |
0b1000: 'timing_clock', | |
0b1010: 'start', | |
0b1011: 'continue', | |
0b1100: 'stop', | |
0b1110: 'active_sensing', | |
0b1111: 'reset', | |
} | |
@classmethod | |
def from_buf(cls, buf): | |
delta_time = read_var_int(buf) | |
event = cls() | |
event.delta_time = delta_time | |
tp = buf.read(1)[0] | |
if tp == 0xFF: | |
event.type = 'meta' | |
event.meta_type = buf.read(1)[0] | |
v_length = read_var_int(buf) | |
event.data = buf.read(v_length) | |
event.name = cls._meta_type_map.get(event.meta_type, 'n/a') | |
return event | |
mask = (tp & 0xF0) >> 4 | |
if mask != 0b1111: | |
event.channel = tp & 0x0F | |
if mask == 0b1000: | |
event.type = 'note_off' | |
event.key = buf.read(1)[0] & 0x7F | |
event.velocity = buf.read(1)[0] & 0x7F | |
elif mask == 0b1001: | |
event.type = 'note_on' | |
event.key = buf.read(1)[0] & 0x7F | |
event.velocity = buf.read(1)[0] & 0x7F | |
elif mask == 0b1010: | |
event.type = 'polyphonic_key_pressure' | |
event.key = buf.read(1)[0] & 0x7F | |
event.pressure = buf.read(1)[0] & 0x7F | |
elif mask == 0b1011: | |
c = buf.read(1)[0] & 0x7F | |
v = buf.read(1)[0] & 0x7F | |
if 122 <= c <= 127: | |
event.type = 'channel_mode_message' | |
else: | |
event.type = 'control_change' | |
event.control_number = c | |
event.value = v | |
elif mask == 0b1100: | |
event.type = 'program_change' | |
event.program_number = buf.read(1)[0] & 0x7F | |
elif mask == 0b1101: | |
event.type = 'channel_pressure' | |
event.pressure = buf.read(1)[0] & 0x7F | |
elif mask == 0b1110: | |
event.type = 'pitch_wheel_change' | |
a, b = buf.read(2) | |
event.value = ((b & 0x7F) << 7) | (a & 0x7F) | |
elif mask == 0b1111: | |
smask = tp & 0x0F | |
if smask == 0x0: | |
# vendor specific | |
event.type = 'system_exclusive' | |
event.id = buf.read(1)[0] & 0x7F | |
event.data = [] | |
while True: | |
tmp = buf.read(1)[0] | |
if tmp == 0b11110111: | |
break | |
event.data.append(tmp) | |
elif smask == 0b0010: | |
event.type = 'song_position_pointer' | |
a, b = buf.read(2) | |
event.value = ((b & 0x7F) << 7) | (a & 0x7F) | |
elif smask == 0b0011: | |
event.type = 'song_select' | |
event.song = buf.read(1)[0] & 0x7F | |
else: | |
event.type = cls._system_map.get(smask, '- undefined -') | |
else: | |
raise Exception('wut {:08b}'.format(tp)) | |
return event | |
def __repr__(self): | |
args = [arg for arg in dir(self) if not arg.startswith('_') | |
and not callable(getattr(self, arg)) | |
and arg != 'type'] | |
args.sort(key=str.lower) | |
if not args: | |
return 'Event(type={!r})'.format(self.type) | |
args_str = ', '.join('{}={!r}'.format(arg, getattr(self, arg)) | |
for arg in args) | |
return 'Event(type={!r}, {})'.format( | |
self.type, args_str) | |
def event_reader(buf): | |
while True: | |
try: | |
event = Event.from_buf(buf) | |
except EOFError: | |
return | |
yield event | |
def main(filename): | |
with open(filename, 'rb') as f: | |
raw_data = f.read() | |
buf = io.BytesIO(raw_data) | |
if buf.read(4) != b'MThd': | |
raise TypeError('Incompatible file type') | |
header_length = struct.unpack('>L', buf.read(4))[0] | |
assert header_length == 6, 'Should be 6.' | |
fmt, num, division = struct.unpack('>HHH', buf.read(6)) | |
if fmt == 0: | |
print('Format: Single Track.') | |
elif fmt == 1: | |
print('Format: Multi Track.') | |
elif fmt == 2: | |
print('Format: MUlti Song.') | |
print('Number of Tracks: {}'.format(num)) | |
if division > 0: | |
print('{} ticks per beat.'.format(division)) | |
else: | |
print('SMPTE Timecode value {} (dunno how to do).'.format(division)) | |
for track in track_reader(buf): | |
print('New Track Chunk!') | |
for event in event_reader(track): | |
print(event) | |
if __name__ == '__main__': | |
main('foobar.mid') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment