# This file is part of LilyPond, the GNU music typesetter. # # Copyright (C) 2001--2020 Han-Wen Nienhuys # Jan Nieuwenhuizen # # # LilyPond is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # LilyPond is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with LilyPond. If not, see . # import midi # s = open ("s.midi").read () # midi.parse_track (s) # midi.parse (s) # # # returns a MIDI file as the tuple # # ((format, division), TRACKLIST) # division (>0) = TPQN*4 # # or (<0) TBD # # each track is an EVENTLIST, where EVENT is # # (time, (type, ARG1, [ARG2])) # time = cumulative delta time # MIDI event: # type = MIDI status+channel >= x80 # META-event = xFF: # type = meta-event type <= x7F # ARG1 = length # ARG2 = data import array import struct class error (Exception): pass # class warning (Exception): pass def _add_constants(): channelVoiceMessages = ( (0x80, "NOTE_OFF"), (0x90, "NOTE_ON"), (0xA0, "POLYPHONIC_KEY_PRESSURE"), (0xB0, "CONTROLLER_CHANGE"), (0xC0, "PROGRAM_CHANGE"), (0xD0, "CHANNEL_KEY_PRESSURE"), (0xE0, "PITCH_BEND"), ) channelModeMessages = ( (0x78, "ALL_SOUND_OFF"), (0x79, "RESET_ALL_CONTROLLERS"), (0x7A, "LOCAL_CONTROL"), (0x7B, "ALL_NOTES_OFF"), (0x7C, "OMNI_MODE_OFF"), (0x7D, "OMNI_MODE_ON"), (0x7E, "MONO_MODE_ON"), (0x7F, "POLY_MODE_ON"), ) metaEvents = ( (0x00, "SEQUENCE_NUMBER"), (0x01, "TEXT_EVENT"), (0x02, "COPYRIGHT_NOTICE"), (0x03, "SEQUENCE_TRACK_NAME"), (0x04, "INSTRUMENT_NAME"), (0x05, "LYRIC"), # renamed LYRIC_DISPLAY MIDI RP-26 (0x06, "MARKER"), (0x07, "CUE_POINT"), (0x08, "PROGRAM_NAME"), # added MIDI RP-19 (0X09, "DEVICE_NAME"), # added MIDI RP-19 (0x20, "MIDI_CHANNEL_PREFIX"), (0x21, "MIDI_PORT"), (0x2F, "END_OF_TRACK"), (0x51, "SET_TEMPO"), (0x54, "SMTPE_OFFSET"), (0x58, "TIME_SIGNATURE"), (0x59, "KEY_SIGNATURE"), (0x60, "XMF_PATCH_TYPE_PREFIX"), # added MIDI RP-32 (0x7F, "SEQUENCER_SPECIFIC_META_EVENT"), (0xFF, "META_EVENT"), ) globals().update((desc, msg) for msg, desc in channelVoiceMessages + channelModeMessages + metaEvents) _add_constants() def _get_variable_length_number(nextbyte, getbyte): sum = 0 while nextbyte >= 0x80: sum = (sum + (nextbyte & 0x7F)) << 7 nextbyte = getbyte() return sum + nextbyte def _first_command_is_repeat(status, nextbyte, getbyte): raise error('the first midi command in the track is a repeat') def _read_two_bytes(status, nextbyte, getbyte): return status, nextbyte def _read_three_bytes(status, nextbyte, getbyte): return status, nextbyte, getbyte() def _read_string(nextbyte, getbyte): length = _get_variable_length_number(nextbyte, getbyte) return ''.join(chr(getbyte()) for i in range(length)) def _read_f0_byte(status, nextbyte, getbyte): if status == 0xff: return status, nextbyte, _read_string(getbyte(), getbyte) return status, _read_string(nextbyte, getbyte) _read_midi_event = ( _first_command_is_repeat, # 0 None, # 10 None, # 20 None, # 30 None, # 40 None, # 50 None, # 60 data entry??? None, # 70 all notes off??? _read_three_bytes, # 80 note off _read_three_bytes, # 90 note on _read_three_bytes, # a0 poly aftertouch _read_three_bytes, # b0 control _read_two_bytes, # c0 prog change _read_two_bytes, # d0 ch aftertouch _read_three_bytes, # e0 pitchwheel range _read_f0_byte, # f0 ) def _parse_track_body(data, clocks_max): # This seems to be the fastest way of getting bytes in order as integers. dataiter = iter(array.array('B', data)) getbyte = dataiter.__next__ time = 0 status = 0 try: for nextbyte in dataiter: time += _get_variable_length_number(nextbyte, getbyte) if clocks_max and time > clocks_max: break nextbyte = getbyte() if nextbyte >= 0x80: status = nextbyte nextbyte = getbyte() yield time, _read_midi_event[status >> 4](status, nextbyte, getbyte) except StopIteration: # If the track ended just before the start of an event, the for loop # will exit normally. If it ends anywhere else, we end up here. print(len(list(dataiter))) raise error('a track ended in the middle of a MIDI command') def _parse_hunk(data, pos, type, magic): if data[pos:pos+4] != magic: raise error('expected %r, got %r' % (magic, data[pos:pos+4])) try: length, = struct.unpack('>I', data[pos+4:pos+8]) except struct.error: raise error( 'the %s header is truncated (may be an incomplete download)' % type) endpos = pos + 8 + length data = data[pos+8:endpos] if len(data) != length: raise error( 'the %s is truncated (may be an incomplete download)' % type) return data, endpos def _parse_tracks(midi, pos, num_tracks, clocks_max): if num_tracks > 256: raise error('too many tracks: %d' % num_tracks) for i in range(num_tracks): trackdata, pos = _parse_hunk(midi, pos, 'track', b'MTrk') yield list(_parse_track_body(trackdata, clocks_max)) # if pos < len(midi): # warn def parse_track(track, clocks_max=None): track_body, end = _parse_hunk(track, 0, 'track', b'MTrk') # if end < len(track): # warn return list(_parse_track_body(track_body, clocks_max)) def parse(midi, clocks_max=None): header, first_track_pos = _parse_hunk(midi, 0, 'file', b'MThd') try: format, num_tracks, division = struct.unpack('>3H', header[:6]) except struct.error: raise error('the file header is too short') # if division < 0: # raise error ('cannot handle non-metrical time') tracks = list(_parse_tracks(midi, first_track_pos, num_tracks, clocks_max)) return (format, division*4), tracks