Skip to content

Instantly share code, notes, and snippets.

@nitori
Last active November 8, 2016 20:24
Show Gist options
  • Save nitori/81d6366e5a87a60d5bd6abd258d6aa20 to your computer and use it in GitHub Desktop.
Save nitori/81d6366e5a87a60d5bd6abd258d6aa20 to your computer and use it in GitHub Desktop.
Very basic MIDI file parser. Does nothing much.
# 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