Chickaboo/Advanced-MIDI-Renderer
1
1#! /usr/bin/python32# unsupported 20091104 ...3# ['set_sequence_number', dtime, sequence]4# ['raw_data', dtime, raw]5 6# 20150914 jimbo1qaz MIDI.py str/bytes bug report7# I found a MIDI file which had Shift-JIS titles. When midi.py decodes it as8# latin-1, it produces a string which cannot even be accessed without raising9# a UnicodeDecodeError. Maybe, when converting raw byte strings from MIDI,10# you should keep them as bytes, not improperly decode them. However, this11# would change the API. (ie: text = a "string" ? of 0 or more bytes). It12# could break compatiblity, but there's not much else you can do to fix the bug13# https://en.wikipedia.org/wiki/Shift_JIS14 15r'''16This module offers functions: concatenate_scores(), grep(),17merge_scores(), mix_scores(), midi2opus(), midi2score(), opus2midi(),18opus2score(), play_score(), score2midi(), score2opus(), score2stats(),19score_type(), segment(), timeshift() and to_millisecs(),20where "midi" means the MIDI-file bytes (as can be put in a .mid file,21or piped into aplaymidi), and "opus" and "score" are list-structures22as inspired by Sean Burke's MIDI-Perl CPAN module.23 24Warning: Version 6.4 is not necessarily backward-compatible with25previous versions, in that text-data is now bytes, not strings.26This reflects the fact that many MIDI files have text data in27encodings other that ISO-8859-1, for example in Shift-JIS.28 29Download MIDI.py from http://www.pjb.com.au/midi/free/MIDI.py30and put it in your PYTHONPATH. MIDI.py depends on Python3.31 32There is also a call-compatible translation into Lua of this33module: see http://www.pjb.com.au/comp/lua/MIDI.html34 35Backup web site: https://peterbillam.gitlab.io/miditools/36 37The "opus" is a direct translation of the midi-file-events, where38the times are delta-times, in ticks, since the previous event.39 40The "score" is more human-centric; it uses absolute times, and41combines the separate note_on and note_off events into one "note"42event, with a duration:43 ['note', start_time, duration, channel, note, velocity] # in a "score"44 45 EVENTS (in an "opus" structure)46 ['note_off', dtime, channel, note, velocity] # in an "opus"47 ['note_on', dtime, channel, note, velocity] # in an "opus"48 ['key_after_touch', dtime, channel, note, velocity]49 ['control_change', dtime, channel, controller(0-127), value(0-127)]50 ['patch_change', dtime, channel, patch]51 ['channel_after_touch', dtime, channel, velocity]52 ['pitch_wheel_change', dtime, channel, pitch_wheel]53 ['text_event', dtime, text]54 ['copyright_text_event', dtime, text]55 ['track_name', dtime, text]56 ['instrument_name', dtime, text]57 ['lyric', dtime, text]58 ['marker', dtime, text]59 ['cue_point', dtime, text]60 ['text_event_08', dtime, text]61 ['text_event_09', dtime, text]62 ['text_event_0a', dtime, text]63 ['text_event_0b', dtime, text]64 ['text_event_0c', dtime, text]65 ['text_event_0d', dtime, text]66 ['text_event_0e', dtime, text]67 ['text_event_0f', dtime, text]68 ['end_track', dtime]69 ['set_tempo', dtime, tempo]70 ['smpte_offset', dtime, hr, mn, se, fr, ff]71 ['time_signature', dtime, nn, dd, cc, bb]72 ['key_signature', dtime, sf, mi]73 ['sequencer_specific', dtime, raw]74 ['raw_meta_event', dtime, command(0-255), raw]75 ['sysex_f0', dtime, raw]76 ['sysex_f7', dtime, raw]77 ['song_position', dtime, song_pos]78 ['song_select', dtime, song_number]79 ['tune_request', dtime]80 81 DATA TYPES82 channel = a value 0 to 1583 controller = 0 to 127 (see http://www.pjb.com.au/muscript/gm.html#cc )84 dtime = time measured in "ticks", 0 to 26843545585 velocity = a value 0 (soft) to 127 (loud)86 note = a value 0 to 127 (middle-C is 60)87 patch = 0 to 127 (see http://www.pjb.com.au/muscript/gm.html )88 pitch_wheel = a value -8192 to 8191 (0x1FFF)89 raw = bytes, of length 0 or more (for sysex events see below)90 sequence_number = a value 0 to 65,535 (0xFFFF)91 song_pos = a value 0 to 16,383 (0x3FFF)92 song_number = a value 0 to 12793 tempo = microseconds per crochet (quarter-note), 0 to 1677721594 text = bytes, of length 0 or more95 ticks = the number of ticks per crochet (quarter-note)96 97 In sysex_f0 events, the raw data must not start with a \xF0 byte,98 since this gets added automatically;99 but it must end with an explicit \xF7 byte!100 In the very unlikely case that you ever need to split sysex data101 into one sysex_f0 followed by one or more sysex_f7s, then only the102 last of those sysex_f7 events must end with the explicit \xF7 byte103 (again, the raw data of individual sysex_f7 events must not start104 with any \xF7 byte, since this gets added automatically).105 106 Since version 6.4, text data is in bytes, not in a ISO-8859-1 string.107 108 109 GOING THROUGH A SCORE WITHIN A PYTHON PROGRAM110 channels = {2,3,5,8,13}111 itrack = 1 # skip 1st element which is ticks112 while itrack < len(score):113 for event in score[itrack]:114 if event[0] == 'note': # for example,115 pass # do something to all notes116 # or, to work on events in only particular channels...117 channel_index = MIDI.Event2channelindex.get(event[0], False)118 if channel_index and (event[channel_index] in channels):119 pass # do something to channels 2,3,5,8 and 13120 itrack += 1121 122'''123 124import sys, struct, copy125# sys.stdout = os.fdopen(sys.stdout.fileno(), 'wb')126Version = '6.7'127VersionDate = '20201120'128# 20201120 6.7 call to bytest() removed, and protect _unshift_ber_int129# 20160702 6.6 to_millisecs() now handles set_tempo across multiple Tracks130# 20150921 6.5 segment restores controllers as well as patch and tempo131# 20150914 6.4 text data is bytes or bytearray, not ISO-8859-1 strings132# 20150628 6.3 absent any set_tempo, default is 120bpm (see MIDI file spec 1.1)133# 20150101 6.2 all text events can be 8-bit; let user get the right encoding134# 20141231 6.1 fix _some_text_event; sequencer_specific data can be 8-bit135# 20141230 6.0 synth_specific data can be 8-bit136# 20120504 5.9 add the contents of mid_opus_tracks()137# 20120208 5.8 fix num_notes_by_channel() ; should be a dict138# 20120129 5.7 _encode handles empty tracks; score2stats num_notes_by_channel139# 20111111 5.6 fix patch 45 and 46 in Number2patch, should be Harp140# 20110129 5.5 add mix_opus_tracks() and event2alsaseq()141# 20110126 5.4 "previous message repeated N times" to save space on stderr142# 20110125 5.2 opus2score terminates unended notes at the end of the track143# 20110124 5.1 the warnings in midi2opus display track_num144# 21110122 5.0 if garbage, midi2opus returns the opus so far145# 21110119 4.9 non-ascii chars stripped out of the text_events146# 21110110 4.8 note_on with velocity=0 treated as a note-off147# 21110108 4.6 unknown F-series event correctly eats just one byte148# 21011010 4.2 segment() uses start_time, end_time named params149# 21011005 4.1 timeshift() must not pad the set_tempo command150# 21011003 4.0 pitch2note_event must be chapitch2note_event151# 21010918 3.9 set_sequence_number supported, FWIW152# 20100913 3.7 many small bugfixes; passes all tests153# 20100910 3.6 concatenate_scores enforce ticks=1000, just like merge_scores154# 20100908 3.5 minor bugs fixed in score2stats155# 20091104 3.4 tune_request now supported156# 20091104 3.3 fixed bug in decoding song_position and song_select157# 20091104 3.2 unsupported: set_sequence_number tune_request raw_data158# 20091101 3.1 document how to traverse a score within Python159# 20091021 3.0 fixed bug in score2stats detecting GM-mode = 0160# 20091020 2.9 score2stats reports GM-mode and bank msb,lsb events161# 20091019 2.8 in merge_scores, channel 9 must remain channel 9 (in GM)162# 20091018 2.7 handles empty tracks gracefully163# 20091015 2.6 grep() selects channels164# 20091010 2.5 merge_scores reassigns channels to avoid conflicts165# 20091010 2.4 fixed bug in to_millisecs which now only does opusses166# 20091010 2.3 score2stats returns channels & patch_changes, by_track & total167# 20091010 2.2 score2stats() returns also pitches and percussion dicts168# 20091010 2.1 bugs: >= not > in segment, to notice patch_change at time 0169# 20091010 2.0 bugs: spurious pop(0) ( in _decode sysex170# 20091008 1.9 bugs: ISO decoding in sysex; str( not int( in note-off warning171# 20091008 1.8 add concatenate_scores()172# 20091006 1.7 score2stats() measures nticks and ticks_per_quarter173# 20091004 1.6 first mix_scores() and merge_scores()174# 20090424 1.5 timeshift() bugfix: earliest only sees events after from_time175# 20090330 1.4 timeshift() has also a from_time argument176# 20090322 1.3 timeshift() has also a start_time argument177# 20090319 1.2 add segment() and timeshift()178# 20090301 1.1 add to_millisecs()179 180_previous_warning = '' # 5.4181_previous_times = 0 # 5.4182#------------------------------- Encoding stuff --------------------------183 184def opus2midi(opus=[]):185 r'''The argument is a list: the first item in the list is the "ticks"186parameter, the others are the tracks. Each track is a list187of midi-events, and each event is itself a list; see above.188opus2midi() returns a bytestring of the MIDI, which can then be189written either to a file opened in binary mode (mode='wb'),190or to stdout by means of: sys.stdout.buffer.write()191 192my_opus = [193 96, 194 [ # track 0:195 ['patch_change', 0, 1, 8], # and these are the events...196 ['note_on', 5, 1, 25, 96],197 ['note_off', 96, 1, 25, 0],198 ['note_on', 0, 1, 29, 96],199 ['note_off', 96, 1, 29, 0],200 ], # end of track 0201]202my_midi = opus2midi(my_opus)203sys.stdout.buffer.write(my_midi)204'''205 if len(opus) < 2:206 opus=[1000, [],]207 tracks = copy.deepcopy(opus)208 ticks = int(tracks.pop(0))209 ntracks = len(tracks)210 if ntracks == 1:211 format = 0212 else:213 format = 1214 215 my_midi = b"MThd\x00\x00\x00\x06"+struct.pack('>HHH',format,ntracks,ticks)216 for track in tracks:217 events = _encode(track)218 my_midi += b'MTrk' + struct.pack('>I',len(events)) + events219 _clean_up_warnings()220 return my_midi221 222 223def score2opus(score=None):224 r'''225The argument is a list: the first item in the list is the "ticks"226parameter, the others are the tracks. Each track is a list227of score-events, and each event is itself a list. A score-event228is similar to an opus-event (see above), except that in a score:229 1) the times are expressed as an absolute number of ticks230 from the track's start time231 2) the pairs of 'note_on' and 'note_off' events in an "opus"232 are abstracted into a single 'note' event in a "score":233 ['note', start_time, duration, channel, pitch, velocity]234score2opus() returns a list specifying the equivalent "opus".235 236my_score = [237 96,238 [ # track 0:239 ['patch_change', 0, 1, 8],240 ['note', 5, 96, 1, 25, 96],241 ['note', 101, 96, 1, 29, 96]242 ], # end of track 0243]244my_opus = score2opus(my_score)245'''246 if len(score) < 2:247 score=[1000, [],]248 tracks = copy.deepcopy(score)249 ticks = int(tracks.pop(0))250 opus_tracks = []251 for scoretrack in tracks:252 time2events = dict([])253 for scoreevent in scoretrack:254 if scoreevent[0] == 'note':255 note_on_event = ['note_on',scoreevent[1],256 scoreevent[3],scoreevent[4],scoreevent[5]]257 note_off_event = ['note_off',scoreevent[1]+scoreevent[2],258 scoreevent[3],scoreevent[4],scoreevent[5]]259 if time2events.get(note_on_event[1]):260 time2events[note_on_event[1]].append(note_on_event)261 else:262 time2events[note_on_event[1]] = [note_on_event,]263 if time2events.get(note_off_event[1]):264 time2events[note_off_event[1]].append(note_off_event)265 else:266 time2events[note_off_event[1]] = [note_off_event,]267 continue268 if time2events.get(scoreevent[1]):269 time2events[scoreevent[1]].append(scoreevent)270 else:271 time2events[scoreevent[1]] = [scoreevent,]272 273 sorted_times = [] # list of keys274 for k in time2events.keys():275 sorted_times.append(k)276 sorted_times.sort()277 278 sorted_events = [] # once-flattened list of values sorted by key279 for time in sorted_times:280 sorted_events.extend(time2events[time])281 282 abs_time = 0283 for event in sorted_events: # convert abs times => delta times284 delta_time = event[1] - abs_time285 abs_time = event[1]286 event[1] = delta_time287 opus_tracks.append(sorted_events)288 opus_tracks.insert(0,ticks)289 _clean_up_warnings()290 return opus_tracks291 292def score2midi(score=None):293 r'''294Translates a "score" into MIDI, using score2opus() then opus2midi()295'''296 return opus2midi(score2opus(score))297 298#--------------------------- Decoding stuff ------------------------299 300def midi2opus(midi=b''):301 r'''Translates MIDI into a "opus". For a description of the302"opus" format, see opus2midi()303'''304 my_midi=bytearray(midi)305 if len(my_midi) < 4:306 _clean_up_warnings()307 return [1000,[],]308 id = bytes(my_midi[0:4])309 if id != b'MThd':310 _warn("midi2opus: midi starts with "+str(id)+" instead of 'MThd'")311 _clean_up_warnings()312 return [1000,[],]313 [length, format, tracks_expected, ticks] = struct.unpack(314 '>IHHH', bytes(my_midi[4:14]))315 if length != 6:316 _warn("midi2opus: midi header length was "+str(length)+" instead of 6")317 _clean_up_warnings()318 return [1000,[],]319 my_opus = [ticks,]320 my_midi = my_midi[14:]321 track_num = 1 # 5.1322 while len(my_midi) >= 8:323 track_type = bytes(my_midi[0:4])324 if track_type != b'MTrk':325 _warn('midi2opus: Warning: track #'+str(track_num)+' type is '+str(track_type)+" instead of b'MTrk'")326 [track_length] = struct.unpack('>I', my_midi[4:8])327 my_midi = my_midi[8:]328 if track_length > len(my_midi):329 _warn('midi2opus: track #'+str(track_num)+' length '+str(track_length)+' is too large')330 _clean_up_warnings()331 return my_opus # 5.0332 my_midi_track = my_midi[0:track_length]333 my_track = _decode(my_midi_track)334 my_opus.append(my_track)335 my_midi = my_midi[track_length:]336 track_num += 1 # 5.1337 _clean_up_warnings()338 return my_opus339 340def opus2score(opus=[]):341 r'''For a description of the "opus" and "score" formats,342see opus2midi() and score2opus().343'''344 if len(opus) < 2:345 _clean_up_warnings()346 return [1000,[],]347 tracks = copy.deepcopy(opus) # couple of slices probably quicker...348 ticks = int(tracks.pop(0))349 score = [ticks,]350 for opus_track in tracks:351 ticks_so_far = 0352 score_track = []353 chapitch2note_on_events = dict([]) # 4.0354 for opus_event in opus_track:355 ticks_so_far += opus_event[1]356 if opus_event[0] == 'note_off' or (opus_event[0] == 'note_on' and opus_event[4] == 0): # 4.8357 cha = opus_event[2]358 pitch = opus_event[3]359 key = cha*128 + pitch360 if chapitch2note_on_events.get(key):361 new_event = chapitch2note_on_events[key].pop(0)362 new_event[2] = ticks_so_far - new_event[1]363 score_track.append(new_event)364 elif pitch > 127:365 pass #_warn('opus2score: note_off with no note_on, bad pitch='+str(pitch))366 else:367 pass #_warn('opus2score: note_off with no note_on cha='+str(cha)+' pitch='+str(pitch))368 elif opus_event[0] == 'note_on':369 cha = opus_event[2]370 pitch = opus_event[3]371 key = cha*128 + pitch372 new_event = ['note',ticks_so_far,0,cha,pitch, opus_event[4]]373 if chapitch2note_on_events.get(key):374 chapitch2note_on_events[key].append(new_event)375 else:376 chapitch2note_on_events[key] = [new_event,]377 else:378 opus_event[1] = ticks_so_far379 score_track.append(opus_event)380 # check for unterminated notes (Oisín) -- 5.2381 for chapitch in chapitch2note_on_events:382 note_on_events = chapitch2note_on_events[chapitch]383 for new_e in note_on_events:384 new_e[2] = ticks_so_far - new_e[1]385 score_track.append(new_e)386 pass #_warn("opus2score: note_on with no note_off cha="+str(new_e[3])+' pitch='+str(new_e[4])+'; adding note_off at end')387 score.append(score_track)388 _clean_up_warnings()389 return score390 391def midi2score(midi=b''):392 r'''393Translates MIDI into a "score", using midi2opus() then opus2score()394'''395 return opus2score(midi2opus(midi))396 397def midi2ms_score(midi=b''):398 r'''399Translates MIDI into a "score" with one beat per second and one400tick per millisecond, using midi2opus() then to_millisecs()401then opus2score()402'''403 return opus2score(to_millisecs(midi2opus(midi)))404 405#------------------------ Other Transformations ---------------------406 407def to_millisecs(old_opus=None):408 r'''Recallibrates all the times in an "opus" to use one beat409per second and one tick per millisecond. This makes it410hard to retrieve any information about beats or barlines,411but it does make it easy to mix different scores together.412'''413 if old_opus == None:414 return [1000,[],]415 try:416 old_tpq = int(old_opus[0])417 except IndexError: # 5.0418 _warn('to_millisecs: the opus '+str(type(old_opus))+' has no elements')419 return [1000,[],]420 new_opus = [1000,]421 # 6.7 first go through building a table of set_tempos by absolute-tick422 ticks2tempo = {}423 itrack = 1424 while itrack < len(old_opus):425 ticks_so_far = 0426 for old_event in old_opus[itrack]:427 if old_event[0] == 'note':428 raise TypeError('to_millisecs needs an opus, not a score')429 ticks_so_far += old_event[1]430 if old_event[0] == 'set_tempo':431 ticks2tempo[ticks_so_far] = old_event[2]432 itrack += 1433 # then get the sorted-array of their keys434 tempo_ticks = [] # list of keys435 for k in ticks2tempo.keys():436 tempo_ticks.append(k)437 tempo_ticks.sort()438 # then go through converting to millisec, testing if the next439 # set_tempo lies before the next track-event, and using it if so.440 itrack = 1441 while itrack < len(old_opus):442 ms_per_old_tick = 500.0 / old_tpq # float: will round later 6.3443 i_tempo_ticks = 0444 ticks_so_far = 0445 ms_so_far = 0.0446 previous_ms_so_far = 0.0447 new_track = [['set_tempo',0,1000000],] # new "crochet" is 1 sec448 for old_event in old_opus[itrack]:449 # detect if ticks2tempo has something before this event450 # 20160702 if ticks2tempo is at the same time, leave it451 event_delta_ticks = old_event[1]452 if (i_tempo_ticks < len(tempo_ticks) and453 tempo_ticks[i_tempo_ticks] < (ticks_so_far + old_event[1])):454 delta_ticks = tempo_ticks[i_tempo_ticks] - ticks_so_far455 ms_so_far += (ms_per_old_tick * delta_ticks)456 ticks_so_far = tempo_ticks[i_tempo_ticks]457 ms_per_old_tick = ticks2tempo[ticks_so_far] / (1000.0*old_tpq)458 i_tempo_ticks += 1459 event_delta_ticks -= delta_ticks460 new_event = copy.deepcopy(old_event) # now handle the new event461 ms_so_far += (ms_per_old_tick * old_event[1])462 new_event[1] = round(ms_so_far - previous_ms_so_far)463 if old_event[0] != 'set_tempo':464 previous_ms_so_far = ms_so_far465 new_track.append(new_event)466 ticks_so_far += event_delta_ticks467 new_opus.append(new_track)468 itrack += 1469 _clean_up_warnings()470 return new_opus471 472def event2alsaseq(event=None): # 5.5473 r'''Converts an event into the format needed by the alsaseq module,474http://pp.com.mx/python/alsaseq475The type of track (opus or score) is autodetected.476'''477 pass478 479def grep(score=None, channels=None):480 r'''Returns a "score" containing only the channels specified481'''482 if score == None:483 return [1000,[],]484 ticks = score[0]485 new_score = [ticks,]486 if channels == None:487 return new_score488 channels = set(channels)489 global Event2channelindex490 itrack = 1491 while itrack < len(score):492 new_score.append([])493 for event in score[itrack]:494 channel_index = Event2channelindex.get(event[0], False)495 if channel_index:496 if event[channel_index] in channels:497 new_score[itrack].append(event)498 else:499 new_score[itrack].append(event)500 itrack += 1501 return new_score502 503def play_score(score=None):504 r'''Converts the "score" to midi, and feeds it into 'aplaymidi -'505'''506 if score == None:507 return508 import subprocess509 pipe = subprocess.Popen(['aplaymidi','-'], stdin=subprocess.PIPE)510 if score_type(score) == 'opus':511 pipe.stdin.write(opus2midi(score))512 else:513 pipe.stdin.write(score2midi(score))514 pipe.stdin.close()515 516def 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}):517 r'''Returns a "score" shifted in time by "shift" ticks, or shifted518so that the first event starts at "start_time" ticks.519 520If "from_time" is specified, only those events in the score521that begin after it are shifted. If "start_time" is less than522"from_time" (or "shift" is negative), then the intermediate523notes are deleted, though patch-change events are preserved.524 525If "tracks" are specified, then only those tracks get shifted.526"tracks" can be a list, tuple or set; it gets converted to set527internally.528 529It is deprecated to specify both "shift" and "start_time".530If this does happen, timeshift() will print a warning to531stderr and ignore the "shift" argument.532 533If "shift" is negative and sufficiently large that it would534leave some event with a negative tick-value, then the score535is shifted so that the first event occurs at time 0. This536also occurs if "start_time" is negative, and is also the537default if neither "shift" nor "start_time" are specified.538'''539 #_warn('tracks='+str(tracks))540 if score == None or len(score) < 2:541 return [1000, [],]542 new_score = [score[0],]543 my_type = score_type(score)544 if my_type == '':545 return new_score546 if my_type == 'opus':547 _warn("timeshift: opus format is not supported\n")548 # _clean_up_scores() 6.2; doesn't exist! what was it supposed to do?549 return new_score550 if not (shift == None) and not (start_time == None):551 _warn("timeshift: shift and start_time specified: ignoring shift\n")552 shift = None553 if shift == None:554 if (start_time == None) or (start_time < 0):555 start_time = 0556 # shift = start_time - from_time557 558 i = 1 # ignore first element (ticks)559 tracks = set(tracks) # defend against tuples and lists560 earliest = 1000000000561 if not (start_time == None) or shift < 0: # first find the earliest event562 while i < len(score):563 if len(tracks) and not ((i-1) in tracks):564 i += 1565 continue566 for event in score[i]:567 if event[1] < from_time:568 continue # just inspect the to_be_shifted events569 if event[1] < earliest:570 earliest = event[1]571 i += 1572 if earliest > 999999999:573 earliest = 0574 if shift == None:575 shift = start_time - earliest576 elif (earliest + shift) < 0:577 start_time = 0578 shift = 0 - earliest579 580 i = 1 # ignore first element (ticks)581 while i < len(score):582 if len(tracks) == 0 or not ((i-1) in tracks): # 3.8583 new_score.append(score[i])584 i += 1585 continue586 new_track = []587 for event in score[i]:588 new_event = list(event)589 #if new_event[1] == 0 and shift > 0 and new_event[0] != 'note':590 # pass591 #elif new_event[1] >= from_time:592 if new_event[1] >= from_time:593 # 4.1 must not rightshift set_tempo594 if new_event[0] != 'set_tempo' or shift<0:595 new_event[1] += shift596 elif (shift < 0) and (new_event[1] >= (from_time+shift)):597 continue598 new_track.append(new_event)599 if len(new_track) > 0:600 new_score.append(new_track)601 i += 1602 _clean_up_warnings()603 return new_score604 605def segment(score=None, start_time=None, end_time=None, start=0, end=100000000,606 tracks={0,1,2,3,4,5,6,7,8,10,11,12,13,14,15}):607 r'''Returns a "score" which is a segment of the one supplied608as the argument, beginning at "start_time" ticks and ending609at "end_time" ticks (or at the end if "end_time" is not supplied).610If the set "tracks" is specified, only those tracks will611be returned.612'''613 if score == None or len(score) < 2:614 return [1000, [],]615 if start_time == None: # as of 4.2 start_time is recommended616 start_time = start # start is legacy usage617 if end_time == None: # likewise618 end_time = end619 new_score = [score[0],]620 my_type = score_type(score)621 if my_type == '':622 return new_score623 if my_type == 'opus':624 # more difficult (disconnecting note_on's from their note_off's)...625 _warn("segment: opus format is not supported\n")626 _clean_up_warnings()627 return new_score628 i = 1 # ignore first element (ticks); we count in ticks anyway629 tracks = set(tracks) # defend against tuples and lists630 while i < len(score):631 if len(tracks) and not ((i-1) in tracks):632 i += 1633 continue634 new_track = []635 channel2cc_num = {} # most recent controller change before start636 channel2cc_val = {}637 channel2cc_time = {}638 channel2patch_num = {} # keep most recent patch change before start639 channel2patch_time = {}640 set_tempo_num = 500000 # most recent tempo change before start 6.3641 set_tempo_time = 0642 earliest_note_time = end_time643 for event in score[i]:644 if event[0] == 'control_change': # 6.5645 cc_time = channel2cc_time.get(event[2]) or 0646 if (event[1] <= start_time) and (event[1] >= cc_time):647 channel2cc_num[event[2]] = event[3]648 channel2cc_val[event[2]] = event[4]649 channel2cc_time[event[2]] = event[1]650 elif event[0] == 'patch_change':651 patch_time = channel2patch_time.get(event[2]) or 0652 if (event[1]<=start_time) and (event[1] >= patch_time): # 2.0653 channel2patch_num[event[2]] = event[3]654 channel2patch_time[event[2]] = event[1]655 elif event[0] == 'set_tempo':656 if (event[1]<=start_time) and (event[1]>=set_tempo_time): #6.4657 set_tempo_num = event[2]658 set_tempo_time = event[1]659 if (event[1] >= start_time) and (event[1] <= end_time):660 new_track.append(event)661 if (event[0] == 'note') and (event[1] < earliest_note_time):662 earliest_note_time = event[1]663 if len(new_track) > 0:664 new_track.append(['set_tempo', start_time, set_tempo_num])665 for c in channel2patch_num:666 new_track.append(['patch_change',start_time,c,channel2patch_num[c]],)667 for c in channel2cc_num: # 6.5668 new_track.append(['control_change',start_time,c,channel2cc_num[c],channel2cc_val[c]])669 new_score.append(new_track)670 i += 1671 _clean_up_warnings()672 return new_score673 674def score_type(opus_or_score=None):675 r'''Returns a string, either 'opus' or 'score' or ''676'''677 if opus_or_score == None or str(type(opus_or_score)).find('list')<0 or len(opus_or_score) < 2:678 return ''679 i = 1 # ignore first element680 while i < len(opus_or_score):681 for event in opus_or_score[i]:682 if event[0] == 'note':683 return 'score'684 elif event[0] == 'note_on':685 return 'opus'686 i += 1687 return ''688 689def concatenate_scores(scores):690 r'''Concatenates a list of scores into one score.691If the scores differ in their "ticks" parameter,692they will all get converted to millisecond-tick format.693'''694 # the deepcopys are needed if the input_score's are refs to the same obj695 # e.g. if invoked by midisox's repeat()696 input_scores = _consistentise_ticks(scores) # 3.7697 output_score = copy.deepcopy(input_scores[0])698 for input_score in input_scores[1:]:699 output_stats = score2stats(output_score)700 delta_ticks = output_stats['nticks']701 itrack = 1702 while itrack < len(input_score):703 if itrack >= len(output_score): # new output track if doesn't exist704 output_score.append([])705 for event in input_score[itrack]:706 output_score[itrack].append(copy.deepcopy(event))707 output_score[itrack][-1][1] += delta_ticks708 itrack += 1709 return output_score710 711def merge_scores(scores):712 r'''Merges a list of scores into one score. A merged score comprises713all of the tracks from all of the input scores; un-merging is possible714by selecting just some of the tracks. If the scores differ in their715"ticks" parameter, they will all get converted to millisecond-tick716format. merge_scores attempts to resolve channel-conflicts,717but there are of course only 15 available channels...718'''719 input_scores = _consistentise_ticks(scores) # 3.6720 output_score = [1000]721 channels_so_far = set()722 all_channels = {0,1,2,3,4,5,6,7,8,10,11,12,13,14,15}723 global Event2channelindex724 for input_score in input_scores:725 new_channels = set(score2stats(input_score).get('channels_total', []))726 new_channels.discard(9) # 2.8 cha9 must remain cha9 (in GM)727 for channel in channels_so_far & new_channels:728 # consistently choose lowest avaiable, to ease testing729 free_channels = list(all_channels - (channels_so_far|new_channels))730 if len(free_channels) > 0:731 free_channels.sort()732 free_channel = free_channels[0]733 else:734 free_channel = None735 break736 itrack = 1737 while itrack < len(input_score):738 for input_event in input_score[itrack]:739 channel_index=Event2channelindex.get(input_event[0],False)740 if channel_index and input_event[channel_index]==channel:741 input_event[channel_index] = free_channel742 itrack += 1743 channels_so_far.add(free_channel)744 745 channels_so_far |= new_channels746 output_score.extend(input_score[1:])747 return output_score748 749def _ticks(event):750 return event[1]751def mix_opus_tracks(input_tracks): # 5.5752 r'''Mixes an array of tracks into one track. A mixed track753cannot be un-mixed. It is assumed that the tracks share the same754ticks parameter and the same tempo.755Mixing score-tracks is trivial (just insert all events into one array).756Mixing opus-tracks is only slightly harder, but it's common enough757that a dedicated function is useful.758'''759 output_score = [1000, []]760 for input_track in input_tracks: # 5.8761 input_score = opus2score([1000, input_track])762 for event in input_score[1]:763 output_score[1].append(event)764 output_score[1].sort(key=_ticks) 765 output_opus = score2opus(output_score)766 return output_opus[1]767 768def mix_scores(scores):769 r'''Mixes a list of scores into one one-track score.770A mixed score cannot be un-mixed. Hopefully the scores771have no undesirable channel-conflicts between them.772If the scores differ in their "ticks" parameter,773they will all get converted to millisecond-tick format.774'''775 input_scores = _consistentise_ticks(scores) # 3.6776 output_score = [1000, []]777 for input_score in input_scores:778 for input_track in input_score[1:]:779 output_score[1].extend(input_track)780 return output_score781 782def score2stats(opus_or_score=None):783 r'''Returns a dict of some basic stats about the score, like784bank_select (list of tuples (msb,lsb)),785channels_by_track (list of lists), channels_total (set),786general_midi_mode (list),787ntracks, nticks, patch_changes_by_track (list of dicts),788num_notes_by_channel (list of numbers),789patch_changes_total (set),790percussion (dict histogram of channel 9 events),791pitches (dict histogram of pitches on channels other than 9),792pitch_range_by_track (list, by track, of two-member-tuples),793pitch_range_sum (sum over tracks of the pitch_ranges),794'''795 bank_select_msb = -1796 bank_select_lsb = -1797 bank_select = []798 channels_by_track = []799 channels_total = set([])800 general_midi_mode = []801 num_notes_by_channel = dict([])802 patches_used_by_track = []803 patches_used_total = set([])804 patch_changes_by_track = []805 patch_changes_total = set([])806 percussion = dict([]) # histogram of channel 9 "pitches"807 pitches = dict([]) # histogram of pitch-occurrences channels 0-8,10-15808 pitch_range_sum = 0 # u pitch-ranges of each track809 pitch_range_by_track = []810 is_a_score = True811 if opus_or_score == None:812 return {'bank_select':[], 'channels_by_track':[], 'channels_total':[],813 'general_midi_mode':[], 'ntracks':0, 'nticks':0,814 'num_notes_by_channel':dict([]),815 'patch_changes_by_track':[], 'patch_changes_total':[],816 'percussion':{}, 'pitches':{}, 'pitch_range_by_track':[],817 'ticks_per_quarter':0, 'pitch_range_sum':0}818 ticks_per_quarter = opus_or_score[0]819 i = 1 # ignore first element, which is ticks820 nticks = 0821 while i < len(opus_or_score):822 highest_pitch = 0823 lowest_pitch = 128824 channels_this_track = set([])825 patch_changes_this_track = dict({})826 for event in opus_or_score[i]:827 if event[0] == 'note':828 num_notes_by_channel[event[3]] = num_notes_by_channel.get(event[3],0) + 1829 if event[3] == 9:830 percussion[event[4]] = percussion.get(event[4],0) + 1831 else:832 pitches[event[4]] = pitches.get(event[4],0) + 1833 if event[4] > highest_pitch:834 highest_pitch = event[4]835 if event[4] < lowest_pitch:836 lowest_pitch = event[4]837 channels_this_track.add(event[3])838 channels_total.add(event[3])839 finish_time = event[1] + event[2]840 if finish_time > nticks:841 nticks = finish_time842 elif event[0] == 'note_off' or (event[0] == 'note_on' and event[4] == 0): # 4.8843 finish_time = event[1]844 if finish_time > nticks:845 nticks = finish_time846 elif event[0] == 'note_on':847 is_a_score = False848 num_notes_by_channel[event[2]] = num_notes_by_channel.get(event[2],0) + 1849 if event[2] == 9:850 percussion[event[3]] = percussion.get(event[3],0) + 1851 else:852 pitches[event[3]] = pitches.get(event[3],0) + 1853 if event[3] > highest_pitch:854 highest_pitch = event[3]855 if event[3] < lowest_pitch:856 lowest_pitch = event[3]857 channels_this_track.add(event[2])858 channels_total.add(event[2])859 elif event[0] == 'patch_change':860 patch_changes_this_track[event[2]] = event[3]861 patch_changes_total.add(event[3])862 elif event[0] == 'control_change':863 if event[3] == 0: # bank select MSB864 bank_select_msb = event[4]865 elif event[3] == 32: # bank select LSB866 bank_select_lsb = event[4]867 if bank_select_msb >= 0 and bank_select_lsb >= 0:868 bank_select.append((bank_select_msb,bank_select_lsb))869 bank_select_msb = -1870 bank_select_lsb = -1871 elif event[0] == 'sysex_f0':872 if _sysex2midimode.get(event[2], -1) >= 0:873 general_midi_mode.append(_sysex2midimode.get(event[2]))874 if is_a_score:875 if event[1] > nticks:876 nticks = event[1]877 else:878 nticks += event[1]879 if lowest_pitch == 128:880 lowest_pitch = 0881 channels_by_track.append(channels_this_track)882 patch_changes_by_track.append(patch_changes_this_track)883 pitch_range_by_track.append((lowest_pitch,highest_pitch))884 pitch_range_sum += (highest_pitch-lowest_pitch)885 i += 1886 887 return {'bank_select':bank_select,888 'channels_by_track':channels_by_track,889 'channels_total':channels_total,890 'general_midi_mode':general_midi_mode,891 'ntracks':len(opus_or_score)-1,892 'nticks':nticks,893 'num_notes_by_channel':num_notes_by_channel,894 'patch_changes_by_track':patch_changes_by_track,895 'patch_changes_total':patch_changes_total,896 'percussion':percussion,897 'pitches':pitches,898 'pitch_range_by_track':pitch_range_by_track,899 'pitch_range_sum':pitch_range_sum,900 'ticks_per_quarter':ticks_per_quarter}901 902#----------------------------- Event stuff --------------------------903 904_sysex2midimode = {905 "\x7E\x7F\x09\x01\xF7": 1,906 "\x7E\x7F\x09\x02\xF7": 0,907 "\x7E\x7F\x09\x03\xF7": 2,908}909 910# Some public-access tuples:911MIDI_events = tuple('''note_off note_on key_after_touch912control_change patch_change channel_after_touch913pitch_wheel_change'''.split())914 915Text_events = tuple('''text_event copyright_text_event916track_name instrument_name lyric marker cue_point text_event_08917text_event_09 text_event_0a text_event_0b text_event_0c918text_event_0d text_event_0e text_event_0f'''.split())919 920Nontext_meta_events = tuple('''end_track set_tempo921smpte_offset time_signature key_signature sequencer_specific922raw_meta_event sysex_f0 sysex_f7 song_position song_select923tune_request'''.split())924# unsupported: raw_data925 926# Actually, 'tune_request' is is F-series event, not strictly a meta-event...927Meta_events = Text_events + Nontext_meta_events928All_events = MIDI_events + Meta_events929 930# And three dictionaries:931Number2patch = { # General MIDI patch numbers:9320:'Acoustic Grand',9331:'Bright Acoustic',9342:'Electric Grand',9353:'Honky-Tonk',9364:'Electric Piano 1',9375:'Electric Piano 2',9386:'Harpsichord',9397:'Clav',9408:'Celesta',9419:'Glockenspiel',94210:'Music Box',94311:'Vibraphone',94412:'Marimba',94513:'Xylophone',94614:'Tubular Bells',94715:'Dulcimer',94816:'Drawbar Organ',94917:'Percussive Organ',95018:'Rock Organ',95119:'Church Organ',95220:'Reed Organ',95321:'Accordion',95422:'Harmonica',95523:'Tango Accordion',95624:'Acoustic Guitar(nylon)',95725:'Acoustic Guitar(steel)',95826:'Electric Guitar(jazz)',95927:'Electric Guitar(clean)',96028:'Electric Guitar(muted)',96129:'Overdriven Guitar',96230:'Distortion Guitar',96331:'Guitar Harmonics',96432:'Acoustic Bass',96533:'Electric Bass(finger)',96634:'Electric Bass(pick)',96735:'Fretless Bass',96836:'Slap Bass 1',96937:'Slap Bass 2',97038:'Synth Bass 1',97139:'Synth Bass 2',97240:'Violin',97341:'Viola',97442:'Cello',97543:'Contrabass',97644:'Tremolo Strings',97745:'Pizzicato Strings',97846:'Orchestral Harp',97947:'Timpani',98048:'String Ensemble 1',98149:'String Ensemble 2',98250:'SynthStrings 1',98351:'SynthStrings 2',98452:'Choir Aahs',98553:'Voice Oohs',98654:'Synth Voice',98755:'Orchestra Hit',98856:'Trumpet',98957:'Trombone',99058:'Tuba',99159:'Muted Trumpet',99260:'French Horn',99361:'Brass Section',99462:'SynthBrass 1',99563:'SynthBrass 2',99664:'Soprano Sax',99765:'Alto Sax',99866:'Tenor Sax',99967:'Baritone Sax',100068:'Oboe',100169:'English Horn',100270:'Bassoon',100371:'Clarinet',100472:'Piccolo',100573:'Flute',100674:'Recorder',100775:'Pan Flute',100876:'Blown Bottle',100977:'Skakuhachi',101078:'Whistle',101179:'Ocarina',101280:'Lead 1 (square)',101381:'Lead 2 (sawtooth)',101482:'Lead 3 (calliope)',101583:'Lead 4 (chiff)',101684:'Lead 5 (charang)',101785:'Lead 6 (voice)',101886:'Lead 7 (fifths)',101987:'Lead 8 (bass+lead)',102088:'Pad 1 (new age)',102189:'Pad 2 (warm)',102290:'Pad 3 (polysynth)',102391:'Pad 4 (choir)',102492:'Pad 5 (bowed)',102593:'Pad 6 (metallic)',102694:'Pad 7 (halo)',102795:'Pad 8 (sweep)',102896:'FX 1 (rain)',102997:'FX 2 (soundtrack)',103098:'FX 3 (crystal)',103199:'FX 4 (atmosphere)',1032100:'FX 5 (brightness)',1033101:'FX 6 (goblins)',1034102:'FX 7 (echoes)',1035103:'FX 8 (sci-fi)',1036104:'Sitar',1037105:'Banjo',1038106:'Shamisen',1039107:'Koto',1040108:'Kalimba',1041109:'Bagpipe',1042110:'Fiddle',1043111:'Shanai',1044112:'Tinkle Bell',1045113:'Agogo',1046114:'Steel Drums',1047115:'Woodblock',1048116:'Taiko Drum',1049117:'Melodic Tom',1050118:'Synth Drum',1051119:'Reverse Cymbal',1052120:'Guitar Fret Noise',1053121:'Breath Noise',1054122:'Seashore',1055123:'Bird Tweet',1056124:'Telephone Ring',1057125:'Helicopter',1058126:'Applause',1059127:'Gunshot',1060}1061Notenum2percussion = { # General MIDI Percussion (on Channel 9):106235:'Acoustic Bass Drum',106336:'Bass Drum 1',106437:'Side Stick',106538:'Acoustic Snare',106639:'Hand Clap',106740:'Electric Snare',106841:'Low Floor Tom',106942:'Closed Hi-Hat',107043:'High Floor Tom',107144:'Pedal Hi-Hat',107245:'Low Tom',107346:'Open Hi-Hat',107447:'Low-Mid Tom',107548:'Hi-Mid Tom',107649:'Crash Cymbal 1',107750:'High Tom',107851:'Ride Cymbal 1',107952:'Chinese Cymbal',108053:'Ride Bell',108154:'Tambourine',108255:'Splash Cymbal',108356:'Cowbell',108457:'Crash Cymbal 2',108558:'Vibraslap',108659:'Ride Cymbal 2',108760:'Hi Bongo',108861:'Low Bongo',108962:'Mute Hi Conga',109063:'Open Hi Conga',109164:'Low Conga',109265:'High Timbale',109366:'Low Timbale',109467:'High Agogo',109568:'Low Agogo',109669:'Cabasa',109770:'Maracas',109871:'Short Whistle',109972:'Long Whistle',110073:'Short Guiro',110174:'Long Guiro',110275:'Claves',110376:'Hi Wood Block',110477:'Low Wood Block',110578:'Mute Cuica',110679:'Open Cuica',110780:'Mute Triangle',110881:'Open Triangle',1109}1110 1111Event2channelindex = { 'note':3, 'note_off':2, 'note_on':2,1112 'key_after_touch':2, 'control_change':2, 'patch_change':2,1113 'channel_after_touch':2, 'pitch_wheel_change':21114}1115 1116################################################################1117# The code below this line is full of frightening things, all to1118# do with the actual encoding and decoding of binary MIDI data.1119 1120def _twobytes2int(byte_a):1121 r'''decode a 16 bit quantity from two bytes,'''1122 return (byte_a[1] | (byte_a[0] << 8))1123 1124def _int2twobytes(int_16bit):1125 r'''encode a 16 bit quantity into two bytes,'''1126 return bytes([(int_16bit>>8) & 0xFF, int_16bit & 0xFF])1127 1128def _read_14_bit(byte_a):1129 r'''decode a 14 bit quantity from two bytes,'''1130 return (byte_a[0] | (byte_a[1] << 7))1131 1132def _write_14_bit(int_14bit):1133 r'''encode a 14 bit quantity into two bytes,'''1134 return bytes([int_14bit & 0x7F, (int_14bit>>7) & 0x7F])1135 1136def _ber_compressed_int(integer):1137 r'''BER compressed integer (not an ASN.1 BER, see perlpacktut for1138details). Its bytes represent an unsigned integer in base 128,1139most significant digit first, with as few digits as possible.1140Bit eight (the high bit) is set on each byte except the last.1141'''1142 ber = bytearray(b'')1143 seven_bits = 0x7F & integer1144 ber.insert(0, seven_bits) # XXX surely should convert to a char ?1145 integer >>= 71146 while integer > 0:1147 seven_bits = 0x7F & integer1148 ber.insert(0, 0x80|seven_bits) # XXX surely should convert to a char ?1149 integer >>= 71150 return ber1151 1152def _unshift_ber_int(ba):1153 r'''Given a bytearray, returns a tuple of (the ber-integer at the1154start, and the remainder of the bytearray).1155'''1156 if not len(ba): # 6.71157 _warn('_unshift_ber_int: no integer found')1158 return ((0, b""))1159 byte = ba.pop(0)1160 integer = 01161 while True:1162 integer += (byte & 0x7F)1163 if not (byte & 0x80):1164 return ((integer, ba))1165 if not len(ba):1166 _warn('_unshift_ber_int: no end-of-integer found')1167 return ((0, ba))1168 byte = ba.pop(0)1169 integer <<= 71170 1171def _clean_up_warnings(): # 5.41172 # Call this before returning from any publicly callable function1173 # whenever there's a possibility that a warning might have been printed1174 # by the function, or by any private functions it might have called.1175 global _previous_times1176 global _previous_warning1177 if _previous_times > 1:1178 # E:1176, 0: invalid syntax (<string>, line 1176) (syntax-error) ???1179 # print(' previous message repeated '+str(_previous_times)+' times', file=sys.stderr)1180 # 6.71181 sys.stderr.write(' previous message repeated {0} times\n'.format(_previous_times))1182 elif _previous_times > 0:1183 sys.stderr.write(' previous message repeated\n')1184 _previous_times = 01185 _previous_warning = ''1186 1187def _warn(s=''):1188 global _previous_times1189 global _previous_warning1190 if s == _previous_warning: # 5.41191 _previous_times = _previous_times + 11192 else:1193 _clean_up_warnings()1194 sys.stderr.write(str(s)+"\n")1195 _previous_warning = s1196 1197def _some_text_event(which_kind=0x01, text=b'some_text'):1198 if str(type(text)).find("'str'") >= 0: # 6.4 test for back-compatibility1199 data = bytes(text, encoding='ISO-8859-1')1200 else: