Chickaboo/Advanced-MIDI-Renderer
1
1#! /usr/bin/python32 3r'''4###############################################################################5#6# Tegridy MIDI X Module (TMIDI X / tee-midi eks)7#8# NOTE: TMIDI X Module starts after the partial MIDI.py module @ line 14619#10# Based upon MIDI.py module v.6.7. by Peter Billam / pjb.com.au11#12# Project Los Angeles13#14# Tegridy Code 202615#16# https://github.com/Tegridy-Code/Project-Los-Angeles17#18###################################################################################19#20# Copyright 2026 Project Los Angeles / Tegridy Code21#22# Licensed under the Apache License, Version 2.0 (the "License");23# you may not use this file except in compliance with the License.24# You may obtain a copy of the License at25#26# http://www.apache.org/licenses/LICENSE-2.027#28# Unless required by applicable law or agreed to in writing, software29# distributed under the License is distributed on an "AS IS" BASIS,30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.31# See the License for the specific language governing permissions and32# limitations under the License.33#34###################################################################################35#36# PARTIAL MIDI.py Module v.6.7. by Peter Billam37# Please see TMIDI 2.3/tegridy-tools repo for full MIDI.py module code38# 39# Or you can always download the latest full version from:40#41# https://pjb.com.au/42# https://peterbillam.gitlab.io/miditools/43# 44# Copyright 2020 Peter Billam45#46###################################################################################47'''48 49###################################################################################50 51__version__ = "26.5.19" # TMIDIX version52 53###################################################################################54 55print('=' * 70)56print('TMIDIX Python module')57print('Version:', __version__)58print('=' * 70)59print('Loading module...')60 61###################################################################################62 63import sys, struct, copy64 65###################################################################################66 67Version = '6.7'68VersionDate = '20201120'69 70###################################################################################71 72_previous_warning = '' # 5.473_previous_times = 0 # 5.474_no_warning = False75 76###################################################################################77 78def set_no_warning(value: bool):79 global _no_warning80 _no_warning = value81 82###################################################################################83 84#------------------------------- Encoding stuff --------------------------85 86def opus2midi(opus=[], text_encoding='ISO-8859-1'):87 r'''The argument is a list: the first item in the list is the "ticks"88parameter, the others are the tracks. Each track is a list89of midi-events, and each event is itself a list; see above.90opus2midi() returns a bytestring of the MIDI, which can then be91written either to a file opened in binary mode (mode='wb'),92or to stdout by means of: sys.stdout.buffer.write()93 94my_opus = [95 96, 96 [ # track 0:97 ['patch_change', 0, 1, 8], # and these are the events...98 ['note_on', 5, 1, 25, 96],99 ['note_off', 96, 1, 25, 0],100 ['note_on', 0, 1, 29, 96],101 ['note_off', 96, 1, 29, 0],102 ], # end of track 0103]104my_midi = opus2midi(my_opus)105sys.stdout.buffer.write(my_midi)106'''107 if len(opus) < 2:108 opus=[1000, [],]109 tracks = copy.deepcopy(opus)110 ticks = int(tracks.pop(0))111 ntracks = len(tracks)112 if ntracks == 1:113 format = 0114 else:115 format = 1116 117 my_midi = b"MThd\x00\x00\x00\x06"+struct.pack('>HHH',format,ntracks,ticks)118 for track in tracks:119 events = _encode(track, text_encoding=text_encoding)120 my_midi += b'MTrk' + struct.pack('>I',len(events)) + events121 _clean_up_warnings()122 return my_midi123 124 125def score2opus(score=None, text_encoding='ISO-8859-1'):126 r'''127The argument is a list: the first item in the list is the "ticks"128parameter, the others are the tracks. Each track is a list129of score-events, and each event is itself a list. A score-event130is similar to an opus-event (see above), except that in a score:131 1) the times are expressed as an absolute number of ticks132 from the track's start time133 2) the pairs of 'note_on' and 'note_off' events in an "opus"134 are abstracted into a single 'note' event in a "score":135 ['note', start_time, duration, channel, pitch, velocity]136score2opus() returns a list specifying the equivalent "opus".137 138my_score = [139 96,140 [ # track 0:141 ['patch_change', 0, 1, 8],142 ['note', 5, 96, 1, 25, 96],143 ['note', 101, 96, 1, 29, 96]144 ], # end of track 0145]146my_opus = score2opus(my_score)147'''148 if len(score) < 2:149 score=[1000, [],]150 tracks = copy.deepcopy(score)151 ticks = int(tracks.pop(0))152 opus_tracks = []153 for scoretrack in tracks:154 time2events = dict([])155 for scoreevent in scoretrack:156 if scoreevent[0] == 'note':157 note_on_event = ['note_on',scoreevent[1],158 scoreevent[3],scoreevent[4],scoreevent[5]]159 note_off_event = ['note_off',scoreevent[1]+scoreevent[2],160 scoreevent[3],scoreevent[4],scoreevent[5]]161 if time2events.get(note_on_event[1]):162 time2events[note_on_event[1]].append(note_on_event)163 else:164 time2events[note_on_event[1]] = [note_on_event,]165 if time2events.get(note_off_event[1]):166 time2events[note_off_event[1]].append(note_off_event)167 else:168 time2events[note_off_event[1]] = [note_off_event,]169 continue170 if time2events.get(scoreevent[1]):171 time2events[scoreevent[1]].append(scoreevent)172 else:173 time2events[scoreevent[1]] = [scoreevent,]174 175 sorted_times = [] # list of keys176 for k in time2events.keys():177 sorted_times.append(k)178 sorted_times.sort()179 180 sorted_events = [] # once-flattened list of values sorted by key181 for time in sorted_times:182 sorted_events.extend(time2events[time])183 184 abs_time = 0185 for event in sorted_events: # convert abs times => delta times186 delta_time = event[1] - abs_time187 abs_time = event[1]188 event[1] = delta_time189 opus_tracks.append(sorted_events)190 opus_tracks.insert(0,ticks)191 _clean_up_warnings()192 return opus_tracks193 194def score2midi(score=None, text_encoding='ISO-8859-1'):195 r'''196Translates a "score" into MIDI, using score2opus() then opus2midi()197'''198 return opus2midi(score2opus(score, text_encoding), text_encoding)199 200#--------------------------- Decoding stuff ------------------------201 202def midi2opus(midi=b'', do_not_check_MIDI_signature=False):203 r'''Translates MIDI into a "opus". For a description of the204"opus" format, see opus2midi()205'''206 my_midi=bytearray(midi)207 if len(my_midi) < 4:208 _clean_up_warnings()209 return [1000,[],]210 id = bytes(my_midi[0:4])211 if id != b'MThd':212 _warn("midi2opus: midi starts with "+str(id)+" instead of 'MThd'")213 _clean_up_warnings()214 if do_not_check_MIDI_signature == False:215 return [1000,[],]216 [length, format, tracks_expected, ticks] = struct.unpack(217 '>IHHH', bytes(my_midi[4:14]))218 if length != 6:219 _warn("midi2opus: midi header length was "+str(length)+" instead of 6")220 _clean_up_warnings()221 return [1000,[],]222 my_opus = [ticks,]223 my_midi = my_midi[14:]224 track_num = 1 # 5.1225 while len(my_midi) >= 8:226 track_type = bytes(my_midi[0:4])227 if track_type != b'MTrk':228 #_warn('midi2opus: Warning: track #'+str(track_num)+' type is '+str(track_type)+" instead of b'MTrk'")229 pass230 [track_length] = struct.unpack('>I', my_midi[4:8])231 my_midi = my_midi[8:]232 if track_length > len(my_midi):233 _warn('midi2opus: track #'+str(track_num)+' length '+str(track_length)+' is too large')234 _clean_up_warnings()235 return my_opus # 5.0236 my_midi_track = my_midi[0:track_length]237 my_track = _decode(my_midi_track)238 my_opus.append(my_track)239 my_midi = my_midi[track_length:]240 track_num += 1 # 5.1241 _clean_up_warnings()242 return my_opus243 244def opus2score(opus=[]):245 r'''For a description of the "opus" and "score" formats,246see opus2midi() and score2opus().247'''248 if len(opus) < 2:249 _clean_up_warnings()250 return [1000,[],]251 tracks = copy.deepcopy(opus) # couple of slices probably quicker...252 ticks = int(tracks.pop(0))253 score = [ticks,]254 for opus_track in tracks:255 ticks_so_far = 0256 score_track = []257 chapitch2note_on_events = dict([]) # 4.0258 for opus_event in opus_track:259 ticks_so_far += opus_event[1]260 if opus_event[0] == 'note_off' or (opus_event[0] == 'note_on' and opus_event[4] == 0): # 4.8261 cha = opus_event[2]262 pitch = opus_event[3]263 key = cha*128 + pitch264 if chapitch2note_on_events.get(key):265 new_event = chapitch2note_on_events[key].pop(0)266 new_event[2] = ticks_so_far - new_event[1]267 score_track.append(new_event)268 elif pitch > 127:269 pass #_warn('opus2score: note_off with no note_on, bad pitch='+str(pitch))270 else:271 pass #_warn('opus2score: note_off with no note_on cha='+str(cha)+' pitch='+str(pitch))272 elif opus_event[0] == 'note_on':273 cha = opus_event[2]274 pitch = opus_event[3]275 key = cha*128 + pitch276 new_event = ['note',ticks_so_far,0,cha,pitch, opus_event[4]]277 if chapitch2note_on_events.get(key):278 chapitch2note_on_events[key].append(new_event)279 else:280 chapitch2note_on_events[key] = [new_event,]281 else:282 opus_event[1] = ticks_so_far283 score_track.append(opus_event)284 # check for unterminated notes (Oisín) -- 5.2285 for chapitch in chapitch2note_on_events:286 note_on_events = chapitch2note_on_events[chapitch]287 for new_e in note_on_events:288 new_e[2] = ticks_so_far - new_e[1]289 score_track.append(new_e)290 pass #_warn("opus2score: note_on with no note_off cha="+str(new_e[3])+' pitch='+str(new_e[4])+'; adding note_off at end')291 score.append(score_track)292 _clean_up_warnings()293 return score294 295def midi2score(midi=b'', do_not_check_MIDI_signature=False):296 r'''297Translates MIDI into a "score", using midi2opus() then opus2score()298'''299 return opus2score(midi2opus(midi, do_not_check_MIDI_signature))300 301def midi2ms_score(midi=b'', do_not_check_MIDI_signature=False):302 r'''303Translates MIDI into a "score" with one beat per second and one304tick per millisecond, using midi2opus() then to_millisecs()305then opus2score()306'''307 return opus2score(to_millisecs(midi2opus(midi, do_not_check_MIDI_signature)))308 309def midi2single_track_ms_score(midi_path_or_bytes, 310 recalculate_channels = False, 311 pass_old_timings_events= False, 312 verbose = False, 313 do_not_check_MIDI_signature=False314 ):315 r'''316Translates MIDI into a single track "score" with 16 instruments and one beat per second and one317tick per millisecond318'''319 320 if type(midi_path_or_bytes) == bytes:321 midi_data = midi_path_or_bytes322 323 elif type(midi_path_or_bytes) == str:324 midi_data = open(midi_path_or_bytes, 'rb').read() 325 326 score = midi2score(midi_data, do_not_check_MIDI_signature)327 328 if recalculate_channels:329 330 events_matrixes = []331 332 itrack = 1333 events_matrixes_channels = []334 while itrack < len(score):335 events_matrix = []336 for event in score[itrack]:337 if event[0] == 'note' and event[3] != 9:338 event[3] = (16 * (itrack-1)) + event[3]339 if event[3] not in events_matrixes_channels:340 events_matrixes_channels.append(event[3])341 342 events_matrix.append(event)343 events_matrixes.append(events_matrix)344 itrack += 1345 346 events_matrix1 = []347 for e in events_matrixes:348 events_matrix1.extend(e)349 350 if verbose:351 if len(events_matrixes_channels) > 16:352 print('MIDI has', len(events_matrixes_channels), 'instruments!', len(events_matrixes_channels) - 16, 'instrument(s) will be removed!')353 354 for e in events_matrix1:355 if e[0] == 'note' and e[3] != 9:356 if e[3] in events_matrixes_channels[:15]:357 if events_matrixes_channels[:15].index(e[3]) < 9:358 e[3] = events_matrixes_channels[:15].index(e[3])359 else:360 e[3] = events_matrixes_channels[:15].index(e[3])+1361 else:362 events_matrix1.remove(e)363 364 if e[0] in ['patch_change', 'control_change', 'channel_after_touch', 'key_after_touch', 'pitch_wheel_change'] and e[2] != 9:365 if e[2] in [e % 16 for e in events_matrixes_channels[:15]]:366 if [e % 16 for e in events_matrixes_channels[:15]].index(e[2]) < 9:367 e[2] = [e % 16 for e in events_matrixes_channels[:15]].index(e[2])368 else:369 e[2] = [e % 16 for e in events_matrixes_channels[:15]].index(e[2])+1370 else:371 events_matrix1.remove(e)372 373 else:374 events_matrix1 = []375 itrack = 1376 377 while itrack < len(score):378 for event in score[itrack]:379 events_matrix1.append(event)380 itrack += 1 381 382 opus = score2opus([score[0], events_matrix1])383 ms_score = opus2score(to_millisecs(opus, pass_old_timings_events=pass_old_timings_events))384 385 return ms_score386 387#------------------------ Other Transformations ---------------------388 389def to_millisecs(old_opus=None, desired_time_in_ms=1, pass_old_timings_events = False):390 r'''Recallibrates all the times in an "opus" to use one beat391per second and one tick per millisecond. This makes it392hard to retrieve any information about beats or barlines,393but it does make it easy to mix different scores together.394'''395 if old_opus == None:396 return [1000 * desired_time_in_ms,[],]397 try:398 old_tpq = int(old_opus[0])399 except IndexError: # 5.0400 _warn('to_millisecs: the opus '+str(type(old_opus))+' has no elements')401 return [1000 * desired_time_in_ms,[],]402 new_opus = [1000 * desired_time_in_ms,]403 # 6.7 first go through building a table of set_tempos by absolute-tick404 ticks2tempo = {}405 itrack = 1406 while itrack < len(old_opus):407 ticks_so_far = 0408 for old_event in old_opus[itrack]:409 if old_event[0] == 'note':410 raise TypeError('to_millisecs needs an opus, not a score')411 ticks_so_far += old_event[1]412 if old_event[0] == 'set_tempo':413 ticks2tempo[ticks_so_far] = old_event[2]414 itrack += 1415 # then get the sorted-array of their keys416 tempo_ticks = [] # list of keys417 for k in ticks2tempo.keys():418 tempo_ticks.append(k)419 tempo_ticks.sort()420 # then go through converting to millisec, testing if the next421 # set_tempo lies before the next track-event, and using it if so.422 itrack = 1423 while itrack < len(old_opus):424 ms_per_old_tick = 400 / old_tpq # float: will round later 6.3425 i_tempo_ticks = 0426 ticks_so_far = 0427 ms_so_far = 0.0428 previous_ms_so_far = 0.0429 430 if pass_old_timings_events:431 new_track = [['set_tempo',0,1000000 * desired_time_in_ms],['old_tpq', 0, old_tpq]] # new "crochet" is 1 sec432 else:433 new_track = [['set_tempo',0,1000000 * desired_time_in_ms],] # new "crochet" is 1 sec434 for old_event in old_opus[itrack]:435 # detect if ticks2tempo has something before this event436 # 20160702 if ticks2tempo is at the same time, leave it437 event_delta_ticks = old_event[1] * desired_time_in_ms438 if (i_tempo_ticks < len(tempo_ticks) and439 tempo_ticks[i_tempo_ticks] < (ticks_so_far + old_event[1]) * desired_time_in_ms):440 delta_ticks = tempo_ticks[i_tempo_ticks] - ticks_so_far441 ms_so_far += (ms_per_old_tick * delta_ticks * desired_time_in_ms)442 ticks_so_far = tempo_ticks[i_tempo_ticks]443 ms_per_old_tick = ticks2tempo[ticks_so_far] / (1000.0*old_tpq * desired_time_in_ms)444 i_tempo_ticks += 1445 event_delta_ticks -= delta_ticks446 new_event = copy.deepcopy(old_event) # now handle the new event447 ms_so_far += (ms_per_old_tick * old_event[1] * desired_time_in_ms)448 new_event[1] = round(ms_so_far - previous_ms_so_far)449 450 if pass_old_timings_events:451 if old_event[0] != 'set_tempo':452 previous_ms_so_far = ms_so_far453 new_track.append(new_event)454 else:455 new_event[0] = 'old_set_tempo'456 previous_ms_so_far = ms_so_far457 new_track.append(new_event)458 else:459 if old_event[0] != 'set_tempo':460 previous_ms_so_far = ms_so_far461 new_track.append(new_event)462 ticks_so_far += event_delta_ticks463 new_opus.append(new_track)464 itrack += 1465 _clean_up_warnings()466 return new_opus467 468def event2alsaseq(event=None): # 5.5469 r'''Converts an event into the format needed by the alsaseq module,470http://pp.com.mx/python/alsaseq471The type of track (opus or score) is autodetected.472'''473 pass474 475def grep(score=None, channels=None):476 r'''Returns a "score" containing only the channels specified477'''478 if score == None:479 return [1000,[],]480 ticks = score[0]481 new_score = [ticks,]482 if channels == None:483 return new_score484 channels = set(channels)485 global Event2channelindex486 itrack = 1487 while itrack < len(score):488 new_score.append([])489 for event in score[itrack]:490 channel_index = Event2channelindex.get(event[0], False)491 if channel_index:492 if event[channel_index] in channels:493 new_score[itrack].append(event)494 else:495 new_score[itrack].append(event)496 itrack += 1497 return new_score498 499def score2stats(opus_or_score=None):500 r'''Returns a dict of some basic stats about the score, like501bank_select (list of tuples (msb,lsb)),502channels_by_track (list of lists), channels_total (set),503general_midi_mode (list),504ntracks, nticks, patch_changes_by_track (list of dicts),505num_notes_by_channel (list of numbers),506patch_changes_total (set),507percussion (dict histogram of channel 9 events),508pitches (dict histogram of pitches on channels other than 9),509pitch_range_by_track (list, by track, of two-member-tuples),510pitch_range_sum (sum over tracks of the pitch_ranges),511'''512 bank_select_msb = -1513 bank_select_lsb = -1514 bank_select = []515 channels_by_track = []516 channels_total = set([])517 general_midi_mode = []518 num_notes_by_channel = dict([])519 patches_used_by_track = []520 patches_used_total = set([])521 patch_changes_by_track = []522 patch_changes_total = set([])523 percussion = dict([]) # histogram of channel 9 "pitches"524 pitches = dict([]) # histogram of pitch-occurrences channels 0-8,10-15525 pitch_range_sum = 0 # u pitch-ranges of each track526 pitch_range_by_track = []527 is_a_score = True528 if opus_or_score == None:529 return {'bank_select':[], 'channels_by_track':[], 'channels_total':[],530 'general_midi_mode':[], 'ntracks':0, 'nticks':0,531 'num_notes_by_channel':dict([]),532 'patch_changes_by_track':[], 'patch_changes_total':[],533 'percussion':{}, 'pitches':{}, 'pitch_range_by_track':[],534 'ticks_per_quarter':0, 'pitch_range_sum':0}535 ticks_per_quarter = opus_or_score[0]536 i = 1 # ignore first element, which is ticks537 nticks = 0538 while i < len(opus_or_score):539 highest_pitch = 0540 lowest_pitch = 128541 channels_this_track = set([])542 patch_changes_this_track = dict({})543 for event in opus_or_score[i]:544 if event[0] == 'note':545 num_notes_by_channel[event[3]] = num_notes_by_channel.get(event[3],0) + 1546 if event[3] == 9:547 percussion[event[4]] = percussion.get(event[4],0) + 1548 else:549 pitches[event[4]] = pitches.get(event[4],0) + 1550 if event[4] > highest_pitch:551 highest_pitch = event[4]552 if event[4] < lowest_pitch:553 lowest_pitch = event[4]554 channels_this_track.add(event[3])555 channels_total.add(event[3])556 finish_time = event[1] + event[2]557 if finish_time > nticks:558 nticks = finish_time559 elif event[0] == 'note_off' or (event[0] == 'note_on' and event[4] == 0): # 4.8560 finish_time = event[1]561 if finish_time > nticks:562 nticks = finish_time563 elif event[0] == 'note_on':564 is_a_score = False565 num_notes_by_channel[event[2]] = num_notes_by_channel.get(event[2],0) + 1566 if event[2] == 9:567 percussion[event[3]] = percussion.get(event[3],0) + 1568 else:569 pitches[event[3]] = pitches.get(event[3],0) + 1570 if event[3] > highest_pitch:571 highest_pitch = event[3]572 if event[3] < lowest_pitch:573 lowest_pitch = event[3]574 channels_this_track.add(event[2])575 channels_total.add(event[2])576 elif event[0] == 'patch_change':577 patch_changes_this_track[event[2]] = event[3]578 patch_changes_total.add(event[3])579 elif event[0] == 'control_change':580 if event[3] == 0: # bank select MSB581 bank_select_msb = event[4]582 elif event[3] == 32: # bank select LSB583 bank_select_lsb = event[4]584 if bank_select_msb >= 0 and bank_select_lsb >= 0:585 bank_select.append((bank_select_msb,bank_select_lsb))586 bank_select_msb = -1587 bank_select_lsb = -1588 elif event[0] == 'sysex_f0':589 if _sysex2midimode.get(event[2], -1) >= 0:590 general_midi_mode.append(_sysex2midimode.get(event[2]))591 if is_a_score:592 if event[1] > nticks:593 nticks = event[1]594 else:595 nticks += event[1]596 if lowest_pitch == 128:597 lowest_pitch = 0598 channels_by_track.append(channels_this_track)599 patch_changes_by_track.append(patch_changes_this_track)600 pitch_range_by_track.append((lowest_pitch,highest_pitch))601 pitch_range_sum += (highest_pitch-lowest_pitch)602 i += 1603 604 return {'bank_select':bank_select,605 'channels_by_track':channels_by_track,606 'channels_total':channels_total,607 'general_midi_mode':general_midi_mode,608 'ntracks':len(opus_or_score)-1,609 'nticks':nticks,610 'num_notes_by_channel':num_notes_by_channel,611 'patch_changes_by_track':patch_changes_by_track,612 'patch_changes_total':patch_changes_total,613 'percussion':percussion,614 'pitches':pitches,615 'pitch_range_by_track':pitch_range_by_track,616 'pitch_range_sum':pitch_range_sum,617 'ticks_per_quarter':ticks_per_quarter}618 619#----------------------------- Event stuff --------------------------620 621_sysex2midimode = {622 "\x7E\x7F\x09\x01\xF7": 1,623 "\x7E\x7F\x09\x02\xF7": 0,624 "\x7E\x7F\x09\x03\xF7": 2,625}626 627# Some public-access tuples:628MIDI_events = tuple('''note_off note_on key_after_touch629control_change patch_change channel_after_touch630pitch_wheel_change'''.split())631 632Text_events = tuple('''text_event copyright_text_event633track_name instrument_name lyric marker cue_point text_event_08634text_event_09 text_event_0a text_event_0b text_event_0c635text_event_0d text_event_0e text_event_0f'''.split())636 637Nontext_meta_events = tuple('''end_track set_tempo638smpte_offset time_signature key_signature sequencer_specific639raw_meta_event sysex_f0 sysex_f7 song_position song_select640tune_request'''.split())641# unsupported: raw_data642 643# Actually, 'tune_request' is is F-series event, not strictly a meta-event...644Meta_events = Text_events + Nontext_meta_events645All_events = MIDI_events + Meta_events646 647# And three dictionaries:648Number2patch = { # General MIDI patch numbers:6490:'Acoustic Grand',6501:'Bright Acoustic',6512:'Electric Grand',6523:'Honky-Tonk',6534:'Electric Piano 1',6545:'Electric Piano 2',6556:'Harpsichord',6567:'Clav',6578:'Celesta',6589:'Glockenspiel',65910:'Music Box',66011:'Vibraphone',66112:'Marimba',66213:'Xylophone',66314:'Tubular Bells',66415:'Dulcimer',66516:'Drawbar Organ',66617:'Percussive Organ',66718:'Rock Organ',66819:'Church Organ',66920:'Reed Organ',67021:'Accordion',67122:'Harmonica',67223:'Tango Accordion',67324:'Acoustic Guitar(nylon)',67425:'Acoustic Guitar(steel)',67526:'Electric Guitar(jazz)',67627:'Electric Guitar(clean)',67728:'Electric Guitar(muted)',67829:'Overdriven Guitar',67930:'Distortion Guitar',68031:'Guitar Harmonics',68132:'Acoustic Bass',68233:'Electric Bass(finger)',68334:'Electric Bass(pick)',68435:'Fretless Bass',68536:'Slap Bass 1',68637:'Slap Bass 2',68738:'Synth Bass 1',68839:'Synth Bass 2',68940:'Violin',69041:'Viola',69142:'Cello',69243:'Contrabass',69344:'Tremolo Strings',69445:'Pizzicato Strings',69546:'Orchestral Harp',69647:'Timpani',69748:'String Ensemble 1',69849:'String Ensemble 2',69950:'SynthStrings 1',70051:'SynthStrings 2',70152:'Choir Aahs',70253:'Voice Oohs',70354:'Synth Voice',70455:'Orchestra Hit',70556:'Trumpet',70657:'Trombone',70758:'Tuba',70859:'Muted Trumpet',70960:'French Horn',71061:'Brass Section',71162:'SynthBrass 1',71263:'SynthBrass 2',71364:'Soprano Sax',71465:'Alto Sax',71566:'Tenor Sax',71667:'Baritone Sax',71768:'Oboe',71869:'English Horn',71970:'Bassoon',72071:'Clarinet',72172:'Piccolo',72273:'Flute',72374:'Recorder',72475:'Pan Flute',72576:'Blown Bottle',72677:'Skakuhachi',72778:'Whistle',72879:'Ocarina',72980:'Lead 1 (square)',73081:'Lead 2 (sawtooth)',73182:'Lead 3 (calliope)',73283:'Lead 4 (chiff)',73384:'Lead 5 (charang)',73485:'Lead 6 (voice)',73586:'Lead 7 (fifths)',73687:'Lead 8 (bass+lead)',73788:'Pad 1 (new age)',73889:'Pad 2 (warm)',73990:'Pad 3 (polysynth)',74091:'Pad 4 (choir)',74192:'Pad 5 (bowed)',74293:'Pad 6 (metallic)',74394:'Pad 7 (halo)',74495:'Pad 8 (sweep)',74596:'FX 1 (rain)',74697:'FX 2 (soundtrack)',74798:'FX 3 (crystal)',74899:'FX 4 (atmosphere)',749100:'FX 5 (brightness)',750101:'FX 6 (goblins)',751102:'FX 7 (echoes)',752103:'FX 8 (sci-fi)',753104:'Sitar',754105:'Banjo',755106:'Shamisen',756107:'Koto',757108:'Kalimba',758109:'Bagpipe',759110:'Fiddle',760111:'Shanai',761112:'Tinkle Bell',762113:'Agogo',763114:'Steel Drums',764115:'Woodblock',765116:'Taiko Drum',766117:'Melodic Tom',767118:'Synth Drum',768119:'Reverse Cymbal',769120:'Guitar Fret Noise',770121:'Breath Noise',771122:'Seashore',772123:'Bird Tweet',773124:'Telephone Ring',774125:'Helicopter',775126:'Applause',776127:'Gunshot',777}778Notenum2percussion = { # General MIDI Percussion (on Channel 9):77935:'Acoustic Bass Drum',78036:'Bass Drum 1',78137:'Side Stick',78238:'Acoustic Snare',78339:'Hand Clap',78440:'Electric Snare',78541:'Low Floor Tom',78642:'Closed Hi-Hat',78743:'High Floor Tom',78844:'Pedal Hi-Hat',78945:'Low Tom',79046:'Open Hi-Hat',79147:'Low-Mid Tom',79248:'Hi-Mid Tom',79349:'Crash Cymbal 1',79450:'High Tom',79551:'Ride Cymbal 1',79652:'Chinese Cymbal',79753:'Ride Bell',79854:'Tambourine',79955:'Splash Cymbal',80056:'Cowbell',80157:'Crash Cymbal 2',80258:'Vibraslap',80359:'Ride Cymbal 2',80460:'Hi Bongo',80561:'Low Bongo',80662:'Mute Hi Conga',80763:'Open Hi Conga',80864:'Low Conga',80965:'High Timbale',81066:'Low Timbale',81167:'High Agogo',81268:'Low Agogo',81369:'Cabasa',81470:'Maracas',81571:'Short Whistle',81672:'Long Whistle',81773:'Short Guiro',81874:'Long Guiro',81975:'Claves',82076:'Hi Wood Block',82177:'Low Wood Block',82278:'Mute Cuica',82379:'Open Cuica',82480:'Mute Triangle',82581:'Open Triangle',826}827 828Event2channelindex = { 'note':3, 'note_off':2, 'note_on':2,829 'key_after_touch':2, 'control_change':2, 'patch_change':2,830 'channel_after_touch':2, 'pitch_wheel_change':2831}832 833################################################################834# The code below this line is full of frightening things, all to835# do with the actual encoding and decoding of binary MIDI data.836 837def _twobytes2int(byte_a):838 r'''decode a 16 bit quantity from two bytes,'''839 return (byte_a[1] | (byte_a[0] << 8))840 841def _int2twobytes(int_16bit):842 r'''encode a 16 bit quantity into two bytes,'''843 return bytes([(int_16bit>>8) & 0xFF, int_16bit & 0xFF])844 845def _read_14_bit(byte_a):846 r'''decode a 14 bit quantity from two bytes,'''847 return (byte_a[0] | (byte_a[1] << 7))848 849def _write_14_bit(int_14bit):850 r'''encode a 14 bit quantity into two bytes,'''851 return bytes([int_14bit & 0x7F, (int_14bit>>7) & 0x7F])852 853def _ber_compressed_int(integer):854 r'''BER compressed integer (not an ASN.1 BER, see perlpacktut for855details). Its bytes represent an unsigned integer in base 128,856most significant digit first, with as few digits as possible.857Bit eight (the high bit) is set on each byte except the last.858'''859 ber = bytearray(b'')860 seven_bits = 0x7F & integer861 ber.insert(0, seven_bits) # XXX surely should convert to a char ?862 integer >>= 7863 while integer > 0:864 seven_bits = 0x7F & integer865 ber.insert(0, 0x80|seven_bits) # XXX surely should convert to a char ?866 integer >>= 7867 return ber868 869def _unshift_ber_int(ba):870 r'''Given a bytearray, returns a tuple of (the ber-integer at the871start, and the remainder of the bytearray).872'''873 if not len(ba): # 6.7874 _warn('_unshift_ber_int: no integer found')875 return ((0, b""))876 byte = ba[0]877 ba = ba[1:]878 integer = 0879 while True:880 integer += (byte & 0x7F)881 if not (byte & 0x80):882 return ((integer, ba))883 if not len(ba):884 _warn('_unshift_ber_int: no end-of-integer found')885 return ((0, ba))886 byte = ba[0]887 ba = ba[1:]888 integer <<= 7889 890 891def _clean_up_warnings(): # 5.4892 # Call this before returning from any publicly callable function893 # whenever there's a possibility that a warning might have been printed894 # by the function, or by any private functions it might have called.895 if _no_warning:896 return897 global _previous_times898 global _previous_warning899 if _previous_times > 1:900 # E:1176, 0: invalid syntax (<string>, line 1176) (syntax-error) ???901 # print(' previous message repeated '+str(_previous_times)+' times', file=sys.stderr)902 # 6.7903 sys.stderr.write(' previous message repeated {0} times\n'.format(_previous_times))904 elif _previous_times > 0:905 sys.stderr.write(' previous message repeated\n')906 _previous_times = 0907 _previous_warning = ''908 909 910def _warn(s=''):911 if _no_warning:912 return913 global _previous_times914 global _previous_warning915 if s == _previous_warning: # 5.4916 _previous_times = _previous_times + 1917 else:918 _clean_up_warnings()919 sys.stderr.write(str(s) + "\n")920 _previous_warning = s921 922 923def _some_text_event(which_kind=0x01, text=b'some_text', text_encoding='ISO-8859-1'):924 if str(type(text)).find("'str'") >= 0: # 6.4 test for back-compatibility925 data = bytes(text, encoding=text_encoding)926 else:927 data = bytes(text)928 return b'\xFF' + bytes((which_kind,)) + _ber_compressed_int(len(data)) + data929 930 931def _consistentise_ticks(scores): # 3.6932 # used by mix_scores, merge_scores, concatenate_scores933 if len(scores) == 1:934 return copy.deepcopy(scores)935 are_consistent = True936 ticks = scores[0][0]937 iscore = 1938 while iscore < len(scores):939 if scores[iscore][0] != ticks:940 are_consistent = False941 break942 iscore += 1943 if are_consistent:944 return copy.deepcopy(scores)945 new_scores = []946 iscore = 0947 while iscore < len(scores):948 score = scores[iscore]949 new_scores.append(opus2score(to_millisecs(score2opus(score))))950 iscore += 1951 return new_scores952 953 954###########################################################################955def _decode(trackdata=b'', exclude=None, include=None,956 event_callback=None, exclusive_event_callback=None, no_eot_magic=False):957 r'''Decodes MIDI track data into an opus-style list of events.958The options:959 'exclude' is a list of event types which will be ignored SHOULD BE A SET960 'include' (and no exclude), makes exclude a list961 of all possible events, /minus/ what include specifies962 'event_callback' is a coderef963 'exclusive_event_callback' is a coderef964'''965 trackdata = bytearray(trackdata)966 if exclude == None:967 exclude = []968 if include == None:969 include = []970 if include and not exclude:971 exclude = All_events972 include = set(include)973 exclude = set(exclude)974 975 # Pointer = 0; not used here; we eat through the bytearray instead.976 event_code = -1; # used for running status977 event_count = 0;978 events = []979 980 while (len(trackdata)):981 # loop while there's anything to analyze ...982 eot = False # When True, the event registrar aborts this loop983 event_count += 1984 985 E = []986 # E for events - we'll feed it to the event registrar at the end.987 988 # Slice off the delta time code, and analyze it989 [time, trackdata] = _unshift_ber_int(trackdata)990 991 # Now let's see what we can make of the command992 first_byte = trackdata[0] & 0xFF993 trackdata = trackdata[1:]994 if (first_byte < 0xF0): # It's a MIDI event995 if (first_byte & 0x80):996 event_code = first_byte997 else:998 # It wants running status; use last event_code value999 trackdata.insert(0, first_byte)1000 if (event_code == -1):1001 _warn("Running status not set; Aborting track.")1002 return []1003 1004 command = event_code & 0xF01005 channel = event_code & 0x0F1006 1007 if (command == 0xF6): # 0-byte argument1008 pass1009 elif (command == 0xC0 or command == 0xD0): # 1-byte argument1010 parameter = trackdata[0] # could be B1011 trackdata = trackdata[1:]1012 else: # 2-byte argument could be BB or 14-bit1013 parameter = (trackdata[0], trackdata[1])1014 trackdata = trackdata[2:]1015 1016 #################################################################1017 # MIDI events1018 1019 if (command == 0x80):1020 if 'note_off' in exclude:1021 continue1022 E = ['note_off', time, channel, parameter[0], parameter[1]]1023 elif (command == 0x90):1024 if 'note_on' in exclude:1025 continue1026 E = ['note_on', time, channel, parameter[0], parameter[1]]1027 elif (command == 0xA0):1028 if 'key_after_touch' in exclude:1029 continue1030 E = ['key_after_touch', time, channel, parameter[0], parameter[1]]1031 elif (command == 0xB0):1032 if 'control_change' in exclude:1033 continue1034 E = ['control_change', time, channel, parameter[0], parameter[1]]1035 elif (command == 0xC0):1036 if 'patch_change' in exclude:1037 continue1038 E = ['patch_change', time, channel, parameter]1039 elif (command == 0xD0):1040 if 'channel_after_touch' in exclude:1041 continue1042 E = ['channel_after_touch', time, channel, parameter]1043 elif (command == 0xE0):1044 if 'pitch_wheel_change' in exclude:1045 continue1046 E = ['pitch_wheel_change', time, channel,1047 _read_14_bit(parameter) - 0x2000]1048 else:1049 _warn("Shouldn't get here; command=" + hex(command))1050 1051 elif (first_byte == 0xFF): # It's a Meta-Event! ##################1052 # [command, length, remainder] =1053 # unpack("xCwa*", substr(trackdata, $Pointer, 6));1054 # Pointer += 6 - len(remainder);1055 # # Move past JUST the length-encoded.1056 command = trackdata[0] & 0xFF1057 trackdata = trackdata[1:]1058 [length, trackdata] = _unshift_ber_int(trackdata)1059 if (command == 0x00):1060 if (length == 2):1061 E = ['set_sequence_number', time, _twobytes2int(trackdata)]1062 else:1063 _warn('set_sequence_number: length must be 2, not ' + str(length))1064 E = ['set_sequence_number', time, 0]1065 1066 elif command >= 0x01 and command <= 0x0f: # Text events1067 # 6.2 take it in bytes; let the user get the right encoding.1068 # text_str = trackdata[0:length].decode('ascii','ignore')1069 # text_str = trackdata[0:length].decode('ISO-8859-1')1070 # 6.4 take it in bytes; let the user get the right encoding.1071 text_data = bytes(trackdata[0:length]) # 6.41072 # Defined text events1073 if (command == 0x01):1074 E = ['text_event', time, text_data]1075 elif (command == 0x02):1076 E = ['copyright_text_event', time, text_data]1077 elif (command == 0x03):1078 E = ['track_name', time, text_data]1079 elif (command == 0x04):1080 E = ['instrument_name', time, text_data]1081 elif (command == 0x05):1082 E = ['lyric', time, text_data]1083 elif (command == 0x06):1084 E = ['marker', time, text_data]1085 elif (command == 0x07):1086 E = ['cue_point', time, text_data]1087 # Reserved but apparently unassigned text events1088 elif (command == 0x08):1089 E = ['text_event_08', time, text_data]1090 elif (command == 0x09):1091 E = ['text_event_09', time, text_data]1092 elif (command == 0x0a):1093 E = ['text_event_0a', time, text_data]1094 elif (command == 0x0b):1095 E = ['text_event_0b', time, text_data]1096 elif (command == 0x0c):1097 E = ['text_event_0c', time, text_data]1098 elif (command == 0x0d):1099 E = ['text_event_0d', time, text_data]1100 elif (command == 0x0e):1101 E = ['text_event_0e', time, text_data]1102 elif (command == 0x0f):1103 E = ['text_event_0f', time, text_data]1104 1105 # Now the sticky events -------------------------------------1106 elif (command == 0x2F):1107 E = ['end_track', time]1108 # The code for handling this, oddly, comes LATER,1109 # in the event registrar.1110 elif (command == 0x51): # DTime, Microseconds/Crochet1111 if length != 3:1112 _warn('set_tempo event, but length=' + str(length))1113 E = ['set_tempo', time,1114 struct.unpack(">I", b'\x00' + trackdata[0:3])[0]]1115 elif (command == 0x54):1116 if length != 5: # DTime, HR, MN, SE, FR, FF1117 _warn('smpte_offset event, but length=' + str(length))1118 E = ['smpte_offset', time] + list(struct.unpack(">BBBBB", trackdata[0:5]))1119 elif (command == 0x58):1120 if length != 4: # DTime, NN, DD, CC, BB1121 _warn('time_signature event, but length=' + str(length))1122 E = ['time_signature', time] + list(trackdata[0:4])1123 elif (command == 0x59):1124 if length != 2: # DTime, SF(signed), MI1125 _warn('key_signature event, but length=' + str(length))1126 E = ['key_signature', time] + list(struct.unpack(">bB", trackdata[0:2]))1127 elif (command == 0x7F): # 6.41128 E = ['sequencer_specific', time, bytes(trackdata[0:length])]1129 else:1130 E = ['raw_meta_event', time, command,1131 bytes(trackdata[0:length])] # 6.01132 # "[uninterpretable meta-event command of length length]"1133 # DTime, Command, Binary Data1134 # It's uninterpretable; record it as raw_data.1135 1136 # Pointer += length; # Now move Pointer1137 trackdata = trackdata[length:]1138 1139 ######################################################################1140 elif (first_byte == 0xF0 or first_byte == 0xF7):1141 # Note that sysexes in MIDI /files/ are different than sysexes1142 # in MIDI transmissions!! The vast majority of system exclusive1143 # messages will just use the F0 format. For instance, the1144 # transmitted message F0 43 12 00 07 F7 would be stored in a1145 # MIDI file as F0 05 43 12 00 07 F7. As mentioned above, it is1146 # required to include the F7 at the end so that the reader of the1147 # MIDI file knows that it has read the entire message. (But the F71148 # is omitted if this is a non-final block in a multiblock sysex;1149 # but the F7 (if there) is counted in the message's declared1150 # length, so we don't have to think about it anyway.)1151 # command = trackdata.pop(0)1152 [length, trackdata] = _unshift_ber_int(trackdata)1153 if first_byte == 0xF0:1154 # 20091008 added ISO-8859-1 to get an 8-bit str1155 # 6.4 return bytes instead1156 E = ['sysex_f0', time, bytes(trackdata[0:length])]1157 else:1158 E = ['sysex_f7', time, bytes(trackdata[0:length])]1159 trackdata = trackdata[length:]1160 1161 ######################################################################1162 # Now, the MIDI file spec says:1163 # <track data> = <MTrk event>+1164 # <MTrk event> = <delta-time> <event>1165 # <event> = <MIDI event> | <sysex event> | <meta-event>1166 # I know that, on the wire, <MIDI event> can include note_on,1167 # note_off, and all the other 8x to Ex events, AND Fx events1168 # other than F0, F7, and FF -- namely, <song position msg>,1169 # <song select msg>, and <tune request>.1170 #1171 # Whether these can occur in MIDI files is not clear specified1172 # from the MIDI file spec. So, I'm going to assume that1173 # they CAN, in practice, occur. I don't know whether it's1174 # proper for you to actually emit these into a MIDI file.1175 1176 elif (first_byte == 0xF2): # DTime, Beats1177 # <song position msg> ::= F2 <data pair>1178 E = ['song_position', time, _read_14_bit(trackdata[:2])]1179 trackdata = trackdata[2:]1180 1181 elif (first_byte == 0xF3): # <song select msg> ::= F3 <data singlet>1182 # E = ['song_select', time, struct.unpack('>B',trackdata.pop(0))[0]]1183 E = ['song_select', time, trackdata[0]]1184 trackdata = trackdata[1:]1185 # DTime, Thing (what?! song number? whatever ...)1186 1187 elif (first_byte == 0xF6): # DTime1188 E = ['tune_request', time]1189 # What would a tune request be doing in a MIDI /file/?1190 1191 #########################################################1192 # ADD MORE META-EVENTS HERE. TODO:1193 # f1 -- MTC Quarter Frame Message. One data byte follows1194 # the Status; it's the time code value, from 0 to 127.1195 # f8 -- MIDI clock. no data.1196 # fa -- MIDI start. no data.1197 # fb -- MIDI continue. no data.1198 # fc -- MIDI stop. no data.1199 # fe -- Active sense. no data.1200 # f4 f5 f9 fd -- unallocated