Spaces:
Sleeping
Sleeping
File size: 6,744 Bytes
f65fe85 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 |
# This file is part of LilyPond, the GNU music typesetter.
#
# Copyright (C) 2001--2020 Han-Wen Nienhuys <[email protected]>
# Jan Nieuwenhuizen <[email protected]>
#
#
# 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 <http://www.gnu.org/licenses/>.
# 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
|