avans06/Audio-To-MIDI-And-Advanced-Renderer
37
1#! /usr/bin/python32 3r'''###############################################################################4###################################################################################5#6#7# Tegridy MIDI X Module (TMIDI X / tee-midi eks)8#9# NOTE: TMIDI X Module starts after the partial MIDI.py module @ line 145010#11# Based upon MIDI.py module v.6.7. by Peter Billam / pjb.com.au12#13# Project Los Angeles14#15# Tegridy Code 202516#17# https://github.com/Tegridy-Code/Project-Los-Angeles18#19#20###################################################################################21###################################################################################22# Copyright 2025 Project Los Angeles / Tegridy Code23#24# Licensed under the Apache License, Version 2.0 (the "License");25# you may not use this file except in compliance with the License.26# You may obtain a copy of the License at27#28# http://www.apache.org/licenses/LICENSE-2.029#30# Unless required by applicable law or agreed to in writing, software31# distributed under the License is distributed on an "AS IS" BASIS,32# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.33# See the License for the specific language governing permissions and34# limitations under the License.35###################################################################################36###################################################################################37#38# PARTIAL MIDI.py Module v.6.7. by Peter Billam39# Please see TMIDI 2.3/tegridy-tools repo for full MIDI.py module code40# 41# Or you can always download the latest full version from:42#43# https://pjb.com.au/44# https://peterbillam.gitlab.io/miditools/45# 46# Copyright 2020 Peter Billam47#48###################################################################################49###################################################################################50# unsupported 20091104 ...51# ['set_sequence_number', dtime, sequence]52# ['raw_data', dtime, raw]53 54# 20150914 jimbo1qaz MIDI.py str/bytes bug report55# I found a MIDI file which had Shift-JIS titles. When midi.py decodes it as56# latin-1, it produces a string which cannot even be accessed without raising57# a UnicodeDecodeError. Maybe, when converting raw byte strings from MIDI,58# you should keep them as bytes, not improperly decode them. However, this59# would change the API. (ie: text = a "string" ? of 0 or more bytes). It60# could break compatiblity, but there's not much else you can do to fix the bug61# https://en.wikipedia.org/wiki/Shift_JIS62 63This module offers functions: concatenate_scores(), grep(),64merge_scores(), mix_scores(), midi2opus(), midi2score(), opus2midi(),65opus2score(), play_score(), score2midi(), score2opus(), score2stats(),66score_type(), segment(), timeshift() and to_millisecs(),67where "midi" means the MIDI-file bytes (as can be put in a .mid file,68or piped into aplaymidi), and "opus" and "score" are list-structures69as inspired by Sean Burke's MIDI-Perl CPAN module.70 71Warning: Version 6.4 is not necessarily backward-compatible with72previous versions, in that text-data is now bytes, not strings.73This reflects the fact that many MIDI files have text data in74encodings other that ISO-8859-1, for example in Shift-JIS.75 76Download MIDI.py from http://www.pjb.com.au/midi/free/MIDI.py77and put it in your PYTHONPATH. MIDI.py depends on Python3.78 79There is also a call-compatible translation into Lua of this80module: see http://www.pjb.com.au/comp/lua/MIDI.html81 82Backup web site: https://peterbillam.gitlab.io/miditools/83 84The "opus" is a direct translation of the midi-file-events, where85the times are delta-times, in ticks, since the previous event.86 87The "score" is more human-centric; it uses absolute times, and88combines the separate note_on and note_off events into one "note"89event, with a duration:90 ['note', start_time, duration, channel, note, velocity] # in a "score"91 92 EVENTS (in an "opus" structure)93 ['note_off', dtime, channel, note, velocity] # in an "opus"94 ['note_on', dtime, channel, note, velocity] # in an "opus"95 ['key_after_touch', dtime, channel, note, velocity]96 ['control_change', dtime, channel, controller(0-127), value(0-127)]97 ['patch_change', dtime, channel, patch]98 ['channel_after_touch', dtime, channel, velocity]99 ['pitch_wheel_change', dtime, channel, pitch_wheel]100 ['text_event', dtime, text]101 ['copyright_text_event', dtime, text]102 ['track_name', dtime, text]103 ['instrument_name', dtime, text]104 ['lyric', dtime, text]105 ['marker', dtime, text]106 ['cue_point', dtime, text]107 ['text_event_08', dtime, text]108 ['text_event_09', dtime, text]109 ['text_event_0a', dtime, text]110 ['text_event_0b', dtime, text]111 ['text_event_0c', dtime, text]112 ['text_event_0d', dtime, text]113 ['text_event_0e', dtime, text]114 ['text_event_0f', dtime, text]115 ['end_track', dtime]116 ['set_tempo', dtime, tempo]117 ['smpte_offset', dtime, hr, mn, se, fr, ff]118 ['time_signature', dtime, nn, dd, cc, bb]119 ['key_signature', dtime, sf, mi]120 ['sequencer_specific', dtime, raw]121 ['raw_meta_event', dtime, command(0-255), raw]122 ['sysex_f0', dtime, raw]123 ['sysex_f7', dtime, raw]124 ['song_position', dtime, song_pos]125 ['song_select', dtime, song_number]126 ['tune_request', dtime]127 128 DATA TYPES129 channel = a value 0 to 15130 controller = 0 to 127 (see http://www.pjb.com.au/muscript/gm.html#cc )131 dtime = time measured in "ticks", 0 to 268435455132 velocity = a value 0 (soft) to 127 (loud)133 note = a value 0 to 127 (middle-C is 60)134 patch = 0 to 127 (see http://www.pjb.com.au/muscript/gm.html )135 pitch_wheel = a value -8192 to 8191 (0x1FFF)136 raw = bytes, of length 0 or more (for sysex events see below)137 sequence_number = a value 0 to 65,535 (0xFFFF)138 song_pos = a value 0 to 16,383 (0x3FFF)139 song_number = a value 0 to 127140 tempo = microseconds per crochet (quarter-note), 0 to 16777215141 text = bytes, of length 0 or more142 ticks = the number of ticks per crochet (quarter-note)143 144 In sysex_f0 events, the raw data must not start with a \xF0 byte,145 since this gets added automatically;146 but it must end with an explicit \xF7 byte!147 In the very unlikely case that you ever need to split sysex data148 into one sysex_f0 followed by one or more sysex_f7s, then only the149 last of those sysex_f7 events must end with the explicit \xF7 byte150 (again, the raw data of individual sysex_f7 events must not start151 with any \xF7 byte, since this gets added automatically).152 153 Since version 6.4, text data is in bytes, not in a ISO-8859-1 string.154 155 156 GOING THROUGH A SCORE WITHIN A PYTHON PROGRAM157 channels = {2,3,5,8,13}158 itrack = 1 # skip 1st element which is ticks159 while itrack < len(score):160 for event in score[itrack]:161 if event[0] == 'note': # for example,162 pass # do something to all notes163 # or, to work on events in only particular channels...164 channel_index = MIDI.Event2channelindex.get(event[0], False)165 if channel_index and (event[channel_index] in channels):166 pass # do something to channels 2,3,5,8 and 13167 itrack += 1168 169'''170 171###################################################################################172 173__version__ = "25.7.8"174 175print('=' * 70)176print('TMIDIX Python module')177print('Version:', __version__)178print('=' * 70)179print('Loading module...')180 181###################################################################################182 183import sys, struct, copy184 185Version = '6.7'186VersionDate = '20201120'187# 20201120 6.7 call to bytest() removed, and protect _unshift_ber_int188# 20160702 6.6 to_millisecs() now handles set_tempo across multiple Tracks189# 20150921 6.5 segment restores controllers as well as patch and tempo190# 20150914 6.4 text data is bytes or bytearray, not ISO-8859-1 strings191# 20150628 6.3 absent any set_tempo, default is 120bpm (see MIDI file spec 1.1)192# 20150101 6.2 all text events can be 8-bit; let user get the right encoding193# 20141231 6.1 fix _some_text_event; sequencer_specific data can be 8-bit194# 20141230 6.0 synth_specific data can be 8-bit195# 20120504 5.9 add the contents of mid_opus_tracks()196# 20120208 5.8 fix num_notes_by_channel() ; should be a dict197# 20120129 5.7 _encode handles empty tracks; score2stats num_notes_by_channel198# 20111111 5.6 fix patch 45 and 46 in Number2patch, should be Harp199# 20110129 5.5 add mix_opus_tracks() and event2alsaseq()200# 20110126 5.4 "previous message repeated N times" to save space on stderr201# 20110125 5.2 opus2score terminates unended notes at the end of the track202# 20110124 5.1 the warnings in midi2opus display track_num203# 21110122 5.0 if garbage, midi2opus returns the opus so far204# 21110119 4.9 non-ascii chars stripped out of the text_events205# 21110110 4.8 note_on with velocity=0 treated as a note-off206# 21110108 4.6 unknown F-series event correctly eats just one byte207# 21011010 4.2 segment() uses start_time, end_time named params208# 21011005 4.1 timeshift() must not pad the set_tempo command209# 21011003 4.0 pitch2note_event must be chapitch2note_event210# 21010918 3.9 set_sequence_number supported, FWIW211# 20100913 3.7 many small bugfixes; passes all tests212# 20100910 3.6 concatenate_scores enforce ticks=1000, just like merge_scores213# 20100908 3.5 minor bugs fixed in score2stats214# 20091104 3.4 tune_request now supported215# 20091104 3.3 fixed bug in decoding song_position and song_select216# 20091104 3.2 unsupported: set_sequence_number tune_request raw_data217# 20091101 3.1 document how to traverse a score within Python218# 20091021 3.0 fixed bug in score2stats detecting GM-mode = 0219# 20091020 2.9 score2stats reports GM-mode and bank msb,lsb events220# 20091019 2.8 in merge_scores, channel 9 must remain channel 9 (in GM)221# 20091018 2.7 handles empty tracks gracefully222# 20091015 2.6 grep() selects channels223# 20091010 2.5 merge_scores reassigns channels to avoid conflicts224# 20091010 2.4 fixed bug in to_millisecs which now only does opusses225# 20091010 2.3 score2stats returns channels & patch_changes, by_track & total226# 20091010 2.2 score2stats() returns also pitches and percussion dicts227# 20091010 2.1 bugs: >= not > in segment, to notice patch_change at time 0228# 20091010 2.0 bugs: spurious pop(0) ( in _decode sysex229# 20091008 1.9 bugs: ISO decoding in sysex; str( not int( in note-off warning230# 20091008 1.8 add concatenate_scores()231# 20091006 1.7 score2stats() measures nticks and ticks_per_quarter232# 20091004 1.6 first mix_scores() and merge_scores()233# 20090424 1.5 timeshift() bugfix: earliest only sees events after from_time234# 20090330 1.4 timeshift() has also a from_time argument235# 20090322 1.3 timeshift() has also a start_time argument236# 20090319 1.2 add segment() and timeshift()237# 20090301 1.1 add to_millisecs()238 239_previous_warning = '' # 5.4240_previous_times = 0 # 5.4241_no_warning = False242 243#------------------------------- Encoding stuff --------------------------244 245def opus2midi(opus=[], text_encoding='ISO-8859-1'):246 r'''The argument is a list: the first item in the list is the "ticks"247parameter, the others are the tracks. Each track is a list248of midi-events, and each event is itself a list; see above.249opus2midi() returns a bytestring of the MIDI, which can then be250written either to a file opened in binary mode (mode='wb'),251or to stdout by means of: sys.stdout.buffer.write()252 253my_opus = [254 96, 255 [ # track 0:256 ['patch_change', 0, 1, 8], # and these are the events...257 ['note_on', 5, 1, 25, 96],258 ['note_off', 96, 1, 25, 0],259 ['note_on', 0, 1, 29, 96],260 ['note_off', 96, 1, 29, 0],261 ], # end of track 0262]263my_midi = opus2midi(my_opus)264sys.stdout.buffer.write(my_midi)265'''266 if len(opus) < 2:267 opus=[1000, [],]268 tracks = copy.deepcopy(opus)269 ticks = int(tracks.pop(0))270 ntracks = len(tracks)271 if ntracks == 1:272 format = 0273 else:274 format = 1275 276 my_midi = b"MThd\x00\x00\x00\x06"+struct.pack('>HHH',format,ntracks,ticks)277 for track in tracks:278 events = _encode(track, text_encoding=text_encoding)279 my_midi += b'MTrk' + struct.pack('>I',len(events)) + events280 _clean_up_warnings()281 return my_midi282 283 284def score2opus(score=None, text_encoding='ISO-8859-1'):285 r'''286The argument is a list: the first item in the list is the "ticks"287parameter, the others are the tracks. Each track is a list288of score-events, and each event is itself a list. A score-event289is similar to an opus-event (see above), except that in a score:290 1) the times are expressed as an absolute number of ticks291 from the track's start time292 2) the pairs of 'note_on' and 'note_off' events in an "opus"293 are abstracted into a single 'note' event in a "score":294 ['note', start_time, duration, channel, pitch, velocity]295score2opus() returns a list specifying the equivalent "opus".296 297my_score = [298 96,299 [ # track 0:300 ['patch_change', 0, 1, 8],301 ['note', 5, 96, 1, 25, 96],302 ['note', 101, 96, 1, 29, 96]303 ], # end of track 0304]305my_opus = score2opus(my_score)306'''307 if len(score) < 2:308 score=[1000, [],]309 tracks = copy.deepcopy(score)310 ticks = int(tracks.pop(0))311 opus_tracks = []312 for scoretrack in tracks:313 time2events = dict([])314 for scoreevent in scoretrack:315 if scoreevent[0] == 'note':316 note_on_event = ['note_on',scoreevent[1],317 scoreevent[3],scoreevent[4],scoreevent[5]]318 note_off_event = ['note_off',scoreevent[1]+scoreevent[2],319 scoreevent[3],scoreevent[4],scoreevent[5]]320 if time2events.get(note_on_event[1]):321 time2events[note_on_event[1]].append(note_on_event)322 else:323 time2events[note_on_event[1]] = [note_on_event,]324 if time2events.get(note_off_event[1]):325 time2events[note_off_event[1]].append(note_off_event)326 else:327 time2events[note_off_event[1]] = [note_off_event,]328 continue329 if time2events.get(scoreevent[1]):330 time2events[scoreevent[1]].append(scoreevent)331 else:332 time2events[scoreevent[1]] = [scoreevent,]333 334 sorted_times = [] # list of keys335 for k in time2events.keys():336 sorted_times.append(k)337 sorted_times.sort()338 339 sorted_events = [] # once-flattened list of values sorted by key340 for time in sorted_times:341 sorted_events.extend(time2events[time])342 343 abs_time = 0344 for event in sorted_events: # convert abs times => delta times345 delta_time = event[1] - abs_time346 abs_time = event[1]347 event[1] = delta_time348 opus_tracks.append(sorted_events)349 opus_tracks.insert(0,ticks)350 _clean_up_warnings()351 return opus_tracks352 353def score2midi(score=None, text_encoding='ISO-8859-1'):354 r'''355Translates a "score" into MIDI, using score2opus() then opus2midi()356'''357 return opus2midi(score2opus(score, text_encoding), text_encoding)358 359#--------------------------- Decoding stuff ------------------------360 361def midi2opus(midi=b'', do_not_check_MIDI_signature=False):362 r'''Translates MIDI into a "opus". For a description of the363"opus" format, see opus2midi()364'''365 my_midi=bytearray(midi)366 if len(my_midi) < 4:367 _clean_up_warnings()368 return [1000,[],]369 id = bytes(my_midi[0:4])370 if id != b'MThd':371 _warn("midi2opus: midi starts with "+str(id)+" instead of 'MThd'")372 _clean_up_warnings()373 if do_not_check_MIDI_signature == False:374 return [1000,[],]375 [length, format, tracks_expected, ticks] = struct.unpack(376 '>IHHH', bytes(my_midi[4:14]))377 if length != 6:378 _warn("midi2opus: midi header length was "+str(length)+" instead of 6")379 _clean_up_warnings()380 return [1000,[],]381 my_opus = [ticks,]382 my_midi = my_midi[14:]383 track_num = 1 # 5.1384 while len(my_midi) >= 8:385 track_type = bytes(my_midi[0:4])386 if track_type != b'MTrk':387 #_warn('midi2opus: Warning: track #'+str(track_num)+' type is '+str(track_type)+" instead of b'MTrk'")388 pass389 [track_length] = struct.unpack('>I', my_midi[4:8])390 my_midi = my_midi[8:]391 if track_length > len(my_midi):392 _warn('midi2opus: track #'+str(track_num)+' length '+str(track_length)+' is too large')393 _clean_up_warnings()394 return my_opus # 5.0395 my_midi_track = my_midi[0:track_length]396 my_track = _decode(my_midi_track)397 my_opus.append(my_track)398 my_midi = my_midi[track_length:]399 track_num += 1 # 5.1400 _clean_up_warnings()401 return my_opus402 403def opus2score(opus=[]):404 r'''For a description of the "opus" and "score" formats,405see opus2midi() and score2opus().406'''407 if len(opus) < 2:408 _clean_up_warnings()409 return [1000,[],]410 tracks = copy.deepcopy(opus) # couple of slices probably quicker...411 ticks = int(tracks.pop(0))412 score = [ticks,]413 for opus_track in tracks:414 ticks_so_far = 0415 score_track = []416 chapitch2note_on_events = dict([]) # 4.0417 for opus_event in opus_track:418 ticks_so_far += opus_event[1]419 if opus_event[0] == 'note_off' or (opus_event[0] == 'note_on' and opus_event[4] == 0): # 4.8420 cha = opus_event[2]421 pitch = opus_event[3]422 key = cha*128 + pitch423 if chapitch2note_on_events.get(key):424 new_event = chapitch2note_on_events[key].pop(0)425 new_event[2] = ticks_so_far - new_event[1]426 score_track.append(new_event)427 elif pitch > 127:428 pass #_warn('opus2score: note_off with no note_on, bad pitch='+str(pitch))429 else:430 pass #_warn('opus2score: note_off with no note_on cha='+str(cha)+' pitch='+str(pitch))431 elif opus_event[0] == 'note_on':432 cha = opus_event[2]433 pitch = opus_event[3]434 key = cha*128 + pitch435 new_event = ['note',ticks_so_far,0,cha,pitch, opus_event[4]]436 if chapitch2note_on_events.get(key):437 chapitch2note_on_events[key].append(new_event)438 else:439 chapitch2note_on_events[key] = [new_event,]440 else:441 opus_event[1] = ticks_so_far442 score_track.append(opus_event)443 # check for unterminated notes (Oisín) -- 5.2444 for chapitch in chapitch2note_on_events:445 note_on_events = chapitch2note_on_events[chapitch]446 for new_e in note_on_events:447 new_e[2] = ticks_so_far - new_e[1]448 score_track.append(new_e)449 pass #_warn("opus2score: note_on with no note_off cha="+str(new_e[3])+' pitch='+str(new_e[4])+'; adding note_off at end')450 score.append(score_track)451 _clean_up_warnings()452 return score453 454def midi2score(midi=b'', do_not_check_MIDI_signature=False):455 r'''456Translates MIDI into a "score", using midi2opus() then opus2score()457'''458 return opus2score(midi2opus(midi, do_not_check_MIDI_signature))459 460def midi2ms_score(midi=b'', do_not_check_MIDI_signature=False):461 r'''462Translates MIDI into a "score" with one beat per second and one463tick per millisecond, using midi2opus() then to_millisecs()464then opus2score()465'''466 return opus2score(to_millisecs(midi2opus(midi, do_not_check_MIDI_signature)))467 468def midi2single_track_ms_score(midi_path_or_bytes, 469 recalculate_channels = False, 470 pass_old_timings_events= False, 471 verbose = False, 472 do_not_check_MIDI_signature=False473 ):474 r'''475Translates MIDI into a single track "score" with 16 instruments and one beat per second and one476tick per millisecond477'''478 479 if type(midi_path_or_bytes) == bytes:480 midi_data = midi_path_or_bytes481 482 elif type(midi_path_or_bytes) == str:483 midi_data = open(midi_path_or_bytes, 'rb').read() 484 485 score = midi2score(midi_data, do_not_check_MIDI_signature)486 487 if recalculate_channels:488 489 events_matrixes = []490 491 itrack = 1492 events_matrixes_channels = []493 while itrack < len(score):494 events_matrix = []495 for event in score[itrack]:496 if event[0] == 'note' and event[3] != 9:497 event[3] = (16 * (itrack-1)) + event[3]498 if event[3] not in events_matrixes_channels:499 events_matrixes_channels.append(event[3])500 501 events_matrix.append(event)502 events_matrixes.append(events_matrix)503 itrack += 1504 505 events_matrix1 = []506 for e in events_matrixes:507 events_matrix1.extend(e)508 509 if verbose:510 if len(events_matrixes_channels) > 16:511 print('MIDI has', len(events_matrixes_channels), 'instruments!', len(events_matrixes_channels) - 16, 'instrument(s) will be removed!')512 513 for e in events_matrix1:514 if e[0] == 'note' and e[3] != 9:515 if e[3] in events_matrixes_channels[:15]:516 if events_matrixes_channels[:15].index(e[3]) < 9:517 e[3] = events_matrixes_channels[:15].index(e[3])518 else:519 e[3] = events_matrixes_channels[:15].index(e[3])+1520 else:521 events_matrix1.remove(e)522 523 if e[0] in ['patch_change', 'control_change', 'channel_after_touch', 'key_after_touch', 'pitch_wheel_change'] and e[2] != 9:524 if e[2] in [e % 16 for e in events_matrixes_channels[:15]]:525 if [e % 16 for e in events_matrixes_channels[:15]].index(e[2]) < 9:526 e[2] = [e % 16 for e in events_matrixes_channels[:15]].index(e[2])527 else:528 e[2] = [e % 16 for e in events_matrixes_channels[:15]].index(e[2])+1529 else:530 events_matrix1.remove(e)531 532 else:533 events_matrix1 = []534 itrack = 1535 536 while itrack < len(score):537 for event in score[itrack]:538 events_matrix1.append(event)539 itrack += 1 540 541 opus = score2opus([score[0], events_matrix1])542 ms_score = opus2score(to_millisecs(opus, pass_old_timings_events=pass_old_timings_events))543 544 return ms_score545 546#------------------------ Other Transformations ---------------------547 548def to_millisecs(old_opus=None, desired_time_in_ms=1, pass_old_timings_events = False):549 r'''Recallibrates all the times in an "opus" to use one beat550per second and one tick per millisecond. This makes it551hard to retrieve any information about beats or barlines,552but it does make it easy to mix different scores together.553'''554 if old_opus == None:555 return [1000 * desired_time_in_ms,[],]556 try:557 old_tpq = int(old_opus[0])558 except IndexError: # 5.0559 _warn('to_millisecs: the opus '+str(type(old_opus))+' has no elements')560 return [1000 * desired_time_in_ms,[],]561 new_opus = [1000 * desired_time_in_ms,]562 # 6.7 first go through building a table of set_tempos by absolute-tick563 ticks2tempo = {}564 itrack = 1565 while itrack < len(old_opus):566 ticks_so_far = 0567 for old_event in old_opus[itrack]:568 if old_event[0] == 'note':569 raise TypeError('to_millisecs needs an opus, not a score')570 ticks_so_far += old_event[1]571 if old_event[0] == 'set_tempo':572 ticks2tempo[ticks_so_far] = old_event[2]573 itrack += 1574 # then get the sorted-array of their keys575 tempo_ticks = [] # list of keys576 for k in ticks2tempo.keys():577 tempo_ticks.append(k)578 tempo_ticks.sort()579 # then go through converting to millisec, testing if the next580 # set_tempo lies before the next track-event, and using it if so.581 itrack = 1582 while itrack < len(old_opus):583 ms_per_old_tick = 400 / old_tpq # float: will round later 6.3584 i_tempo_ticks = 0585 ticks_so_far = 0586 ms_so_far = 0.0587 previous_ms_so_far = 0.0588 589 if pass_old_timings_events:590 new_track = [['set_tempo',0,1000000 * desired_time_in_ms],['old_tpq', 0, old_tpq]] # new "crochet" is 1 sec591 else:592 new_track = [['set_tempo',0,1000000 * desired_time_in_ms],] # new "crochet" is 1 sec593 for old_event in old_opus[itrack]:594 # detect if ticks2tempo has something before this event595 # 20160702 if ticks2tempo is at the same time, leave it596 event_delta_ticks = old_event[1] * desired_time_in_ms597 if (i_tempo_ticks < len(tempo_ticks) and598 tempo_ticks[i_tempo_ticks] < (ticks_so_far + old_event[1]) * desired_time_in_ms):599 delta_ticks = tempo_ticks[i_tempo_ticks] - ticks_so_far600 ms_so_far += (ms_per_old_tick * delta_ticks * desired_time_in_ms)601 ticks_so_far = tempo_ticks[i_tempo_ticks]602 ms_per_old_tick = ticks2tempo[ticks_so_far] / (1000.0*old_tpq * desired_time_in_ms)603 i_tempo_ticks += 1604 event_delta_ticks -= delta_ticks605 new_event = copy.deepcopy(old_event) # now handle the new event606 ms_so_far += (ms_per_old_tick * old_event[1] * desired_time_in_ms)607 new_event[1] = round(ms_so_far - previous_ms_so_far)608 609 if pass_old_timings_events:610 if old_event[0] != 'set_tempo':611 previous_ms_so_far = ms_so_far612 new_track.append(new_event)613 else:614 new_event[0] = 'old_set_tempo'615 previous_ms_so_far = ms_so_far616 new_track.append(new_event)617 else:618 if old_event[0] != 'set_tempo':619 previous_ms_so_far = ms_so_far620 new_track.append(new_event)621 ticks_so_far += event_delta_ticks622 new_opus.append(new_track)623 itrack += 1624 _clean_up_warnings()625 return new_opus626 627def event2alsaseq(event=None): # 5.5628 r'''Converts an event into the format needed by the alsaseq module,629http://pp.com.mx/python/alsaseq630The type of track (opus or score) is autodetected.631'''632 pass633 634def grep(score=None, channels=None):635 r'''Returns a "score" containing only the channels specified636'''637 if score == None:638 return [1000,[],]639 ticks = score[0]640 new_score = [ticks,]641 if channels == None:642 return new_score643 channels = set(channels)644 global Event2channelindex645 itrack = 1646 while itrack < len(score):647 new_score.append([])648 for event in score[itrack]:649 channel_index = Event2channelindex.get(event[0], False)650 if channel_index:651 if event[channel_index] in channels:652 new_score[itrack].append(event)653 else:654 new_score[itrack].append(event)655 itrack += 1656 return new_score657 658def score2stats(opus_or_score=None):659 r'''Returns a dict of some basic stats about the score, like660bank_select (list of tuples (msb,lsb)),661channels_by_track (list of lists), channels_total (set),662general_midi_mode (list),663ntracks, nticks, patch_changes_by_track (list of dicts),664num_notes_by_channel (list of numbers),665patch_changes_total (set),666percussion (dict histogram of channel 9 events),667pitches (dict histogram of pitches on channels other than 9),668pitch_range_by_track (list, by track, of two-member-tuples),669pitch_range_sum (sum over tracks of the pitch_ranges),670'''671 bank_select_msb = -1672 bank_select_lsb = -1673 bank_select = []674 channels_by_track = []675 channels_total = set([])676 general_midi_mode = []677 num_notes_by_channel = dict([])678 patches_used_by_track = []679 patches_used_total = set([])680 patch_changes_by_track = []681 patch_changes_total = set([])682 percussion = dict([]) # histogram of channel 9 "pitches"683 pitches = dict([]) # histogram of pitch-occurrences channels 0-8,10-15684 pitch_range_sum = 0 # u pitch-ranges of each track685 pitch_range_by_track = []686 is_a_score = True687 if opus_or_score == None:688 return {'bank_select':[], 'channels_by_track':[], 'channels_total':[],689 'general_midi_mode':[], 'ntracks':0, 'nticks':0,690 'num_notes_by_channel':dict([]),691 'patch_changes_by_track':[], 'patch_changes_total':[],692 'percussion':{}, 'pitches':{}, 'pitch_range_by_track':[],693 'ticks_per_quarter':0, 'pitch_range_sum':0}694 ticks_per_quarter = opus_or_score[0]695 i = 1 # ignore first element, which is ticks696 nticks = 0697 while i < len(opus_or_score):698 highest_pitch = 0699 lowest_pitch = 128700 channels_this_track = set([])701 patch_changes_this_track = dict({})702 for event in opus_or_score[i]:703 if event[0] == 'note':704 num_notes_by_channel[event[3]] = num_notes_by_channel.get(event[3],0) + 1705 if event[3] == 9:706 percussion[event[4]] = percussion.get(event[4],0) + 1707 else:708 pitches[event[4]] = pitches.get(event[4],0) + 1709 if event[4] > highest_pitch:710 highest_pitch = event[4]711 if event[4] < lowest_pitch:712 lowest_pitch = event[4]713 channels_this_track.add(event[3])714 channels_total.add(event[3])715 finish_time = event[1] + event[2]716 if finish_time > nticks:717 nticks = finish_time718 elif event[0] == 'note_off' or (event[0] == 'note_on' and event[4] == 0): # 4.8719 finish_time = event[1]720 if finish_time > nticks:721 nticks = finish_time722 elif event[0] == 'note_on':723 is_a_score = False724 num_notes_by_channel[event[2]] = num_notes_by_channel.get(event[2],0) + 1725 if event[2] == 9:726 percussion[event[3]] = percussion.get(event[3],0) + 1727 else:728 pitches[event[3]] = pitches.get(event[3],0) + 1729 if event[3] > highest_pitch:730 highest_pitch = event[3]731 if event[3] < lowest_pitch:732 lowest_pitch = event[3]733 channels_this_track.add(event[2])734 channels_total.add(event[2])735 elif event[0] == 'patch_change':736 patch_changes_this_track[event[2]] = event[3]737 patch_changes_total.add(event[3])738 elif event[0] == 'control_change':739 if event[3] == 0: # bank select MSB740 bank_select_msb = event[4]741 elif event[3] == 32: # bank select LSB742 bank_select_lsb = event[4]743 if bank_select_msb >= 0 and bank_select_lsb >= 0:744 bank_select.append((bank_select_msb,bank_select_lsb))745 bank_select_msb = -1746 bank_select_lsb = -1747 elif event[0] == 'sysex_f0':748 if _sysex2midimode.get(event[2], -1) >= 0:749 general_midi_mode.append(_sysex2midimode.get(event[2]))750 if is_a_score:751 if event[1] > nticks:752 nticks = event[1]753 else:754 nticks += event[1]755 if lowest_pitch == 128:756 lowest_pitch = 0757 channels_by_track.append(channels_this_track)758 patch_changes_by_track.append(patch_changes_this_track)759 pitch_range_by_track.append((lowest_pitch,highest_pitch))760 pitch_range_sum += (highest_pitch-lowest_pitch)761 i += 1762 763 return {'bank_select':bank_select,764 'channels_by_track':channels_by_track,765 'channels_total':channels_total,766 'general_midi_mode':general_midi_mode,767 'ntracks':len(opus_or_score)-1,768 'nticks':nticks,769 'num_notes_by_channel':num_notes_by_channel,770 'patch_changes_by_track':patch_changes_by_track,771 'patch_changes_total':patch_changes_total,772 'percussion':percussion,773 'pitches':pitches,774 'pitch_range_by_track':pitch_range_by_track,775 'pitch_range_sum':pitch_range_sum,776 'ticks_per_quarter':ticks_per_quarter}777 778#----------------------------- Event stuff --------------------------779 780_sysex2midimode = {781 "\x7E\x7F\x09\x01\xF7": 1,782 "\x7E\x7F\x09\x02\xF7": 0,783 "\x7E\x7F\x09\x03\xF7": 2,784}785 786# Some public-access tuples:787MIDI_events = tuple('''note_off note_on key_after_touch788control_change patch_change channel_after_touch789pitch_wheel_change'''.split())790 791Text_events = tuple('''text_event copyright_text_event792track_name instrument_name lyric marker cue_point text_event_08793text_event_09 text_event_0a text_event_0b text_event_0c794text_event_0d text_event_0e text_event_0f'''.split())795 796Nontext_meta_events = tuple('''end_track set_tempo797smpte_offset time_signature key_signature sequencer_specific798raw_meta_event sysex_f0 sysex_f7 song_position song_select799tune_request'''.split())800# unsupported: raw_data801 802# Actually, 'tune_request' is is F-series event, not strictly a meta-event...803Meta_events = Text_events + Nontext_meta_events804All_events = MIDI_events + Meta_events805 806# And three dictionaries:807Number2patch = { # General MIDI patch numbers:8080:'Acoustic Grand',8091:'Bright Acoustic',8102:'Electric Grand',8113:'Honky-Tonk',8124:'Electric Piano 1',8135:'Electric Piano 2',8146:'Harpsichord',8157:'Clav',8168:'Celesta',8179:'Glockenspiel',81810:'Music Box',81911:'Vibraphone',82012:'Marimba',82113:'Xylophone',82214:'Tubular Bells',82315:'Dulcimer',82416:'Drawbar Organ',82517:'Percussive Organ',82618:'Rock Organ',82719:'Church Organ',82820:'Reed Organ',82921:'Accordion',83022:'Harmonica',83123:'Tango Accordion',83224:'Acoustic Guitar(nylon)',83325:'Acoustic Guitar(steel)',83426:'Electric Guitar(jazz)',83527:'Electric Guitar(clean)',83628:'Electric Guitar(muted)',83729:'Overdriven Guitar',83830:'Distortion Guitar',83931:'Guitar Harmonics',84032:'Acoustic Bass',84133:'Electric Bass(finger)',84234:'Electric Bass(pick)',84335:'Fretless Bass',84436:'Slap Bass 1',84537:'Slap Bass 2',84638:'Synth Bass 1',84739:'Synth Bass 2',84840:'Violin',84941:'Viola',85042:'Cello',85143:'Contrabass',85244:'Tremolo Strings',85345:'Pizzicato Strings',85446:'Orchestral Harp',85547:'Timpani',85648:'String Ensemble 1',85749:'String Ensemble 2',85850:'SynthStrings 1',85951:'SynthStrings 2',86052:'Choir Aahs',86153:'Voice Oohs',86254:'Synth Voice',86355:'Orchestra Hit',86456:'Trumpet',86557:'Trombone',86658:'Tuba',86759:'Muted Trumpet',86860:'French Horn',86961:'Brass Section',87062:'SynthBrass 1',87163:'SynthBrass 2',87264:'Soprano Sax',87365:'Alto Sax',87466:'Tenor Sax',87567:'Baritone Sax',87668:'Oboe',87769:'English Horn',87870:'Bassoon',87971:'Clarinet',88072:'Piccolo',88173:'Flute',88274:'Recorder',88375:'Pan Flute',88476:'Blown Bottle',88577:'Skakuhachi',88678:'Whistle',88779:'Ocarina',88880:'Lead 1 (square)',88981:'Lead 2 (sawtooth)',89082:'Lead 3 (calliope)',89183:'Lead 4 (chiff)',89284:'Lead 5 (charang)',89385:'Lead 6 (voice)',89486:'Lead 7 (fifths)',89587:'Lead 8 (bass+lead)',89688:'Pad 1 (new age)',89789:'Pad 2 (warm)',89890:'Pad 3 (polysynth)',89991:'Pad 4 (choir)',90092:'Pad 5 (bowed)',90193:'Pad 6 (metallic)',90294:'Pad 7 (halo)',90395:'Pad 8 (sweep)',90496:'FX 1 (rain)',90597:'FX 2 (soundtrack)',90698:'FX 3 (crystal)',90799:'FX 4 (atmosphere)',908100:'FX 5 (brightness)',909101:'FX 6 (goblins)',910102:'FX 7 (echoes)',911103:'FX 8 (sci-fi)',912104:'Sitar',913105:'Banjo',914106:'Shamisen',915107:'Koto',916108:'Kalimba',917109:'Bagpipe',918110:'Fiddle',919111:'Shanai',920112:'Tinkle Bell',921113:'Agogo',922114:'Steel Drums',923115:'Woodblock',924116:'Taiko Drum',925117:'Melodic Tom',926118:'Synth Drum',927119:'Reverse Cymbal',928120:'Guitar Fret Noise',929121:'Breath Noise',930122:'Seashore',931123:'Bird Tweet',932124:'Telephone Ring',933125:'Helicopter',934126:'Applause',935127:'Gunshot',936}937Notenum2percussion = { # General MIDI Percussion (on Channel 9):93835:'Acoustic Bass Drum',93936:'Bass Drum 1',94037:'Side Stick',94138:'Acoustic Snare',94239:'Hand Clap',94340:'Electric Snare',94441:'Low Floor Tom',94542:'Closed Hi-Hat',94643:'High Floor Tom',94744:'Pedal Hi-Hat',94845:'Low Tom',94946:'Open Hi-Hat',95047:'Low-Mid Tom',95148:'Hi-Mid Tom',95249:'Crash Cymbal 1',95350:'High Tom',95451:'Ride Cymbal 1',95552:'Chinese Cymbal',95653:'Ride Bell',95754:'Tambourine',95855:'Splash Cymbal',95956:'Cowbell',96057:'Crash Cymbal 2',96158:'Vibraslap',96259:'Ride Cymbal 2',96360:'Hi Bongo',96461:'Low Bongo',96562:'Mute Hi Conga',96663:'Open Hi Conga',96764:'Low Conga',96865:'High Timbale',96966:'Low Timbale',97067:'High Agogo',97168:'Low Agogo',97269:'Cabasa',97370:'Maracas',97471:'Short Whistle',97572:'Long Whistle',97673:'Short Guiro',97774:'Long Guiro',97875:'Claves',97976:'Hi Wood Block',98077:'Low Wood Block',98178:'Mute Cuica',98279:'Open Cuica',98380:'Mute Triangle',98481:'Open Triangle',985}986 987Event2channelindex = { 'note':3, 'note_off':2, 'note_on':2,988 'key_after_touch':2, 'control_change':2, 'patch_change':2,989 'channel_after_touch':2, 'pitch_wheel_change':2990}991 992################################################################993# The code below this line is full of frightening things, all to994# do with the actual encoding and decoding of binary MIDI data.995 996def _twobytes2int(byte_a):997 r'''decode a 16 bit quantity from two bytes,'''998 return (byte_a[1] | (byte_a[0] << 8))999 1000def _int2twobytes(int_16bit):1001 r'''encode a 16 bit quantity into two bytes,'''1002 return bytes([(int_16bit>>8) & 0xFF, int_16bit & 0xFF])1003 1004def _read_14_bit(byte_a):1005 r'''decode a 14 bit quantity from two bytes,'''1006 return (byte_a[0] | (byte_a[1] << 7))1007 1008def _write_14_bit(int_14bit):1009 r'''encode a 14 bit quantity into two bytes,'''1010 return bytes([int_14bit & 0x7F, (int_14bit>>7) & 0x7F])1011 1012def _ber_compressed_int(integer):1013 r'''BER compressed integer (not an ASN.1 BER, see perlpacktut for1014details). Its bytes represent an unsigned integer in base 128,1015most significant digit first, with as few digits as possible.1016Bit eight (the high bit) is set on each byte except the last.1017'''1018 ber = bytearray(b'')1019 seven_bits = 0x7F & integer1020 ber.insert(0, seven_bits) # XXX surely should convert to a char ?1021 integer >>= 71022 while integer > 0:1023 seven_bits = 0x7F & integer1024 ber.insert(0, 0x80|seven_bits) # XXX surely should convert to a char ?1025 integer >>= 71026 return ber1027 1028def _unshift_ber_int(ba):1029 r'''Given a bytearray, returns a tuple of (the ber-integer at the1030start, and the remainder of the bytearray).1031'''1032 if not len(ba): # 6.71033 _warn('_unshift_ber_int: no integer found')1034 return ((0, b""))1035 byte = ba[0]1036 ba = ba[1:]1037 integer = 01038 while True:1039 integer += (byte & 0x7F)1040 if not (byte & 0x80):1041 return ((integer, ba))1042 if not len(ba):1043 _warn('_unshift_ber_int: no end-of-integer found')1044 return ((0, ba))1045 byte = ba[0]1046 ba = ba[1:]1047 integer <<= 71048 1049 1050def _clean_up_warnings(): # 5.41051 # Call this before returning from any publicly callable function1052 # whenever there's a possibility that a warning might have been printed1053 # by the function, or by any private functions it might have called.1054 if _no_warning:1055 return1056 global _previous_times1057 global _previous_warning1058 if _previous_times > 1:1059 # E:1176, 0: invalid syntax (<string>, line 1176) (syntax-error) ???1060 # print(' previous message repeated '+str(_previous_times)+' times', file=sys.stderr)1061 # 6.71062 sys.stderr.write(' previous message repeated {0} times\n'.format(_previous_times))1063 elif _previous_times > 0:1064 sys.stderr.write(' previous message repeated\n')1065 _previous_times = 01066 _previous_warning = ''1067 1068 1069def _warn(s=''):1070 if _no_warning:1071 return1072 global _previous_times1073 global _previous_warning1074 if s == _previous_warning: # 5.41075 _previous_times = _previous_times + 11076 else:1077 _clean_up_warnings()1078 sys.stderr.write(str(s) + "\n")1079 _previous_warning = s1080 1081 1082def _some_text_event(which_kind=0x01, text=b'some_text', text_encoding='ISO-8859-1'):1083 if str(type(text)).find("'str'") >= 0: # 6.4 test for back-compatibility1084 data = bytes(text, encoding=text_encoding)1085 else:1086 data = bytes(text)1087 return b'\xFF' + bytes((which_kind,)) + _ber_compressed_int(len(data)) + data1088 1089 1090def _consistentise_ticks(scores): # 3.61091 # used by mix_scores, merge_scores, concatenate_scores1092 if len(scores) == 1:1093 return copy.deepcopy(scores)1094 are_consistent = True1095 ticks = scores[0][0]1096 iscore = 11097 while iscore < len(scores):1098 if scores[iscore][0] != ticks:1099 are_consistent = False1100 break1101 iscore += 11102 if are_consistent:1103 return copy.deepcopy(scores)1104 new_scores = []1105 iscore = 01106 while iscore < len(scores):1107 score = scores[iscore]1108 new_scores.append(opus2score(to_millisecs(score2opus(score))))1109 iscore += 11110 return new_scores1111 1112 1113###########################################################################1114def _decode(trackdata=b'', exclude=None, include=None,1115 event_callback=None, exclusive_event_callback=None, no_eot_magic=False):1116 r'''Decodes MIDI track data into an opus-style list of events.1117The options:1118 'exclude' is a list of event types which will be ignored SHOULD BE A SET1119 'include' (and no exclude), makes exclude a list1120 of all possible events, /minus/ what include specifies1121 'event_callback' is a coderef1122 'exclusive_event_callback' is a coderef1123'''1124 trackdata = bytearray(trackdata)1125 if exclude == None:1126 exclude = []1127 if include == None:1128 include = []1129 if include and not exclude:1130 exclude = All_events1131 include = set(include)1132 exclude = set(exclude)1133 1134 # Pointer = 0; not used here; we eat through the bytearray instead.1135 event_code = -1; # used for running status1136 event_count = 0;1137 events = []1138 1139 while (len(trackdata)):1140 # loop while there's anything to analyze ...1141 eot = False # When True, the event registrar aborts this loop1142 event_count += 11143 1144 E = []1145 # E for events - we'll feed it to the event registrar at the end.1146 1147 # Slice off the delta time code, and analyze it1148 [time, trackdata] = _unshift_ber_int(trackdata)1149 1150 # Now let's see what we can make of the command1151 first_byte = trackdata[0] & 0xFF1152 trackdata = trackdata[1:]1153 if (first_byte < 0xF0): # It's a MIDI event1154 if (first_byte & 0x80):1155 event_code = first_byte1156 else:1157 # It wants running status; use last event_code value1158 trackdata.insert(0, first_byte)1159 if (event_code == -1):1160 _warn("Running status not set; Aborting track.")1161 return []1162 1163 command = event_code & 0xF01164 channel = event_code & 0x0F1165 1166 if (command == 0xF6): # 0-byte argument1167 pass1168 elif (command == 0xC0 or command == 0xD0): # 1-byte argument1169 parameter = trackdata[0] # could be B1170 trackdata = trackdata[1:]1171 else: # 2-byte argument could be BB or 14-bit1172 parameter = (trackdata[0], trackdata[1])1173 trackdata = trackdata[2:]1174 1175 #################################################################1176 # MIDI events1177 1178 if (command == 0x80):1179 if 'note_off' in exclude:1180 continue1181 E = ['note_off', time, channel, parameter[0], parameter[1]]1182 elif (command == 0x90):1183 if 'note_on' in exclude:1184 continue1185 E = ['note_on', time, channel, parameter[0], parameter[1]]1186 elif (command == 0xA0):1187 if 'key_after_touch' in exclude:1188 continue1189 E = ['key_after_touch', time, channel, parameter[0], parameter[1]]1190 elif (command == 0xB0):1191 if 'control_change' in exclude:1192 continue1193 E = ['control_change', time, channel, parameter[0], parameter[1]]1194 elif (command == 0xC0):1195 if 'patch_change' in exclude:1196 continue1197 E = ['patch_change', time, channel, parameter]1198 elif (command == 0xD0):1199 if 'channel_after_touch' in exclude:1200 continue