From d8677d345032752b564878e5da0ea17b112afcad Mon Sep 17 00:00:00 2001 From: Ben Rudiak-Gould Date: Tue, 17 Jan 2017 19:52:31 -0800 Subject: [PATCH] Add rewrite of midi.c in python Work was done in 2012, and came from here: https://codereview.appspot.com/7016046/ --- python/midi.py | 199 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 python/midi.py diff --git a/python/midi.py b/python/midi.py new file mode 100644 index 0000000000..45c2553a15 --- /dev/null +++ b/python/midi.py @@ -0,0 +1,199 @@ +# This file is part of LilyPond, the GNU music typesetter. +# +# Copyright (C) 2001--2012 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 xrange(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 xrange(num_tracks): + trackdata, pos = _parse_hunk (midi, pos, 'track', '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', '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', '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 -- 2.39.2