import struct from dataclasses import dataclass from typing import * import enum def parse_variable_length(buf): n = 0 i = 0 while i < 4: n <<= 7 b = buf[i] i += 1 n |= (b & 0x7f) if not b & 0x80: break else: assert False, bytes(buf[0:5]) return buf[i:], n def parse_header_chunk_type(buf): assert buf[0] == ord('M'), bytes(buf[0:4]) assert buf[1] == ord('T'), bytes(buf[0:4]) assert buf[2] == ord('h'), bytes(buf[0:4]) assert buf[3] == ord('d'), bytes(buf[0:4]) return buf[4:], None def parse_uint32(buf): n, = struct.unpack('>L', buf[:4]) return buf[4:], n def parse_uint16(buf): n, = struct.unpack('>H', buf[:2]) return buf[2:], n @dataclass class MetricalDivision: ticks_per_quarter_note: int @dataclass class TimeCodeDivision: smpte: int ticks_per_frame: int @dataclass class Header: format: int ntrks: int division: Union[MetricalDivision, TimeCodeDivision] def parse_division(buf): buf, n = parse_uint16(buf) if n & (1 << 15): smpte = ((n >> 8) & 0x7f) - 0x80 # sign-extend ticks_per_frame = n & 0xff division = TimeCodeDivision(smpte, ticks_per_frame) else: ticks_per_quarter_note = n division = MetricalDivision(ticks_per_quarter_note) return buf, division def parse_header(buf): buf, _ = parse_header_chunk_type(buf) buf, length = parse_uint32(buf) assert length == 6, length buf, format = parse_uint16(buf) assert format in {0, 1, 2}, format buf, ntrks = parse_uint16(buf) buf, division = parse_division(buf) return buf, Header(format, ntrks, division) @dataclass class NoteOff: channel: int note: int velocity: int @dataclass class NoteOn: channel: int note: int velocity: int @dataclass class PolyphonicKeyPressure: channel: int note: int pressure: int control_change_description = dict([ (0x00, "Bank Select"), (0x01, "Modulation wheel or lever"), (0x02, "Breath Controller"), (0x04, "Foot controller"), (0x05, "Portamento time"), (0x06, "Data entry MSB"), (0x07, "Channel Volume"), (0x08, "Balance"), (0x0A, "Pan"), (0x0B, "Expression Controller"), (0x0C, "Effect Control 1"), (0x0D, "Effect Control 2"), (0x10, "General Purpose Controller 1"), (0x11, "General Purpose Controller 2"), (0x12, "General Purpose Controller 3"), (0x13, "General Purpose Controller 4"), (0x40, "Damper pedal (sustain)"), (0x41, "Portamento On/Off"), (0x42, "Sostenuto"), (0x43, "Soft pedal"), (0x44, "Legato Footswitch (vv = 00-3F:Normal, 40-7F=Legatto)"), (0x45, "Hold 2"), (0x46, "Sound Controller 1 (default: Sound Variation)"), (0x47, "Sound Controller 2 (default: Timbre/Harmonic Intensity)"), (0x48, "Sound Controller 3 (default: Release Time)"), (0x49, "Sound Controller 4 (default: Attack Time)"), (0x4A, "Sound Controller 5 (default: Brightness)"), (0x4B, "Sound Controller 6 (default: no default)"), (0x4C, "Sound Controller 7 (default: no default)"), (0x4D, "Sound Controller 8 (default: no default)"), (0x4E, "Sound Controller 9 (default: no default)"), (0x4F, "Sound Controller 10 (default: no default)"), (0x50, "General Purpose Controller 5"), (0x51, "General Purpose Controller 6"), (0x52, "General Purpose Controller 7"), (0x53, "General Purpose Controller 9"), (0x54, "Portamento Control"), (0x5B, "Effects 1 Depth"), (0x5C, "Effects 2 Depth"), (0x5D, "Effects 3 Depth"), (0x5E, "Effects 4 Depth"), (0x5F, "Effects 5 Depth"), (0x60, "Data increment"), (0x61, "Data decrement"), (0x62, "Non-Registered Parameter Number LSB"), (0x63, "Non-Registered Parameter Number MSB"), (0x64, "Registered Parameter Number LSB"), (0x65, "Registered Parameter Number MSB"), ]) @dataclass class ControlChange: channel: int control: int value: int def __init__(self, channel, control, value): self.channel = channel self.control = (control, control_change_description.get(control, "Undefined")) self.value = value @dataclass class ProgramChange: channel: int program: int @dataclass class ChannelPressure: channel: int pressure: int @dataclass class PitchBendChange: channel: int lsb: int msb: int @dataclass class ChannelMode: channel: int controller: int value: int @dataclass class MIDIEvent: event: Union[ NoteOff, NoteOn, PolyphonicKeyPressure, ControlChange, ProgramChange, ChannelPressure, PitchBendChange, ChannelMode, ] @dataclass class SysexEvent: data: bytes class MetaType(enum.Enum): SequenceNumber = enum.auto() TextEvent = enum.auto() CopyrightNotice = enum.auto() SequenceTrackName = enum.auto() InstrumentName = enum.auto() Lyric = enum.auto() Marker = enum.auto() CuePoint = enum.auto() MIDIChannelPrefix = enum.auto() EndOfTrack = enum.auto() SetTempo = enum.auto() SMPTEOffset = enum.auto() TimeSignature = enum.auto() KeySignature = enum.auto() SequencerSpecific = enum.auto() def __repr__(self): return self.name def _sequence_number(b): assert len(b) == 2, b n, = struct.unpack('= 121 and data[0] <= 127: # 0xb0 is overloaded for both control change and channel mode cls = ChannelMode message = MIDIEvent(cls(channel, *data)) buf = buf[message_length:] return buf, message def parse_event(buf): if (midi := parse_midi_event(buf)) is not None: buf, midi = midi return buf, midi elif (sysex := parse_sysex_event(buf)) is not None: running_status = None buf, sysex = sysex return buf, sysex elif (meta := parse_meta_event(buf)) is not None: running_status = None buf, meta = meta return buf, meta else: assert False, ' '.join([hex(i)[2:] for i in buf[0:40]]) return None @dataclass class Event: delta_time: int event: Union[MIDIEvent, SysexEvent, MetaEvent] def parse_mtrk_event(buf): buf, delta_time = parse_variable_length(buf) buf, event = parse_event(buf) return buf, Event(delta_time, event) def parse_track_chunk_type(buf): assert buf[0] == ord('M'), bytes(buf[0:4]) assert buf[1] == ord('T'), bytes(buf[0:4]) assert buf[2] == ord('r'), bytes(buf[0:4]) assert buf[3] == ord('k'), bytes(buf[0:4]) return buf[4:], None @dataclass class Track: events: list[Event] def parse_track(buf): head_buf = buf buf, _ = parse_track_chunk_type(buf) buf, length = parse_uint32(buf) offset = len(buf) events = [] while (offset - len(buf)) < length: try: buf, event = parse_mtrk_event(buf) except: print('len', len(head_buf) - len(buf), length) raise events.append(event) return buf, Track(events)