Chickaboo/Advanced-MIDI-Renderer
1
1r'''#===================================================================================================================2#3# MIDI to Colab AUdio Python Module4#5# Converts any MIDI file to raw audio which is compatible 6# with Google Colab or HUgging Face Gradio7#8# Version 2.09#10# Includes full source code of MIDI and pyfluidsynth11# 12# Original source code for all modules was retrieved on 07/31/202513#14# Project Los Angeles15# Tegridy Code 202516#17#===================================================================================================================18#19# Critical dependencies20#21# pip install numpy22# sudo apt install fluidsynth23#24#===================================================================================================================25# 26# Example usage:27#28# from midi_to_colab_audio import midi_to_colab_audio29# from IPython.display import display, Audio30#31# raw_audio = midi_to_colab_audio('/content/input.mid')32#33# display(Audio(raw_audio, rate=16000, normalize=False))34#35#===================================================================================================================36#! /usr/bin/python337# unsupported 20091104 ...38# ['set_sequence_number', dtime, sequence]39# ['raw_data', dtime, raw]40 41# 20150914 jimbo1qaz MIDI.py str/bytes bug report42# I found a MIDI file which had Shift-JIS titles. When midi.py decodes it as43# latin-1, it produces a string which cannot even be accessed without raising44# a UnicodeDecodeError. Maybe, when converting raw byte strings from MIDI,45# you should keep them as bytes, not improperly decode them. However, this46# would change the API. (ie: text = a "string" ? of 0 or more bytes). It47# could break compatiblity, but there's not much else you can do to fix the bug48# https://en.wikipedia.org/wiki/Shift_JIS49 50This module offers functions: concatenate_scores(), grep(),51merge_scores(), mix_scores(), midi2opus(), midi2score(), opus2midi(),52opus2score(), play_score(), score2midi(), score2opus(), score2stats(),53score_type(), segment(), timeshift() and to_millisecs(),54where "midi" means the MIDI-file bytes (as can be put in a .mid file,55or piped into aplaymidi), and "opus" and "score" are list-structures56as inspired by Sean Burke's MIDI-Perl CPAN module.57 58Warning: Version 6.4 is not necessarily backward-compatible with59previous versions, in that text-data is now bytes, not strings.60This reflects the fact that many MIDI files have text data in61encodings other that ISO-8859-1, for example in Shift-JIS.62 63Download MIDI.py from http://www.pjb.com.au/midi/free/MIDI.py64and put it in your PYTHONPATH. MIDI.py depends on Python3.65 66There is also a call-compatible translation into Lua of this67module: see http://www.pjb.com.au/comp/lua/MIDI.html68 69Backup web site: https://peterbillam.gitlab.io/miditools/70 71The "opus" is a direct translation of the midi-file-events, where72the times are delta-times, in ticks, since the previous event.73 74The "score" is more human-centric; it uses absolute times, and75combines the separate note_on and note_off events into one "note"76event, with a duration:77 ['note', start_time, duration, channel, note, velocity] # in a "score"78 79 EVENTS (in an "opus" structure)80 ['note_off', dtime, channel, note, velocity] # in an "opus"81 ['note_on', dtime, channel, note, velocity] # in an "opus"82 ['key_after_touch', dtime, channel, note, velocity]83 ['control_change', dtime, channel, controller(0-127), value(0-127)]84 ['patch_change', dtime, channel, patch]85 ['channel_after_touch', dtime, channel, velocity]86 ['pitch_wheel_change', dtime, channel, pitch_wheel]87 ['text_event', dtime, text]88 ['copyright_text_event', dtime, text]89 ['track_name', dtime, text]90 ['instrument_name', dtime, text]91 ['lyric', dtime, text]92 ['marker', dtime, text]93 ['cue_point', dtime, text]94 ['text_event_08', dtime, text]95 ['text_event_09', dtime, text]96 ['text_event_0a', dtime, text]97 ['text_event_0b', dtime, text]98 ['text_event_0c', dtime, text]99 ['text_event_0d', dtime, text]100 ['text_event_0e', dtime, text]101 ['text_event_0f', dtime, text]102 ['end_track', dtime]103 ['set_tempo', dtime, tempo]104 ['smpte_offset', dtime, hr, mn, se, fr, ff]105 ['time_signature', dtime, nn, dd, cc, bb]106 ['key_signature', dtime, sf, mi]107 ['sequencer_specific', dtime, raw]108 ['raw_meta_event', dtime, command(0-255), raw]109 ['sysex_f0', dtime, raw]110 ['sysex_f7', dtime, raw]111 ['song_position', dtime, song_pos]112 ['song_select', dtime, song_number]113 ['tune_request', dtime]114 115 DATA TYPES116 channel = a value 0 to 15117 controller = 0 to 127 (see http://www.pjb.com.au/muscript/gm.html#cc )118 dtime = time measured in "ticks", 0 to 268435455119 velocity = a value 0 (soft) to 127 (loud)120 note = a value 0 to 127 (middle-C is 60)121 patch = 0 to 127 (see http://www.pjb.com.au/muscript/gm.html )122 pitch_wheel = a value -8192 to 8191 (0x1FFF)123 raw = bytes, of length 0 or more (for sysex events see below)124 sequence_number = a value 0 to 65,535 (0xFFFF)125 song_pos = a value 0 to 16,383 (0x3FFF)126 song_number = a value 0 to 127127 tempo = microseconds per crochet (quarter-note), 0 to 16777215128 text = bytes, of length 0 or more129 ticks = the number of ticks per crochet (quarter-note)130 131 In sysex_f0 events, the raw data must not start with a \xF0 byte,132 since this gets added automatically;133 but it must end with an explicit \xF7 byte!134 In the very unlikely case that you ever need to split sysex data135 into one sysex_f0 followed by one or more sysex_f7s, then only the136 last of those sysex_f7 events must end with the explicit \xF7 byte137 (again, the raw data of individual sysex_f7 events must not start138 with any \xF7 byte, since this gets added automatically).139 140 Since version 6.4, text data is in bytes, not in a ISO-8859-1 string.141 142 143 GOING THROUGH A SCORE WITHIN A PYTHON PROGRAM144 channels = {2,3,5,8,13}145 itrack = 1 # skip 1st element which is ticks146 while itrack < len(score):147 for event in score[itrack]:148 if event[0] == 'note': # for example,149 pass # do something to all notes150 # or, to work on events in only particular channels...151 channel_index = MIDI.Event2channelindex.get(event[0], False)152 if channel_index and (event[channel_index] in channels):153 pass # do something to channels 2,3,5,8 and 13154 itrack += 1155 156'''157 158import sys, struct, copy159# sys.stdout = os.fdopen(sys.stdout.fileno(), 'wb')160Version = '6.7'161VersionDate = '20201120'162# 20201120 6.7 call to bytest() removed, and protect _unshift_ber_int163# 20160702 6.6 to_millisecs() now handles set_tempo across multiple Tracks164# 20150921 6.5 segment restores controllers as well as patch and tempo165# 20150914 6.4 text data is bytes or bytearray, not ISO-8859-1 strings166# 20150628 6.3 absent any set_tempo, default is 120bpm (see MIDI file spec 1.1)167# 20150101 6.2 all text events can be 8-bit; let user get the right encoding168# 20141231 6.1 fix _some_text_event; sequencer_specific data can be 8-bit169# 20141230 6.0 synth_specific data can be 8-bit170# 20120504 5.9 add the contents of mid_opus_tracks()171# 20120208 5.8 fix num_notes_by_channel() ; should be a dict172# 20120129 5.7 _encode handles empty tracks; score2stats num_notes_by_channel173# 20111111 5.6 fix patch 45 and 46 in Number2patch, should be Harp174# 20110129 5.5 add mix_opus_tracks() and event2alsaseq()175# 20110126 5.4 "previous message repeated N times" to save space on stderr176# 20110125 5.2 opus2score terminates unended notes at the end of the track177# 20110124 5.1 the warnings in midi2opus display track_num178# 21110122 5.0 if garbage, midi2opus returns the opus so far179# 21110119 4.9 non-ascii chars stripped out of the text_events180# 21110110 4.8 note_on with velocity=0 treated as a note-off181# 21110108 4.6 unknown F-series event correctly eats just one byte182# 21011010 4.2 segment() uses start_time, end_time named params183# 21011005 4.1 timeshift() must not pad the set_tempo command184# 21011003 4.0 pitch2note_event must be chapitch2note_event185# 21010918 3.9 set_sequence_number supported, FWIW186# 20100913 3.7 many small bugfixes; passes all tests187# 20100910 3.6 concatenate_scores enforce ticks=1000, just like merge_scores188# 20100908 3.5 minor bugs fixed in score2stats189# 20091104 3.4 tune_request now supported190# 20091104 3.3 fixed bug in decoding song_position and song_select191# 20091104 3.2 unsupported: set_sequence_number tune_request raw_data192# 20091101 3.1 document how to traverse a score within Python193# 20091021 3.0 fixed bug in score2stats detecting GM-mode = 0194# 20091020 2.9 score2stats reports GM-mode and bank msb,lsb events195# 20091019 2.8 in merge_scores, channel 9 must remain channel 9 (in GM)196# 20091018 2.7 handles empty tracks gracefully197# 20091015 2.6 grep() selects channels198# 20091010 2.5 merge_scores reassigns channels to avoid conflicts199# 20091010 2.4 fixed bug in to_millisecs which now only does opusses200# 20091010 2.3 score2stats returns channels & patch_changes, by_track & total201# 20091010 2.2 score2stats() returns also pitches and percussion dicts202# 20091010 2.1 bugs: >= not > in segment, to notice patch_change at time 0203# 20091010 2.0 bugs: spurious pop(0) ( in _decode sysex204# 20091008 1.9 bugs: ISO decoding in sysex; str( not int( in note-off warning205# 20091008 1.8 add concatenate_scores()206# 20091006 1.7 score2stats() measures nticks and ticks_per_quarter207# 20091004 1.6 first mix_scores() and merge_scores()208# 20090424 1.5 timeshift() bugfix: earliest only sees events after from_time209# 20090330 1.4 timeshift() has also a from_time argument210# 20090322 1.3 timeshift() has also a start_time argument211# 20090319 1.2 add segment() and timeshift()212# 20090301 1.1 add to_millisecs()213 214_previous_warning = '' # 5.4215_previous_times = 0 # 5.4216#------------------------------- Encoding stuff --------------------------217 218def opus2midi(opus=[]):219 r'''The argument is a list: the first item in the list is the "ticks"220parameter, the others are the tracks. Each track is a list221of midi-events, and each event is itself a list; see above.222opus2midi() returns a bytestring of the MIDI, which can then be223written either to a file opened in binary mode (mode='wb'),224or to stdout by means of: sys.stdout.buffer.write()225 226my_opus = [227 96, 228 [ # track 0:229 ['patch_change', 0, 1, 8], # and these are the events...230 ['note_on', 5, 1, 25, 96],231 ['note_off', 96, 1, 25, 0],232 ['note_on', 0, 1, 29, 96],233 ['note_off', 96, 1, 29, 0],234 ], # end of track 0235]236my_midi = opus2midi(my_opus)237sys.stdout.buffer.write(my_midi)238'''239 if len(opus) < 2:240 opus=[1000, [],]241 tracks = copy.deepcopy(opus)242 ticks = int(tracks.pop(0))243 ntracks = len(tracks)244 if ntracks == 1:245 format = 0246 else:247 format = 1248 249 my_midi = b"MThd\x00\x00\x00\x06"+struct.pack('>HHH',format,ntracks,ticks)250 for track in tracks:251 events = _encode(track)252 my_midi += b'MTrk' + struct.pack('>I',len(events)) + events253 _clean_up_warnings()254 return my_midi255 256 257def score2opus(score=None):258 r'''259The argument is a list: the first item in the list is the "ticks"260parameter, the others are the tracks. Each track is a list261of score-events, and each event is itself a list. A score-event262is similar to an opus-event (see above), except that in a score:263 1) the times are expressed as an absolute number of ticks264 from the track's start time265 2) the pairs of 'note_on' and 'note_off' events in an "opus"266 are abstracted into a single 'note' event in a "score":267 ['note', start_time, duration, channel, pitch, velocity]268score2opus() returns a list specifying the equivalent "opus".269 270my_score = [271 96,272 [ # track 0:273 ['patch_change', 0, 1, 8],274 ['note', 5, 96, 1, 25, 96],275 ['note', 101, 96, 1, 29, 96]276 ], # end of track 0277]278my_opus = score2opus(my_score)279'''280 if len(score) < 2:281 score=[1000, [],]282 tracks = copy.deepcopy(score)283 ticks = int(tracks.pop(0))284 opus_tracks = []285 for scoretrack in tracks:286 time2events = dict([])287 for scoreevent in scoretrack:288 if scoreevent[0] == 'note':289 note_on_event = ['note_on',scoreevent[1],290 scoreevent[3],scoreevent[4],scoreevent[5]]291 note_off_event = ['note_off',scoreevent[1]+scoreevent[2],292 scoreevent[3],scoreevent[4],scoreevent[5]]293 if time2events.get(note_on_event[1]):294 time2events[note_on_event[1]].append(note_on_event)295 else:296 time2events[note_on_event[1]] = [note_on_event,]297 if time2events.get(note_off_event[1]):298 time2events[note_off_event[1]].append(note_off_event)299 else:300 time2events[note_off_event[1]] = [note_off_event,]301 continue302 if time2events.get(scoreevent[1]):303 time2events[scoreevent[1]].append(scoreevent)304 else:305 time2events[scoreevent[1]] = [scoreevent,]306 307 sorted_times = [] # list of keys308 for k in time2events.keys():309 sorted_times.append(k)310 sorted_times.sort()311 312 sorted_events = [] # once-flattened list of values sorted by key313 for time in sorted_times:314 sorted_events.extend(time2events[time])315 316 abs_time = 0317 for event in sorted_events: # convert abs times => delta times318 delta_time = event[1] - abs_time319 abs_time = event[1]320 event[1] = delta_time321 opus_tracks.append(sorted_events)322 opus_tracks.insert(0,ticks)323 _clean_up_warnings()324 return opus_tracks325 326def score2midi(score=None):327 r'''328Translates a "score" into MIDI, using score2opus() then opus2midi()329'''330 return opus2midi(score2opus(score))331 332#--------------------------- Decoding stuff ------------------------333 334def midi2opus(midi=b''):335 r'''Translates MIDI into a "opus". For a description of the336"opus" format, see opus2midi()337'''338 my_midi=bytearray(midi)339 if len(my_midi) < 4:340 _clean_up_warnings()341 return [1000,[],]342 id = bytes(my_midi[0:4])343 if id != b'MThd':344 _warn("midi2opus: midi starts with "+str(id)+" instead of 'MThd'")345 _clean_up_warnings()346 return [1000,[],]347 [length, format, tracks_expected, ticks] = struct.unpack(348 '>IHHH', bytes(my_midi[4:14]))349 if length != 6:350 _warn("midi2opus: midi header length was "+str(length)+" instead of 6")351 _clean_up_warnings()352 return [1000,[],]353 my_opus = [ticks,]354 my_midi = my_midi[14:]355 track_num = 1 # 5.1356 while len(my_midi) >= 8:357 track_type = bytes(my_midi[0:4])358 if track_type != b'MTrk':359 _warn('midi2opus: Warning: track #'+str(track_num)+' type is '+str(track_type)+" instead of b'MTrk'")360 [track_length] = struct.unpack('>I', my_midi[4:8])361 my_midi = my_midi[8:]362 if track_length > len(my_midi):363 _warn('midi2opus: track #'+str(track_num)+' length '+str(track_length)+' is too large')364 _clean_up_warnings()365 return my_opus # 5.0366 my_midi_track = my_midi[0:track_length]367 my_track = _decode(my_midi_track)368 my_opus.append(my_track)369 my_midi = my_midi[track_length:]370 track_num += 1 # 5.1371 _clean_up_warnings()372 return my_opus373 374def opus2score(opus=[]):375 r'''For a description of the "opus" and "score" formats,376see opus2midi() and score2opus().377'''378 if len(opus) < 2:379 _clean_up_warnings()380 return [1000,[],]381 tracks = copy.deepcopy(opus) # couple of slices probably quicker...382 ticks = int(tracks.pop(0))383 score = [ticks,]384 for opus_track in tracks:385 ticks_so_far = 0386 score_track = []387 chapitch2note_on_events = dict([]) # 4.0388 for opus_event in opus_track:389 ticks_so_far += opus_event[1]390 if opus_event[0] == 'note_off' or (opus_event[0] == 'note_on' and opus_event[4] == 0): # 4.8391 cha = opus_event[2]392 pitch = opus_event[3]393 key = cha*128 + pitch394 if chapitch2note_on_events.get(key):395 new_event = chapitch2note_on_events[key].pop(0)396 new_event[2] = ticks_so_far - new_event[1]397 score_track.append(new_event)398 elif pitch > 127:399 pass #_warn('opus2score: note_off with no note_on, bad pitch='+str(pitch))400 else:401 pass #_warn('opus2score: note_off with no note_on cha='+str(cha)+' pitch='+str(pitch))402 elif opus_event[0] == 'note_on':403 cha = opus_event[2]404 pitch = opus_event[3]405 key = cha*128 + pitch406 new_event = ['note',ticks_so_far,0,cha,pitch, opus_event[4]]407 if chapitch2note_on_events.get(key):408 chapitch2note_on_events[key].append(new_event)409 else:410 chapitch2note_on_events[key] = [new_event,]411 else:412 opus_event[1] = ticks_so_far413 score_track.append(opus_event)414 # check for unterminated notes (Oisín) -- 5.2415 for chapitch in chapitch2note_on_events:416 note_on_events = chapitch2note_on_events[chapitch]417 for new_e in note_on_events:418 new_e[2] = ticks_so_far - new_e[1]419 score_track.append(new_e)420 pass #_warn("opus2score: note_on with no note_off cha="+str(new_e[3])+' pitch='+str(new_e[4])+'; adding note_off at end')421 score.append(score_track)422 _clean_up_warnings()423 return score424 425def midi2score(midi=b''):426 r'''427Translates MIDI into a "score", using midi2opus() then opus2score()428'''429 return opus2score(midi2opus(midi))430 431def midi2ms_score(midi=b''):432 r'''433Translates MIDI into a "score" with one beat per second and one434tick per millisecond, using midi2opus() then to_millisecs()435then opus2score()436'''437 return opus2score(to_millisecs(midi2opus(midi)))438 439#------------------------ Other Transformations ---------------------440 441def to_millisecs(old_opus=None):442 r'''Recallibrates all the times in an "opus" to use one beat443per second and one tick per millisecond. This makes it444hard to retrieve any information about beats or barlines,445but it does make it easy to mix different scores together.446'''447 if old_opus == None:448 return [1000,[],]449 try:450 old_tpq = int(old_opus[0])451 except IndexError: # 5.0452 _warn('to_millisecs: the opus '+str(type(old_opus))+' has no elements')453 return [1000,[],]454 new_opus = [1000,]455 # 6.7 first go through building a table of set_tempos by absolute-tick456 ticks2tempo = {}457 itrack = 1458 while itrack < len(old_opus):459 ticks_so_far = 0460 for old_event in old_opus[itrack]:461 if old_event[0] == 'note':462 raise TypeError('to_millisecs needs an opus, not a score')463 ticks_so_far += old_event[1]464 if old_event[0] == 'set_tempo':465 ticks2tempo[ticks_so_far] = old_event[2]466 itrack += 1467 # then get the sorted-array of their keys468 tempo_ticks = [] # list of keys469 for k in ticks2tempo.keys():470 tempo_ticks.append(k)471 tempo_ticks.sort()472 # then go through converting to millisec, testing if the next473 # set_tempo lies before the next track-event, and using it if so.474 itrack = 1475 while itrack < len(old_opus):476 ms_per_old_tick = 500.0 / old_tpq # float: will round later 6.3477 i_tempo_ticks = 0478 ticks_so_far = 0479 ms_so_far = 0.0480 previous_ms_so_far = 0.0481 new_track = [['set_tempo',0,1000000],] # new "crochet" is 1 sec482 for old_event in old_opus[itrack]:483 # detect if ticks2tempo has something before this event484 # 20160702 if ticks2tempo is at the same time, leave it485 event_delta_ticks = old_event[1]486 if (i_tempo_ticks < len(tempo_ticks) and487 tempo_ticks[i_tempo_ticks] < (ticks_so_far + old_event[1])):488 delta_ticks = tempo_ticks[i_tempo_ticks] - ticks_so_far489 ms_so_far += (ms_per_old_tick * delta_ticks)490 ticks_so_far = tempo_ticks[i_tempo_ticks]491 ms_per_old_tick = ticks2tempo[ticks_so_far] / (1000.0*old_tpq)492 i_tempo_ticks += 1493 event_delta_ticks -= delta_ticks494 new_event = copy.deepcopy(old_event) # now handle the new event495 ms_so_far += (ms_per_old_tick * old_event[1])496 new_event[1] = round(ms_so_far - previous_ms_so_far)497 if old_event[0] != 'set_tempo':498 previous_ms_so_far = ms_so_far499 new_track.append(new_event)500 ticks_so_far += event_delta_ticks501 new_opus.append(new_track)502 itrack += 1503 _clean_up_warnings()504 return new_opus505 506def event2alsaseq(event=None): # 5.5507 r'''Converts an event into the format needed by the alsaseq module,508http://pp.com.mx/python/alsaseq509The type of track (opus or score) is autodetected.510'''511 pass512 513def grep(score=None, channels=None):514 r'''Returns a "score" containing only the channels specified515'''516 if score == None:517 return [1000,[],]518 ticks = score[0]519 new_score = [ticks,]520 if channels == None:521 return new_score522 channels = set(channels)523 global Event2channelindex524 itrack = 1525 while itrack < len(score):526 new_score.append([])527 for event in score[itrack]:528 channel_index = Event2channelindex.get(event[0], False)529 if channel_index:530 if event[channel_index] in channels:531 new_score[itrack].append(event)532 else:533 new_score[itrack].append(event)534 itrack += 1535 return new_score536 537def play_score(score=None):538 r'''Converts the "score" to midi, and feeds it into 'aplaymidi -'539'''540 if score == None:541 return542 import subprocess543 pipe = subprocess.Popen(['aplaymidi','-'], stdin=subprocess.PIPE)544 if score_type(score) == 'opus':545 pipe.stdin.write(opus2midi(score))546 else:547 pipe.stdin.write(score2midi(score))548 pipe.stdin.close()549 550def timeshift(score=None, shift=None, start_time=None, from_time=0, tracks={0,1,2,3,4,5,6,7,8,10,12,13,14,15}):551 r'''Returns a "score" shifted in time by "shift" ticks, or shifted552so that the first event starts at "start_time" ticks.553 554If "from_time" is specified, only those events in the score555that begin after it are shifted. If "start_time" is less than556"from_time" (or "shift" is negative), then the intermediate557notes are deleted, though patch-change events are preserved.558 559If "tracks" are specified, then only those tracks get shifted.560"tracks" can be a list, tuple or set; it gets converted to set561internally.562 563It is deprecated to specify both "shift" and "start_time".564If this does happen, timeshift() will print a warning to565stderr and ignore the "shift" argument.566 567If "shift" is negative and sufficiently large that it would568leave some event with a negative tick-value, then the score569is shifted so that the first event occurs at time 0. This570also occurs if "start_time" is negative, and is also the571default if neither "shift" nor "start_time" are specified.572'''573 #_warn('tracks='+str(tracks))574 if score == None or len(score) < 2:575 return [1000, [],]576 new_score = [score[0],]577 my_type = score_type(score)578 if my_type == '':579 return new_score580 if my_type == 'opus':581 _warn("timeshift: opus format is not supported\n")582 # _clean_up_scores() 6.2; doesn't exist! what was it supposed to do?583 return new_score584 if not (shift == None) and not (start_time == None):585 _warn("timeshift: shift and start_time specified: ignoring shift\n")586 shift = None587 if shift == None:588 if (start_time == None) or (start_time < 0):589 start_time = 0590 # shift = start_time - from_time591 592 i = 1 # ignore first element (ticks)593 tracks = set(tracks) # defend against tuples and lists594 earliest = 1000000000595 if not (start_time == None) or shift < 0: # first find the earliest event596 while i < len(score):597 if len(tracks) and not ((i-1) in tracks):598 i += 1599 continue600 for event in score[i]:601 if event[1] < from_time:602 continue # just inspect the to_be_shifted events603 if event[1] < earliest:604 earliest = event[1]605 i += 1606 if earliest > 999999999:607 earliest = 0608 if shift == None:609 shift = start_time - earliest610 elif (earliest + shift) < 0:611 start_time = 0612 shift = 0 - earliest613 614 i = 1 # ignore first element (ticks)615 while i < len(score):616 if len(tracks) == 0 or not ((i-1) in tracks): # 3.8617 new_score.append(score[i])618 i += 1619 continue620 new_track = []621 for event in score[i]:622 new_event = list(event)623 #if new_event[1] == 0 and shift > 0 and new_event[0] != 'note':624 # pass625 #elif new_event[1] >= from_time:626 if new_event[1] >= from_time:627 # 4.1 must not rightshift set_tempo628 if new_event[0] != 'set_tempo' or shift<0:629 new_event[1] += shift630 elif (shift < 0) and (new_event[1] >= (from_time+shift)):631 continue632 new_track.append(new_event)633 if len(new_track) > 0:634 new_score.append(new_track)635 i += 1636 _clean_up_warnings()637 return new_score638 639def segment(score=None, start_time=None, end_time=None, start=0, end=100000000,640 tracks={0,1,2,3,4,5,6,7,8,10,11,12,13,14,15}):641 r'''Returns a "score" which is a segment of the one supplied642as the argument, beginning at "start_time" ticks and ending643at "end_time" ticks (or at the end if "end_time" is not supplied).644If the set "tracks" is specified, only those tracks will645be returned.646'''647 if score == None or len(score) < 2:648 return [1000, [],]649 if start_time == None: # as of 4.2 start_time is recommended650 start_time = start # start is legacy usage651 if end_time == None: # likewise652 end_time = end653 new_score = [score[0],]654 my_type = score_type(score)655 if my_type == '':656 return new_score657 if my_type == 'opus':658 # more difficult (disconnecting note_on's from their note_off's)...659 _warn("segment: opus format is not supported\n")660 _clean_up_warnings()661 return new_score662 i = 1 # ignore first element (ticks); we count in ticks anyway663 tracks = set(tracks) # defend against tuples and lists664 while i < len(score):665 if len(tracks) and not ((i-1) in tracks):666 i += 1667 continue668 new_track = []669 channel2cc_num = {} # most recent controller change before start670 channel2cc_val = {}671 channel2cc_time = {}672 channel2patch_num = {} # keep most recent patch change before start673 channel2patch_time = {}674 set_tempo_num = 500000 # most recent tempo change before start 6.3675 set_tempo_time = 0676 earliest_note_time = end_time677 for event in score[i]:678 if event[0] == 'control_change': # 6.5679 cc_time = channel2cc_time.get(event[2]) or 0680 if (event[1] <= start_time) and (event[1] >= cc_time):681 channel2cc_num[event[2]] = event[3]682 channel2cc_val[event[2]] = event[4]683 channel2cc_time[event[2]] = event[1]684 elif event[0] == 'patch_change':685 patch_time = channel2patch_time.get(event[2]) or 0686 if (event[1]<=start_time) and (event[1] >= patch_time): # 2.0687 channel2patch_num[event[2]] = event[3]688 channel2patch_time[event[2]] = event[1]689 elif event[0] == 'set_tempo':690 if (event[1]<=start_time) and (event[1]>=set_tempo_time): #6.4691 set_tempo_num = event[2]692 set_tempo_time = event[1]693 if (event[1] >= start_time) and (event[1] <= end_time):694 new_track.append(event)695 if (event[0] == 'note') and (event[1] < earliest_note_time):696 earliest_note_time = event[1]697 if len(new_track) > 0:698 new_track.append(['set_tempo', start_time, set_tempo_num])699 for c in channel2patch_num:700 new_track.append(['patch_change',start_time,c,channel2patch_num[c]],)701 for c in channel2cc_num: # 6.5702 new_track.append(['control_change',start_time,c,channel2cc_num[c],channel2cc_val[c]])703 new_score.append(new_track)704 i += 1705 _clean_up_warnings()706 return new_score707 708def score_type(opus_or_score=None):709 r'''Returns a string, either 'opus' or 'score' or ''710'''711 if opus_or_score == None or str(type(opus_or_score)).find('list')<0 or len(opus_or_score) < 2:712 return ''713 i = 1 # ignore first element714 while i < len(opus_or_score):715 for event in opus_or_score[i]:716 if event[0] == 'note':717 return 'score'718 elif event[0] == 'note_on':719 return 'opus'720 i += 1721 return ''722 723def concatenate_scores(scores):724 r'''Concatenates a list of scores into one score.725If the scores differ in their "ticks" parameter,726they will all get converted to millisecond-tick format.727'''728 # the deepcopys are needed if the input_score's are refs to the same obj729 # e.g. if invoked by midisox's repeat()730 input_scores = _consistentise_ticks(scores) # 3.7731 output_score = copy.deepcopy(input_scores[0])732 for input_score in input_scores[1:]:733 output_stats = score2stats(output_score)734 delta_ticks = output_stats['nticks']735 itrack = 1736 while itrack < len(input_score):737 if itrack >= len(output_score): # new output track if doesn't exist738 output_score.append([])739 for event in input_score[itrack]:740 output_score[itrack].append(copy.deepcopy(event))741 output_score[itrack][-1][1] += delta_ticks742 itrack += 1743 return output_score744 745def merge_scores(scores):746 r'''Merges a list of scores into one score. A merged score comprises747all of the tracks from all of the input scores; un-merging is possible748by selecting just some of the tracks. If the scores differ in their749"ticks" parameter, they will all get converted to millisecond-tick750format. merge_scores attempts to resolve channel-conflicts,751but there are of course only 15 available channels...752'''753 input_scores = _consistentise_ticks(scores) # 3.6754 output_score = [1000]755 channels_so_far = set()756 all_channels = {0,1,2,3,4,5,6,7,8,10,11,12,13,14,15}757 global Event2channelindex758 for input_score in input_scores:759 new_channels = set(score2stats(input_score).get('channels_total', []))760 new_channels.discard(9) # 2.8 cha9 must remain cha9 (in GM)761 for channel in channels_so_far & new_channels:762 # consistently choose lowest avaiable, to ease testing763 free_channels = list(all_channels - (channels_so_far|new_channels))764 if len(free_channels) > 0:765 free_channels.sort()766 free_channel = free_channels[0]767 else:768 free_channel = None769 break770 itrack = 1771 while itrack < len(input_score):772 for input_event in input_score[itrack]:773 channel_index=Event2channelindex.get(input_event[0],False)774 if channel_index and input_event[channel_index]==channel:775 input_event[channel_index] = free_channel776 itrack += 1777 channels_so_far.add(free_channel)778 779 channels_so_far |= new_channels780 output_score.extend(input_score[1:])781 return output_score782 783def _ticks(event):784 return event[1]785def mix_opus_tracks(input_tracks): # 5.5786 r'''Mixes an array of tracks into one track. A mixed track787cannot be un-mixed. It is assumed that the tracks share the same788ticks parameter and the same tempo.789Mixing score-tracks is trivial (just insert all events into one array).790Mixing opus-tracks is only slightly harder, but it's common enough791that a dedicated function is useful.792'''793 output_score = [1000, []]794 for input_track in input_tracks: # 5.8795 input_score = opus2score([1000, input_track])796 for event in input_score[1]:797 output_score[1].append(event)798 output_score[1].sort(key=_ticks) 799 output_opus = score2opus(output_score)800 return output_opus[1]801 802def mix_scores(scores):803 r'''Mixes a list of scores into one one-track score.804A mixed score cannot be un-mixed. Hopefully the scores805have no undesirable channel-conflicts between them.806If the scores differ in their "ticks" parameter,807they will all get converted to millisecond-tick format.808'''809 input_scores = _consistentise_ticks(scores) # 3.6810 output_score = [1000, []]811 for input_score in input_scores:812 for input_track in input_score[1:]:813 output_score[1].extend(input_track)814 return output_score815 816def score2stats(opus_or_score=None):817 r'''Returns a dict of some basic stats about the score, like818bank_select (list of tuples (msb,lsb)),819channels_by_track (list of lists), channels_total (set),820general_midi_mode (list),821ntracks, nticks, patch_changes_by_track (list of dicts),822num_notes_by_channel (list of numbers),823patch_changes_total (set),824percussion (dict histogram of channel 9 events),825pitches (dict histogram of pitches on channels other than 9),826pitch_range_by_track (list, by track, of two-member-tuples),827pitch_range_sum (sum over tracks of the pitch_ranges),828'''829 bank_select_msb = -1830 bank_select_lsb = -1831 bank_select = []832 channels_by_track = []833 channels_total = set([])834 general_midi_mode = []835 num_notes_by_channel = dict([])836 patches_used_by_track = []837 patches_used_total = set([])838 patch_changes_by_track = []839 patch_changes_total = set([])840 percussion = dict([]) # histogram of channel 9 "pitches"841 pitches = dict([]) # histogram of pitch-occurrences channels 0-8,10-15842 pitch_range_sum = 0 # u pitch-ranges of each track843 pitch_range_by_track = []844 is_a_score = True845 if opus_or_score == None:846 return {'bank_select':[], 'channels_by_track':[], 'channels_total':[],847 'general_midi_mode':[], 'ntracks':0, 'nticks':0,848 'num_notes_by_channel':dict([]),849 'patch_changes_by_track':[], 'patch_changes_total':[],850 'percussion':{}, 'pitches':{}, 'pitch_range_by_track':[],851 'ticks_per_quarter':0, 'pitch_range_sum':0}852 ticks_per_quarter = opus_or_score[0]853 i = 1 # ignore first element, which is ticks854 nticks = 0855 while i < len(opus_or_score):856 highest_pitch = 0857 lowest_pitch = 128858 channels_this_track = set([])859 patch_changes_this_track = dict({})860 for event in opus_or_score[i]:861 if event[0] == 'note':862 num_notes_by_channel[event[3]] = num_notes_by_channel.get(event[3],0) + 1863 if event[3] == 9:864 percussion[event[4]] = percussion.get(event[4],0) + 1865 else:866 pitches[event[4]] = pitches.get(event[4],0) + 1867 if event[4] > highest_pitch:868 highest_pitch = event[4]869 if event[4] < lowest_pitch:870 lowest_pitch = event[4]871 channels_this_track.add(event[3])872 channels_total.add(event[3])873 finish_time = event[1] + event[2]874 if finish_time > nticks:875 nticks = finish_time876 elif event[0] == 'note_off' or (event[0] == 'note_on' and event[4] == 0): # 4.8877 finish_time = event[1]878 if finish_time > nticks:879 nticks = finish_time880 elif event[0] == 'note_on':881 is_a_score = False882 num_notes_by_channel[event[2]] = num_notes_by_channel.get(event[2],0) + 1883 if event[2] == 9:884 percussion[event[3]] = percussion.get(event[3],0) + 1885 else:886 pitches[event[3]] = pitches.get(event[3],0) + 1887 if event[3] > highest_pitch:888 highest_pitch = event[3]889 if event[3] < lowest_pitch:890 lowest_pitch = event[3]891 channels_this_track.add(event[2])892 channels_total.add(event[2])893 elif event[0] == 'patch_change':894 patch_changes_this_track[event[2]] = event[3]895 patch_changes_total.add(event[3])896 elif event[0] == 'control_change':897 if event[3] == 0: # bank select MSB898 bank_select_msb = event[4]899 elif event[3] == 32: # bank select LSB900 bank_select_lsb = event[4]901 if bank_select_msb >= 0 and bank_select_lsb >= 0:902 bank_select.append((bank_select_msb,bank_select_lsb))903 bank_select_msb = -1904 bank_select_lsb = -1905 elif event[0] == 'sysex_f0':906 if _sysex2midimode.get(event[2], -1) >= 0:907 general_midi_mode.append(_sysex2midimode.get(event[2]))908 if is_a_score:909 if event[1] > nticks:910 nticks = event[1]911 else:912 nticks += event[1]913 if lowest_pitch == 128:914 lowest_pitch = 0915 channels_by_track.append(channels_this_track)916 patch_changes_by_track.append(patch_changes_this_track)917 pitch_range_by_track.append((lowest_pitch,highest_pitch))918 pitch_range_sum += (highest_pitch-lowest_pitch)919 i += 1920 921 return {'bank_select':bank_select,922 'channels_by_track':channels_by_track,923 'channels_total':channels_total,924 'general_midi_mode':general_midi_mode,925 'ntracks':len(opus_or_score)-1,926 'nticks':nticks,927 'num_notes_by_channel':num_notes_by_channel,928 'patch_changes_by_track':patch_changes_by_track,929 'patch_changes_total':patch_changes_total,930 'percussion':percussion,931 'pitches':pitches,932 'pitch_range_by_track':pitch_range_by_track,933 'pitch_range_sum':pitch_range_sum,934 'ticks_per_quarter':ticks_per_quarter}935 936#----------------------------- Event stuff --------------------------937 938_sysex2midimode = {939 "\x7E\x7F\x09\x01\xF7": 1,940 "\x7E\x7F\x09\x02\xF7": 0,941 "\x7E\x7F\x09\x03\xF7": 2,942}943 944# Some public-access tuples:945MIDI_events = tuple('''note_off note_on key_after_touch946control_change patch_change channel_after_touch947pitch_wheel_change'''.split())948 949Text_events = tuple('''text_event copyright_text_event950track_name instrument_name lyric marker cue_point text_event_08951text_event_09 text_event_0a text_event_0b text_event_0c952text_event_0d text_event_0e text_event_0f'''.split())953 954Nontext_meta_events = tuple('''end_track set_tempo955smpte_offset time_signature key_signature sequencer_specific956raw_meta_event sysex_f0 sysex_f7 song_position song_select957tune_request'''.split())958# unsupported: raw_data959 960# Actually, 'tune_request' is is F-series event, not strictly a meta-event...961Meta_events = Text_events + Nontext_meta_events962All_events = MIDI_events + Meta_events963 964# And three dictionaries:965Number2patch = { # General MIDI patch numbers:9660:'Acoustic Grand',9671:'Bright Acoustic',9682:'Electric Grand',9693:'Honky-Tonk',9704:'Electric Piano 1',9715:'Electric Piano 2',9726:'Harpsichord',9737:'Clav',9748:'Celesta',9759:'Glockenspiel',97610:'Music Box',97711:'Vibraphone',97812:'Marimba',97913:'Xylophone',98014:'Tubular Bells',98115:'Dulcimer',98216:'Drawbar Organ',98317:'Percussive Organ',98418:'Rock Organ',98519:'Church Organ',98620:'Reed Organ',98721:'Accordion',98822:'Harmonica',98923:'Tango Accordion',99024:'Acoustic Guitar(nylon)',99125:'Acoustic Guitar(steel)',99226:'Electric Guitar(jazz)',99327:'Electric Guitar(clean)',99428:'Electric Guitar(muted)',99529:'Overdriven Guitar',99630:'Distortion Guitar',99731:'Guitar Harmonics',99832:'Acoustic Bass',99933:'Electric Bass(finger)',100034:'Electric Bass(pick)',100135:'Fretless Bass',100236:'Slap Bass 1',100337:'Slap Bass 2',100438:'Synth Bass 1',100539:'Synth Bass 2',100640:'Violin',100741:'Viola',100842:'Cello',100943:'Contrabass',101044:'Tremolo Strings',101145:'Pizzicato Strings',101246:'Orchestral Harp',101347:'Timpani',101448:'String Ensemble 1',101549:'String Ensemble 2',101650:'SynthStrings 1',101751:'SynthStrings 2',101852:'Choir Aahs',101953:'Voice Oohs',102054:'Synth Voice',102155:'Orchestra Hit',102256:'Trumpet',102357:'Trombone',102458:'Tuba',102559:'Muted Trumpet',102660:'French Horn',102761:'Brass Section',102862:'SynthBrass 1',102963:'SynthBrass 2',103064:'Soprano Sax',103165:'Alto Sax',103266:'Tenor Sax',103367:'Baritone Sax',103468:'Oboe',103569:'English Horn',103670:'Bassoon',103771:'Clarinet',103872:'Piccolo',103973:'Flute',104074:'Recorder',104175:'Pan Flute',104276:'Blown Bottle',104377:'Skakuhachi',104478:'Whistle',104579:'Ocarina',104680:'Lead 1 (square)',104781:'Lead 2 (sawtooth)',104882:'Lead 3 (calliope)',104983:'Lead 4 (chiff)',105084:'Lead 5 (charang)',105185:'Lead 6 (voice)',105286:'Lead 7 (fifths)',105387:'Lead 8 (bass+lead)',105488:'Pad 1 (new age)',105589:'Pad 2 (warm)',105690:'Pad 3 (polysynth)',105791:'Pad 4 (choir)',105892:'Pad 5 (bowed)',105993:'Pad 6 (metallic)',106094:'Pad 7 (halo)',106195:'Pad 8 (sweep)',106296:'FX 1 (rain)',106397:'FX 2 (soundtrack)',106498:'FX 3 (crystal)',106599:'FX 4 (atmosphere)',1066100:'FX 5 (brightness)',1067101:'FX 6 (goblins)',1068102:'FX 7 (echoes)',1069103:'FX 8 (sci-fi)',1070104:'Sitar',1071105:'Banjo',1072106:'Shamisen',1073107:'Koto',1074108:'Kalimba',1075109:'Bagpipe',1076110:'Fiddle',1077111:'Shanai',1078112:'Tinkle Bell',1079113:'Agogo',1080114:'Steel Drums',1081115:'Woodblock',1082116:'Taiko Drum',1083117:'Melodic Tom',1084118:'Synth Drum',1085119:'Reverse Cymbal',1086120:'Guitar Fret Noise',1087121:'Breath Noise',1088122:'Seashore',1089123:'Bird Tweet',1090124:'Telephone Ring',1091125:'Helicopter',1092126:'Applause',1093127:'Gunshot',1094}1095Notenum2percussion = { # General MIDI Percussion (on Channel 9):109635:'Acoustic Bass Drum',109736:'Bass Drum 1',109837:'Side Stick',109938:'Acoustic Snare',110039:'Hand Clap',110140:'Electric Snare',110241:'Low Floor Tom',110342:'Closed Hi-Hat',110443:'High Floor Tom',110544:'Pedal Hi-Hat',110645:'Low Tom',110746:'Open Hi-Hat',110847:'Low-Mid Tom',110948:'Hi-Mid Tom',111049:'Crash Cymbal 1',111150:'High Tom',111251:'Ride Cymbal 1',111352:'Chinese Cymbal',111453:'Ride Bell',111554:'Tambourine',111655:'Splash Cymbal',111756:'Cowbell',111857:'Crash Cymbal 2',111958:'Vibraslap',112059:'Ride Cymbal 2',112160:'Hi Bongo',112261:'Low Bongo',112362:'Mute Hi Conga',112463:'Open Hi Conga',112564:'Low Conga',112665:'High Timbale',112766:'Low Timbale',112867:'High Agogo',112968:'Low Agogo',113069:'Cabasa',113170:'Maracas',113271:'Short Whistle',113372:'Long Whistle',113473:'Short Guiro',113574:'Long Guiro',113675:'Claves',113776:'Hi Wood Block',113877:'Low Wood Block',113978:'Mute Cuica',114079:'Open Cuica',114180:'Mute Triangle',114281:'Open Triangle',1143}1144 1145Event2channelindex = { 'note':3, 'note_off':2, 'note_on':2,1146 'key_after_touch':2, 'control_change':2, 'patch_change':2,1147 'channel_after_touch':2, 'pitch_wheel_change':21148}1149 1150################################################################1151# The code below this line is full of frightening things, all to1152# do with the actual encoding and decoding of binary MIDI data.1153 1154def _twobytes2int(byte_a):1155 r'''decode a 16 bit quantity from two bytes,'''1156 return (byte_a[1] | (byte_a[0] << 8))1157 1158def _int2twobytes(int_16bit):1159 r'''encode a 16 bit quantity into two bytes,'''1160 return bytes([(int_16bit>>8) & 0xFF, int_16bit & 0xFF])1161 1162def _read_14_bit(byte_a):1163 r'''decode a 14 bit quantity from two bytes,'''1164 return (byte_a[0] | (byte_a[1] << 7))1165 1166def _write_14_bit(int_14bit):1167 r'''encode a 14 bit quantity into two bytes,'''1168 return bytes([int_14bit & 0x7F, (int_14bit>>7) & 0x7F])1169 1170def _ber_compressed_int(integer):1171 r'''BER compressed integer (not an ASN.1 BER, see perlpacktut for1172details). Its bytes represent an unsigned integer in base 128,1173most significant digit first, with as few digits as possible.1174Bit eight (the high bit) is set on each byte except the last.1175'''1176 ber = bytearray(b'')1177 seven_bits = 0x7F & integer1178 ber.insert(0, seven_bits) # XXX surely should convert to a char ?1179 integer >>= 71180 while integer > 0:1181 seven_bits = 0x7F & integer1182 ber.insert(0, 0x80|seven_bits) # XXX surely should convert to a char ?1183 integer >>= 71184 return ber1185 1186def _unshift_ber_int(ba):1187 r'''Given a bytearray, returns a tuple of (the ber-integer at the1188start, and the remainder of the bytearray).1189'''1190 if not len(ba): # 6.71191 _warn('_unshift_ber_int: no integer found')1192 return ((0, b""))1193 byte = ba.pop(0)1194 integer = 01195 while True:1196 integer += (byte & 0x7F)1197 if not (byte & 0x80):1198 return ((integer, ba))1199 if not len(ba):1200 _warn('_unshift_ber_int: no end-of-integer found')