def addProgChange(self, offset, program, oldprg): """ Create a midi program change (handles extended voicing). program - The MIDI program (voice) value oldprg - existing MIDI program """ # We truck around a special pseudo voice 'NONE' or 127.127.127 which # is a signal that we don't want mma to set the voicing. Might be # useful when know the track that mma is using and we have preset # an external synth. if program == NONETONE: return v1, lsb1, msb1 = MMA.midiC.voice2tup(oldprg) v2, lsb2, msb2 = MMA.midiC.voice2tup(program) if msb1 != msb2: # only if CTRL32 has changed self.addToTrack(offset, packBytes((0xb0 | self.channel, 0x20, msb2))) if lsb1 != lsb2: # only if CTRL0 has changed self.addToTrack(offset, packBytes((0xb0 | self.channel, 0x00, lsb2))) # Always do voice change. Maybe not necessary, but let's be safe. self.addToTrack(offset, packBytes((0xc0 | self.channel, v2)), MIDI_PRG)
def addNoteOnToTrack(self, boffset, note, v, startRnd=None, endRnd=None): """ Add a single note on or note off when v=0 to a track. boffset - offset into current bar duration - note len note - midi value of note v - midi velocity, set to 0 for note off startRnd/endRnd - rand val start adjustment Added by louisjb for plectrum. """ # Start offsets onOffset = getOffset(boffset, startRnd, endRnd) onEvent = packBytes((0x90 | self.channel, note, v)) # this avoids situations where the new note is placed # before a NoteOff event. This could be due to a bad # sequence specification or, more likely, due to # RTIME setting the start point of this note before # the current beat in a tight sequence. f = self.lastOffEvent[note] if f is not None and f >= onOffset: offEvent = packBytes(onEvent[:-1], 0) if offEvent in self.miditrk[f]: self.miditrk[f].remove(offEvent) # ON/OFF events (off is on with v = 0) self.addToTrack(onOffset, onEvent, MIDI_NOTE) if v==0: self.lastOffEvent[note] = onOffset return onOffset
def addTrkName(self, offset, msg): """ Creates a midi track name event. """ offset = 0 # ignore user offset, always put this at 0 self.trackname = msg cmd = packBytes((0xff, 0x03)) self.delDup(offset, cmd) self.addToTrack(offset, packBytes(cmd, intToVarNumber(len(msg)), msg))
def addTempo(self, offset, beats): """ Create a midi tempo meta event. beats - beats per second Return - packed midi string """ cmd = packBytes((0xff, 0x51, 0x03)) self.delDup(offset, cmd) self.addToTrack(offset, packBytes(cmd, intTo3Byte(60000000 // beats)))
def addTempo(self, offset, bpm): """ Create a midi tempo meta event. bpm - beats per minute Return - packed midi string """ cmd = packBytes((0xff, 0x51, 0x03)) self.delDup(offset, cmd) self.addToTrack(offset, packBytes(cmd, intTo3Byte(60000000 // bpm))) tempoChanges.append([offset, bpm])
def addGlis(self, offset, v): """ Set the portamento. LowLevel MIDI. This does 2 things: 1. turns portamento on/off, 2. sets the LSN rate. """ if v == 0: self.addToTrack(offset, packBytes((0xb0 | self.channel, 0x41, 0x00))) else: self.addToTrack(offset, packBytes((0xb0 | self.channel, 0x41, 0x7f))) self.addToTrack(offset, packBytes((0xb0 | self.channel, 0x05, v)))
def addChannelVol(self, offset, v): """ Set the midi channel volume.""" tr = self.miditrk cvol = packBytes(0xb0 | self.channel, 0x07) # 2 byte channel vol # Before adding a new channel volume event we check to see if there # are any future channel volume events and delete them. for off in tr: if off >= offset: tr[off] = [e for e in tr[off] if e[0:2] != cvol] self.addToTrack(offset, packBytes(cvol, v))
def addCopyright(self, offset, msg): """ Insert copyright. """ # should never happen since the caller sets offset=0 if offset != 0: error("Copyright message must be at offset 0, not %s." % offset) # We need to bypass addToTrack to force copyright to the start of the track. # Create the copyright event ev = packBytes((0xff, 0x02), intToVarNumber(len(msg)), msg) tr = self.miditrk # this is the meta track # We keep a pointer (ipoint) which points to the position of # the last copyright string. If there isn't one, we create # it in the expect; else it's just incremented try: self.ipoint += 1 except AttributeError: self.ipoint = 0 if offset in tr: tr[offset].insert(self.ipoint, ev) else: tr[offset] = [ev]
def addNoteOff(self, offset): """ Insert a "All Note Off" into the midi stream. Called from the cutTrack() function. """ self.addToTrack(offset, packBytes((0xb0 | self.channel, 0x7b, 0)), MIDI_NOTE )
def insertControl(trk, ln): """ Insert a controller event. """ if len(ln) != 3: error("MidiNote: Controller expecting 3 arguments.") offset = getoffset(trk, ln[0]) v = MMA.midiC.ctrlToValue(ln[1]) if v < 0: v = stoi(ln[1]) if v < 0 or v > 0x7F: error("MidiNote: Controller values must be 0x00 to 0x7f, not '%s'." % ln[1]) d = stoi(ln[2]) if d < 0 or d > 0x7F: error("MidiNote: Control Datum value must be 0x00 to 0x7f, not '%s'." % ln[2]) channel = trk.channel track = gbl.mtrks[channel] # bypass the addctl() defined in midi.py just to keep all the calls in this # module similar. We should have add**() command in midi.py for the above stuff # and redo this.??? track.addToTrack(gbl.tickOffset + offset, packBytes((0xB0 | channel - 1, v, d))) if gbl.debug: print("MidiNote Ctrl %s: inserted Controller %s value %s at offset %s." % (trk.name, v, d, offset))
def insertControl(trk, ln): """ Insert a controller event. """ if len(ln) != 3: error("MidiNote: Controller expecting 3 arguments.") offset = getoffset(trk, ln[0]) v = MMA.midiC.ctrlToValue(ln[1]) if v < 0: v = stoi(ln[1]) if v < 0 or v > 0x7f: error( "MidiNote: Controller values must be 0x00 to 0x7f, not '%s'." % ln[1]) d = stoi(ln[2]) if d < 0 or d > 0x7f: error("MidiNote: Control Datum value must be 0x00 to 0x7f, not '%s'." % ln[2]) channel = trk.channel track = gbl.mtrks[channel] # bypass the addctl() defined in midi.py just to keep all the calls in this # module similar. We should have add**() command in midi.py for the above stuff # and redo this.??? track.addToTrack(gbl.tickOffset + offset, packBytes((0xb0 | channel - 1, v, d))) if gbl.debug: print( "MidiNote Ctrl %s: inserted Controller %s value %s at offset %s." % (trk.name, v, d, offset))
def insertPBrange(trk, ln): """ Insert a range of PB events. """ if len(ln) != 3: error( "MidiNote: PBR expecting 3 arguments <count> <start,end> <v1,v2>.") count = stoi(ln[0]) try: s1, s2 = ln[1].split(',') except: error("MidiNote PBR: event range must be 'v1,v2', not '%s'." % ln[1]) s1 = getoffset(trk, s1) s2 = getoffset(trk, s2) tinc = (s2 - s1) / float(count) try: v1, v2 = ln[2].split(',') except: error("MidiNote PBR: pitch blend range must be 'v1,v2', not '%s'." % ln[2]) v1 = stoi(v1) v2 = stoi(v2) if v1 < -8192 or v1 > 8191 or v2 < -8192 or v2 > 8191: error("MidiNote: PBR values must be -8192..+8191, not '%s'." % ln[2]) v1 += 8192 # convert to 0..16383, max 14 bit value v2 += 8192 vinc = (v2 - v1) / float(count) channel = trk.channel track = gbl.mtrks[channel] ev = packBytes(0xe0 | channel - 1) offset = s1 bend = v1 for i in range(count + 1): v = int(bend) track.addToTrack(gbl.tickOffset + int(offset), packBytes(ev, v % 128, v // 128)) offset += tinc bend += vinc if gbl.debug: print("MidiNote PBR %s: inserted bends %s to %s at offsets %s to %s." % (trk.name, v1 - 8192, v2 - 8192, s1, s2))
def insertChTouchRange(trk, ln): """ Insert a range of channel aftertouch events. """ if len(ln) != 3: error( "MidiNote: ChATR expecting 3 arguments <count> <start,end> <v1,v2>." ) count = stoi(ln[0]) try: s1, s2 = ln[1].split(',') except: error("MidiNote ChATR: event range must be 'v1,v2', not '%s'." % ln[1]) s1 = getoffset(trk, s1) s2 = getoffset(trk, s2) tinc = (s2 - s1) / float(count) try: v1, v2 = ln[2].split(',') except: error("MidiNote ChATR: range must be 'v1,v2', not '%s'." % ln[2]) v1 = stoi(v1) v2 = stoi(v2) if v1 < 0 or v1 > 127 or v2 < 0 or v2 > 127: error("MidiNote: ChATR values must be 0.. 127, not '%s'." % ln[2]) vinc = (v2 - v1) / float(count) channel = trk.channel track = gbl.mtrks[channel] ev = packBytes(0xd0 | channel - 1) offset = s1 bend = v1 for i in range(count + 1): v = int(bend) track.addToTrack(gbl.tickOffset + int(offset), packBytes(ev, v)) offset += tinc bend += vinc if gbl.debug: print( "MidiNote ChATR %s: inserted events %s to %s at offsets %s to %s." % (trk.name, v1, v2, s1, s2))
def insertPBrange(trk, ln): """ Insert a range of PB events. """ if len(ln) != 3: error("MidiNote: PBR expecting 3 arguments <count> <start,end> <v1,v2>.") count = stoi(ln[0]) try: s1, s2 = ln[1].split(",") except: error("MidiNote PBR: event range must be 'v1,v2', not '%s'." % ln[1]) s1 = getoffset(trk, s1) s2 = getoffset(trk, s2) tinc = (s2 - s1) / float(count) try: v1, v2 = ln[2].split(",") except: error("MidiNote PBR: pitch blend range must be 'v1,v2', not '%s'." % ln[2]) v1 = stoi(v1) v2 = stoi(v2) if v1 < -8192 or v1 > 8191 or v2 < -8192 or v2 > 8191: error("MidiNote: PBR values must be -8192..+8191, not '%s'." % ln[2]) v1 += 8192 # convert to 0..16383, max 14 bit value v2 += 8192 vinc = (v2 - v1) / float(count) channel = trk.channel track = gbl.mtrks[channel] ev = packBytes(0xE0 | channel - 1) offset = s1 bend = v1 for i in range(count + 1): v = int(bend) track.addToTrack(gbl.tickOffset + int(offset), packBytes(ev, v % 128, v // 128)) offset += tinc bend += vinc if gbl.debug: print( "MidiNote PBR %s: inserted bends %s to %s at offsets %s to %s." % (trk.name, v1 - 8192, v2 - 8192, s1, s2) )
def addTimeSig(self, offset, nn, dd, cc, bb): """ Create a midi time signature. delta - midi delta offset nn = sig numerator, beats per measure dd - sig denominator, 2=quarter note, 3=eighth, cc - midi clocks/tick bb - # of 32nd notes in quarter (normally 8) This is only called by timeSig.create(). Don't call this directly since the timeSig.create() checks for duplicate settings. """ cmd = packBytes(0xff, 0x58) # we might have several different timesigs on the same offset, # so take time to delete any. self.delDup(offset, cmd) self.addToTrack(offset, packBytes(cmd, (0x04, nn, dd, cc, bb)))
def addTimeSig(self, offset, nn, dd, cc, bb): """ Create a midi time signature. delta - midi delta offset nn = sig numerator, beats per measure dd - sig denominator, 2=quarter note, 3=eighth, cc - midi clocks/tick bb - # of 32nd notes in quarter (normally 8) This is only called by timeSig.set(). Don't call this directly since the timeSig.set() checks for duplicate settings. """ cmd = packBytes(0xff, 0x58) # we might have several different timesigs on the same offset, # so take time to delete any. self.delDup(offset, cmd) self.addToTrack(offset, packBytes(cmd, (0x04, nn, dd, cc, bb)))
def insertChTouchRange(trk, ln): """ Insert a range of channel aftertouch events. """ if len(ln) != 3: error("MidiNote: ChATR expecting 3 arguments <count> <start,end> <v1,v2>.") count = stoi(ln[0]) try: s1, s2 = ln[1].split(",") except: error("MidiNote ChATR: event range must be 'v1,v2', not '%s'." % ln[1]) s1 = getoffset(trk, s1) s2 = getoffset(trk, s2) tinc = (s2 - s1) / float(count) try: v1, v2 = ln[2].split(",") except: error("MidiNote ChATR: range must be 'v1,v2', not '%s'." % ln[2]) v1 = stoi(v1) v2 = stoi(v2) if v1 < 0 or v1 > 127 or v2 < 0 or v2 > 127: error("MidiNote: ChATR values must be 0.. 127, not '%s'." % ln[2]) vinc = (v2 - v1) / float(count) channel = trk.channel track = gbl.mtrks[channel] ev = packBytes(0xD0 | channel - 1) offset = s1 bend = v1 for i in range(count + 1): v = int(bend) track.addToTrack(gbl.tickOffset + int(offset), packBytes(ev, v)) offset += tinc bend += vinc if gbl.debug: print("MidiNote ChATR %s: inserted events %s to %s at offsets %s to %s." % (trk.name, v1, v2, s1, s2))
def addMasterVolume(self, offset, v): """ System Exclusive master volume message. Meta track. """ # We send to packBytes as a long list, no tuples. Just a bit # easier to maintain a long list like this. self.addToTrack(offset, packBytes( 0xf0, # Start sysex 0x07, # message size (needed for SMF) 0x7f, # realtime 0x7f, # disregard channel 0x04, # device control 0x01, # master volume intTo14(v), # params (14 bit) 0xf7 )) # EOX
def insertPB(trk, ln): """ Insert a pitch controller event. """ if len(ln) != 2: error("MidiNote: PB expecting 2 arguments.") offset = getoffset(trk, ln[0]) v = stoi(ln[1]) if v < -8192 or v > 8191: error("MidiNote: PB value must be -8192..+8191, not '%s'." % v) v += 8192 # convert to 0..16383, max 14 bit value channel = trk.channel track = gbl.mtrks[channel] track.addToTrack(gbl.tickOffset + offset, packBytes((0xe0 | channel-1, v % 128, v // 128))) if MMA.debug.debug: dPrint("MidiNote PB %s: inserted bend %s at offset %s." % (trk.name, v-8192, offset))
def insertChTouch(trk, ln): """ Insert a channel aftertouch) event. """ if len(ln) != 2: error("MidiNote: ChAT expecting 2 arguments.") offset = getoffset(trk, ln[0]) v = stoi(ln[1]) if v < 0 or v > 127: error("MidiNote: ChAT value must be 0 .. 127, not '%s'." % v) channel = trk.channel track = gbl.mtrks[channel] track.addToTrack(gbl.tickOffset + offset, packBytes((0xD0 | channel - 1, v))) if gbl.debug: print("MidiNote ChAT %s: inserted channel aftertouch %s at offset %s." % (trk.name, v, offset))
def insertPB(trk, ln): """ Insert a pitch controller event. """ if len(ln) != 2: error("MidiNote: PB expecting 2 arguments.") offset = getoffset(trk, ln[0]) v = stoi(ln[1]) if v < -8192 or v > 8191: error("MidiNote: PB value must be -8192..+8191, not '%s'." % v) v += 8192 # convert to 0..16383, max 14 bit value channel = trk.channel track = gbl.mtrks[channel] track.addToTrack(gbl.tickOffset + offset, packBytes((0xE0 | channel - 1, v % 128, v // 128))) if gbl.debug: print("MidiNote PB %s: inserted bend %s at offset %s." % (trk.name, v - 8192, offset))
def addNoteOnToTrack(self, boffset, note, v, startRnd=None, endRnd=None): """ Add a single note on or note off when v=0 to a track. boffset - offset into current bar duration - note len note - midi value of note v - midi velocity, set to 0 for note off startRnd/endRnd - rand val start adjustment Added by louisjb for plectrum. """ # Start offsets onOffset = getOffset(boffset, startRnd, endRnd) # ON/OFF events (off is on with v = 0) onEvent = packBytes((0x90 | self.channel, note, v)) self.addToTrack(onOffset, onEvent, MIDI_NOTE) return onOffset
def insertChTouch(trk, ln): """ Insert a channel aftertouch) event. """ if len(ln) != 2: error("MidiNote: ChAT expecting 2 arguments.") offset = getoffset(trk, ln[0]) v = stoi(ln[1]) if v < 0 or v > 127: error("MidiNote: ChAT value must be 0 .. 127, not '%s'." % v) channel = trk.channel track = gbl.mtrks[channel] track.addToTrack(gbl.tickOffset + offset, packBytes((0xd0 | channel-1, v))) if MMA.debug.debug: dPrint("MidiNote ChAT %s: inserted channel aftertouch %s at offset %s." % (trk.name, v, offset))
def writeMidiTrack(self, out): """ Create/write the MIDI track. We convert timing offsets to midi-deltas. """ tr = self.miditrk """ If the -1 command line option is set we need to add a terminate to the end of each track. This is done to make looping software like seq24 happy. We do this by truncating all data in the file past the current tick pointer and inserting an all-notes-off at that position. """ if MMA.sync.endsync and self.channel >= 0: eof = gbl.tickOffset for offset in tr.keys(): if offset > eof: del tr[offset] self.addToTrack(eof, packBytes((0xb0 | self.channel, 0x7b, 0))) """ To every MIDI track we generate we add (if the -0 flag was set) an on/off beep at offset 0. This makes for easier sync in multi-tracks. """ if MMA.sync.synctick and self.channel >= 0: t, v = MMA.sync.syncTone self.addToTrack(0, packBytes((0x90 | self.channel, t, v))) self.addToTrack(1, packBytes((0x90 | self.channel, t, 0))) if MMA.debug.debug: ttl = 0 lg = 1 for t in tr: a = len(tr[t]) if a > lg: lg = a ttl += a if self.channel == -1: nm = "META" else: nm = self.trackname dPrint( "<%s> Unique ts: %s; Ttl events %s; Average ev/ts %.2f" % (nm, len(tr), ttl, float(ttl)/len(tr))) last = 0 # Convert all events to MIDI deltas and store in # the track array/list tdata = [] # empty track container lastSts = None # Running status tracker for a in sorted(tr.keys()): delta = a-last if not tr[a]: continue # this skips the delta offset update! for d in tr[a]: """ Running status check. For each packet compare the first byte with the first byte of the previous packet. If it is can be converted to running status we strip out byte 0. Note that valid running status byte are 0x80..0xef. 0xfx are system messages and are not suitable for running status. """ if len(d) > 1: if d[0] == lastSts: d = d[1:] else: lastSts = d[0] if lastSts < 0x80 or lastSts > 0xef or not gbl.runningStatus: lastSts = None tdata.extend([intToVarNumber(delta), d]) delta = 0 last = a # Add an EOF to the track (included in total track size) tdata.append(intToVarNumber(0)) tdata.append(packBytes((0xff, 0x2f, 0x00))) tdata = bytearray(b'').join(tdata) totsize = len(tdata) out.write(b"MTrk") out.write(intToLong(totsize)) out.write(tdata)
def mkHeader(count, tempo, Mtype): return packBytes("MThd", intToLong(6), intToWord(Mtype), intToWord(count), intToWord(tempo))
def addLyric(self, offset, msg): """ Create a midi lyric event. """ self.addToTrack(offset, packBytes((0xff, 0x05), intToVarNumber(len(msg)), msg))
def fermata(ln): """ Apply a fermata timing to the specified beat. """ if len(ln) != 3: error("Fermata: use 'offset' 'duration' 'adjustment'") offset = stof(ln[0], "Fermata: Expecting a value (not '%s') for offset" % ln[0]) if offset < -gbl.QperBar or offset > gbl.QperBar: warning("Fermata: %s is a large beat offset" % offset) dur = stof(ln[1], "Fermata: Expecting a value (not '%s') for duration" % ln[1]) if dur <= 0: error("Fermata: duration must be greater than 0") if dur > gbl.QperBar: warning("Fermata: %s is a large duration." % dur) adj = stof(ln[2], "Fermata: expecting a value (not '%s') for adjustment." % ln[2]) if adj < 100: warning("Fermata: Adjustment less than 100 is shortening beat value.") if adj == 100: error( "Fermata: using value of 100 makes no difference, must be an error." ) moff = int(gbl.tickOffset + (gbl.BperQ * offset)) if moff < 0: error("Fermata: Offset comes before track start.") if offset >= 0: warning( "Fermata: Better results when placed after event (negative offset)." ) fermataDuration = int(gbl.BperQ * dur) # Duration in ticks mend = moff + fermataDuration # This next section is needed to figure out the start tempo (which # is not always gbl.tempo!), the needed tempo at the end of the fermata # section (again, not always gbl.tempo!) and to delete any tempo changes # in the section. All this due to the effects of tempo changes over a # a set of bars. # Extract ALL tempo changes from the meta track and save in a list. # Also, delete any tempo changes found in the miditrack in our range. tempos = [] tcmd = packBytes((0xff, 0x51, 0x03)) # midi TEMPO mt = gbl.mtrks[0].miditrk # The meta track # 1st we copy existing tempos into a new list for t in mt: for ev in mt[t]: if ev[0:3] == tcmd: tempos.append((t, ev[3:])) # save the 24 bit value # now we delete any tempos in the fermata range. This avoids broken loops for t, ev in tempos: if t >= moff and t <= mend: gbl.mtrks[0].delDup(t, tcmd) # find last tempo before fermata bock and last tempo in block tempos.sort() oldTempo = gbl.tempo newTempo = gbl.tempo for f, t in tempos: # f==offsets, t==encoded tempos if f <= moff: oldTempo = 60000000 // byte3ToInt(t) if f >= moff and f <= mend: newTempo = 60000000 // byte3ToInt(t) gbl.mtrks[0].addTempo(moff, int(oldTempo / (adj / 100))) gbl.mtrks[0].addTempo(mend, newTempo) # Move selected events in the effected area to it's start/end. # To start -> note on, program change # to end -> note off # Done only if the fermata comes after desired location ... # otherwise there aren't any events to zap! if offset < 0: for n, tr in gbl.mtrks.items(): # do each track if n <= 0: continue # skip meta track trk = gbl.mtrks[n].miditrk startEvents = [] endEvents = [] for f in sorted(trk): # all in this track (sort keeps orig order) if f > moff and f < mend: # well, only in the fermata range remain = [] for ev in trk[f]: # all events in the offset if not ev: # skip empty events continue # Get event type. The [0:1] is needed to # maintain this as a byte() for python3, otherwise # py3 will think it's an int() and barfs evtype = unpack('B', ev[0:1])[0] >> 4 if evtype == 0x9: # note event if ev[2] == 0: # off to end endEvents.append(ev) else: startEvents.append(ev) # on to start continue if evtype == 0xb or evtype == 0xc: # program/controller change startEvents.append(ev) # all to start (??) continue remain.append(ev) trk[f] = remain # remaining events for this offset if startEvents: if moff in trk: trk[moff].extend(startEvents) else: trk[moff] = startEvents if endEvents: if mend in trk: trk[mend] = endEvents + trk[mend] else: trk[mend] = endEvents if gbl.debug: print("Fermata: Beat %s, Duration %s, Change %s, Bar %s" % (offset, dur, adj, gbl.barNum + 1)) if offset < 0: print(" NoteOn Events moved in tick range %s to %s" % (moff + 1, mend - 1))
def fermata(ln): """ Apply a fermata timing to the specified beat. """ if len(ln) != 3: error("Fermata: use 'offset' 'duration' 'adjustment'") offset = stof(ln[0], "Fermata: Expecting a value (not '%s') for offset" % ln[0]) if offset < -gbl.QperBar or offset > gbl.QperBar: warning("Fermata: %s is a large beat offset" % offset) dur = stof(ln[1], "Fermata: Expecting a value (not '%s') for duration" % ln[1]) if dur <= 0: error("Fermata: duration must be greater than 0") if dur > gbl.QperBar: warning("Fermata: %s is a large duration." % dur) adj = stof(ln[2], "Fermata: expecting a value (not '%s') for adjustment." % ln[2]) if adj < 100: warning("Fermata: Adjustment less than 100 is shortening beat value.") if adj == 100: error("Fermata: using value of 100 makes no difference, must be an error.") moff = int(gbl.tickOffset + (gbl.BperQ * offset)) if moff < 0: error("Fermata: Offset comes before track start.") if offset >= 0: warning("Fermata: Better results when placed after event (negative offset).") fermataDuration = int(gbl.BperQ * dur) # Duration in ticks mend = moff + fermataDuration # This next section is needed to figure out the start tempo (which # is not always gbl.tempo!), the needed tempo at the end of the fermata # section (again, not always gbl.tempo!) and to delete any tempo changes # in the section. All this due to the effects of tempo changes over a # a set of bars. # Extract ALL tempo changes from the meta track and save in a list. # Also, delete any tempo changes found in the miditrack in our range. tempos = [] tcmd = packBytes((0xff, 0x51, 0x03)) # midi TEMPO mt = gbl.mtrks[0].miditrk # The meta track # 1st we copy existing tempos into a new list for t in mt: for ev in mt[t]: if ev[0:3] == tcmd: tempos.append((t, ev[3:])) # save the 24 bit value # now we delete any tempos in the fermata range. This avoids broken loops for t, ev in tempos: if t >= moff and t <= mend: gbl.mtrks[0].delDup(t, tcmd) # find last tempo before fermata bock and last tempo in block tempos.sort() oldTempo = gbl.tempo newTempo = gbl.tempo for f, t in tempos: # f==offsets, t==encoded tempos if f <= moff: oldTempo = 60000000 // byte3ToInt(t) if f >= moff and f <= mend: newTempo = 60000000 // byte3ToInt(t) gbl.mtrks[0].addTempo(moff, int(oldTempo / (adj / 100))) gbl.mtrks[0].addTempo(mend, newTempo) # Move selected events in the effected area to it's start/end. # To start -> note on, program change # to end -> note off # Done only if the fermata comes after desired location ... # otherwise there aren't any events to zap! if offset < 0: for n, tr in gbl.mtrks.items(): # do each track if n <= 0: continue # skip meta track trk = gbl.mtrks[n].miditrk startEvents = [] endEvents = [] for f in sorted(trk): # all in this track (sort keeps orig order) if f > moff and f < mend: # well, only in the fermata range remain = [] for ev in trk[f]: # all events in the offset if not ev: # skip empty events continue # Get event type. The [0:1] is needed to # maintain this as a byte() for python3, otherwise # py3 will think it's an int() and barfs evtype = unpack('B', ev[0:1])[0] >> 4 if evtype == 0x9: # note event if ev[2] == 0: # off to end endEvents.append(ev) else: startEvents.append(ev) # on to start continue if evtype == 0xb or evtype == 0xc: # program/controller change startEvents.append(ev) # all to start (??) continue remain.append(ev) trk[f] = remain # remaining events for this offset if startEvents: if moff in trk: trk[moff].extend(startEvents) else: trk[moff] = startEvents if endEvents: if mend in trk: trk[mend] = endEvents + trk[mend] else: trk[mend] = endEvents if gbl.debug: print("Fermata: Beat %s, Duration %s, Change %s, Bar %s" % (offset, dur, adj, gbl.barNum + 1)) if offset < 0: print(" NoteOn Events moved in tick range %s to %s" % (moff + 1, mend - 1))
def addKeySig(self, offset, n, mi): """ Set the midi key signature. """ cmd = packBytes(0xff, 0x59) self.delDup(offset, cmd) self.addToTrack(offset, packBytes(cmd, (0x02, n, mi)))
def addMarker(self, offset, msg): """ Create a midi MARKER event.""" self.addToTrack(offset, packBytes((0xff, 0x06), intToVarNumber(len(msg)), msg))
def addText(self, offset, msg): """ Create a midi TextEvent.""" self.addToTrack(offset, packBytes((0xff, 0x01), intToVarNumber(len(msg)), msg))
def addCuePoint(self, offset, msg): """ Create a MIDI cue pointr event. """ self.addToTrack(offset, packBytes((0xff, 0x07), intToVarNumber(len(msg)), msg ))
def addPairToTrack(self, boffset, startRnd, endRnd, duration, note, v, unify ): """ Add a note on/off pair to a track. boffset - offset into current bar startRnd, endRnd - rand val start adjustment duration - note len note - midi value of note v - midi velocity unify - if set attempt to unify/compress on/offs This function tries its best to handle overlapping events. Easy to show effect with a table of note ON/OFF pairs. Both events are for the same note pitch. Offsets | 200 | 300 | 320 | 420 ---------|--------|--------|-------|-------- Pair1 | on | | off | Pair2 | | on | | off The logic here will delete the OFF event at 320 and insert a new OFF at 300. Result is that when playing Pair1 will turn off at 300 followed by the same note in Pair2 beginning sounded right after. Why the on/off? Remember: Velocities may be different! However, if the unify flag is set we should end up with: Offsets | 200 | 300 | 320 | 420 ---------|--------|--------|-------|-------- Pair1 | on | | | Pair2 | | | | off """ # Start/end offsets onOffset = getOffset(boffset, startRnd, endRnd) # If the ON or OFF offset is <0 we change it to 0 for a couple of # reasons: # 1. It'll be converted anyway in 'addToTrack', but that doesn't # get back to us here. # 2. If we don't that wrong offset will be reported in lastOffEvent. if onOffset < 0: onOffset = 0 offOffset = onOffset + duration if offOffset < 0: offOffset = 0 # ON/OFF events onEvent = packBytes(0x90 | self.channel, note, v) offEvent = packBytes(onEvent[:-1], 0) noOnFlag = False f = self.lastOffEvent[note] if f is not None and f >= onOffset and f <= offOffset: # evlist is a delta-offset list. It should have a note off event # for the this note. Just in case, we do check; but it's probably # not necessary. The off event is deleted and the unify stuff is done if offEvent in self.miditrk[f]: self.miditrk[f].remove(offEvent) if not unify: self.addToTrack(onOffset, offEvent, MIDI_NOTE) else: noOnFlag = True if not noOnFlag: self.addToTrack(onOffset, onEvent, MIDI_NOTE) self.addToTrack(offOffset, offEvent, MIDI_NOTE) # Save the NOTE OFF time for the next loop. self.lastOffEvent[note] = offOffset
def insertNote(trk, ln): """ Insert specified (raw) MIDI notes into track. """ if len(ln) != 4: error("Use: %s MidiNote: <offset> <note> <velocity> <duration>" % trk.name) acctable = keySig.accList # keysig modifier, use for chord offset = getoffset(trk, ln[0]) # Set a flag if this is a drum track. if trk.vtype == 'DRUM': isdrum = 1 elif trk.vtype in ('MELODY', 'SOLO') and trk.drumType: isdrum = 1 else: isdrum = 0 notes = [] for n in ln[1].split(','): if n[0] in '0123456789': n = stoi(n) else: if isdrum: if n == '*': if trk.vtype in ('MELODY', 'SOLO'): n = trk.drumTone else: n = trk.toneList[gbl.seqCount] else: n = MMA.midiC.drumToValue(n) if n < 0: error("MidiNote: unknown drum tone '%s' in %s." % (n, trk.name)) else: n = note2val(trk, acctable, n) if n < 0 or n > 127: error("MidiNote: Notes must be in the range 0...127, not %s" % n) if trk.transpose and not isdrum: n += gbl.transpose while n < 0: n += 12 while n > 127: n -= 12 if trk.oadjust and not isdrum: n += (trk.oadjust * 12) while n < 0: n += 12 while n > 127: n -= 12 notes.append(n) velocity = stoi(ln[2]) if velocity < 1 or velocity > 127: error("MidiNote: Note velocity must be in the range 1...127, not %s" % velocity) velocity *= trk.vadjust if velocity < 1: velocity = 1 elif velocity > 127: velocity = 127 velocity = int(velocity) # trk.adjust can be a float if trk.tickdur: duration = stoi(ln[3]) else: duration = MMA.notelen.getNoteLen(ln[3]) if trk.articulate: duration = (duration * trk.artic[gbl.seqCount]) // 100 if duration < 1: duration = 1 channel = trk.channel track = gbl.mtrks[channel] for n in notes: onEvent = packBytes((0x90 | channel - 1, n, velocity)) offEvent = packBytes(onEvent[:-1], 0) track.addToTrack(gbl.tickOffset + offset, onEvent) track.addToTrack(gbl.tickOffset + offset + duration, offEvent) if gbl.debug: print("MidiNote Note %s: inserted note %s at offset %s." % (trk.name, notes, offset))
def addPairToTrack(self, boffset, startRnd, endRnd, duration, note, v, unify ): """ Add a note on/off pair to a track. boffset - offset into current bar startRnd, endRnd - rand val start adjustment duration - note len note - midi value of note v - midi velocity unify - if set attempt to unify/compress on/offs This function tries its best to handle overlapping events. Easy to show effect with a table of note ON/OFF pairs. Both events are for the same note pitch. Offsets | 200 | 300 | 320 | 420 ---------|--------|--------|-------|-------- Pair1 | on | | off | Pair2 | | on | | off The logic here will delete the OFF event at 320 and insert a new OFF at 300. Result is that when playing Pair1 will turn off at 300 followed by the same note in Pair2 beginning sounded right after. Why the on/off? Remember: Velocities may be different! However, if the unify flag is set we should end up with: Offsets | 200 | 300 | 320 | 420 ---------|--------|--------|-------|-------- Pair1 | on | | | Pair2 | | | | off """ # Start/end offsets onOffset = getOffset(boffset, startRnd, endRnd) # If the ON or OFF offset is <0 we change it to 0 for a couple of # reasons: # 1. It'll be converted anyway in 'addToTrack', but that doesn't # get back to us here. # 2. If we don't that wrong offset will be reported in lastOffEvent. if onOffset < 0: onOffset = 0 offOffset = onOffset + duration if offOffset < 0: offOffset = 0 # ON/OFF events onEvent = packBytes(0x90 | self.channel, note, v) offEvent = packBytes(onEvent[:-1], 0) noOnFlag = False f = self.lastOffEvent[note] if f != None and f >= onOffset and f <= offOffset: # evlist is a delta-offset list. It should have a note off event # for the this note. Just in case, we do check; but it's probably # not necessary. The off event is deleted and the unify stuff is done if offEvent in self.miditrk[f]: self.miditrk[f].remove(offEvent) if not unify: self.addToTrack(onOffset, offEvent, MIDI_NOTE) else: noOnFlag = True if not noOnFlag: self.addToTrack(onOffset, onEvent, MIDI_NOTE) self.addToTrack(offOffset, offEvent, MIDI_NOTE) # Save the NOTE OFF time for the next loop. self.lastOffEvent[note] = offOffset
def insertNote(trk, ln): """ Insert specified (raw) MIDI notes into track. """ if len(ln) != 4: error("Use: %s MidiNote: <offset> <note> <velocity> <duration>" % trk.name) acctable = keySig.accList # keysig modifier, use for chord offset = getoffset(trk, ln[0]) # Set a flag if this is a drum track. if trk.vtype == "DRUM": isdrum = 1 elif trk.vtype in ("MELODY", "SOLO") and trk.drumType: isdrum = 1 else: isdrum = 0 notes = [] for n in ln[1].split(","): if n[0] in "0123456789": n = stoi(n) else: if isdrum: if n == "*": if trk.vtype in ("MELODY", "SOLO"): n = trk.drumTone else: n = trk.toneList[gbl.seqCount] else: n = MMA.midiC.drumToValue(n) if n < 0: error("MidiNote: unknown drum tone '%s' in %s." % (n, trk.name)) else: n = note2val(trk, acctable, n) if n < 0 or n > 127: error("MidiNote: Notes must be in the range 0...127, not %s" % n) if trk.transpose and not isdrum: n += gbl.transpose while n < 0: n += 12 while n > 127: n -= 12 if trk.oadjust and not isdrum: n += trk.oadjust * 12 while n < 0: n += 12 while n > 127: n -= 12 notes.append(n) velocity = stoi(ln[2]) if velocity < 1 or velocity > 127: error("MidiNote: Note velocity must be in the range 1...127, not %s" % velocity) velocity *= trk.vadjust if velocity < 1: velocity = 1 elif velocity > 127: velocity = 127 velocity = int(velocity) # trk.adjust can be a float if trk.tickdur: duration = stoi(ln[3]) else: duration = MMA.notelen.getNoteLen(ln[3]) if trk.articulate: duration = (duration * trk.artic[gbl.seqCount]) // 100 if duration < 1: duration = 1 channel = trk.channel track = gbl.mtrks[channel] for n in notes: onEvent = packBytes((0x90 | channel - 1, n, velocity)) offEvent = packBytes(onEvent[:-1], 0) track.addToTrack(gbl.tickOffset + offset, onEvent) track.addToTrack(gbl.tickOffset + offset + duration, offEvent) if gbl.debug: print("MidiNote Note %s: inserted note %s at offset %s." % (trk.name, notes, offset))
def addWheel(self, offset, v): """ Set lsb/msb for the modulation wheel. """ self.addToTrack(offset, packBytes((0xe0 | self.channel), intTo14(v)))
def addCtl(self, offset, l): """ Add arbitary control sequence to track.""" self.addToTrack(offset, packBytes(0xb0 | self.channel, l))
def addPan(self, offset, v): """ Set the lsb of the pan setting.""" self.addToTrack(offset, packBytes((0xb0 | self.channel, 0x0a, v)))
def midiinc(ln): """ Include a MIDI file into MMA generated files. """ filename = '' doLyric = 0 doText = 0 channels = [] transpose = None stripSilence = -1 report = 0 istart = 0 # istart/end are in ticks iend = 0xffffff # but are set in options in Beats verbose = 0 octAdjust = 0 velAdjust = 100 ignorePC = 1 stretch = None notopt, ln = opt2pair(ln) if notopt: error("MidiInc: Expecting cmd=opt pairs, not '%s'." % ' '.join(notopt)) for cmd, opt in ln: cmd = cmd.upper() if cmd == 'FILE': filename = MMA.file.fixfname(opt) elif cmd == 'VOLUME': velAdjust = stoi(opt) elif cmd == 'OCTAVE': octAdjust = stoi(opt) if octAdjust < -4 or octAdjust > 4: error("MidiInc: 'Octave' adjustment must be -4 to 4, not %s" % opt) octAdjust *= 12 elif cmd == 'TRANSPOSE': transpose = stoi(opt) if transpose < -24 or transpose > 24: error("MidiInc: 'Transpose' must be -24 to 24, not %s" % opt) elif cmd == 'START': if opt[-1].upper() == 'M': # measures istart = int(stof(opt[:-1]) * gbl.barLen) elif opt[-1].upper() == 'T': # ticks istart = int(stof(opt[:-1])) else: # must be digits, stof() catches errors istart = int((stof(opt)-1) * gbl.BperQ) if istart < 0: error("MidiInc: 'Start' must be > 0.") elif cmd == 'END': if opt[-1].upper() == 'M': iend = int((stof(opt[:-1])-1) * gbl.barLen) elif opt[-1].upper() == 'T': iend = int(stof(opt[:-1])) else: iend = int((stof(opt)-1) * gbl.BperQ) if iend < 0: error("MidiInc: 'End' must be > 0.") elif cmd == 'TEXT': opt = opt.upper() if opt in ("ON", '1'): doText = 1 elif opt in ("OFF", '0'): doText = 0 else: error("MidiInc: 'Text' expecting 'ON' or 'OFF'") elif cmd == 'LYRIC': opt = opt.upper() if opt in ("ON", '1'): doLyric = 1 elif opt in ("OFF", '0'): doLyric = 0 else: error("MidiInc: 'Lyric' expecting 'ON' or 'OFF'") elif cmd == "REPORT": opt = opt.upper() if opt in ("ON", '1'): report = 1 elif opt in ("OFF", '0'): report = 0 else: error("MidiInc: 'Report' expecting 'ON' or 'OFF'") elif cmd == "VERBOSE": opt = opt.upper() if opt in ("ON", '1'): verbose = 1 elif opt in ("OFF", '0'): verbose = 0 else: error("MidiInc: 'Verbose' expecting 'ON' or 'OFF'") elif cmd == "STRIPSILENCE": opt = opt.upper() if opt in ("OFF", '0'): stripSilence = 0 elif opt == "ON": # this is the default stripSilence = -1 else: stripSilence = stoi(opt, "MIdiInc StripSilence= expecting " "'value', 'On' or 'Off', not %s" % opt) elif cmd == "IGNOREPC": opt = opt.upper() if opt in ("TRUE", "ON", "1"): # default ignorePC = 1 elif opt in ("FALSE", "OFF", "0"): # use program change in imported ignorePC = 0 else: error("MIdiInc: 'IncludePC' expecting 'True' or 'False', not %s" % opt) elif cmd == "STRETCH": v = stof(opt) if v < 1 or v > 500: error("MidiInc: 'Stretch' range of 1 to 500, not %s." % opt) stretch = v/100. # If none of the above matched a CMD we assume that it is # a trackname. Keep this as the last test! else: trackAlloc(cmd, 0) if not cmd in gbl.tnames: error("MidiInc: %s is not a valid MMA track" % cmd) opt = opt.split(',') riffmode = 0 printriff = 0 ch = None for o in opt: o = o.upper() if o == 'RIFF': riffmode = 1 elif o == 'SEQUENCE': riffmode = 2 elif o == 'PRINT': printriff = 1 if not riffmode: riffmode = 1 else: if ch is not None: error("MidiInc: Only one channel assignment per track.") ch = stoi(o) if ch < 1 or ch > 16: error("MidiInc: MIDI channel for import must be 1..16, not %s" % ch) channels.append((cmd, ch-1, riffmode, printriff)) # If transpose was NOT set, use the global transpose value # Note special riff value as well. Need to double adjust since # the riff import will do its own adjustment. # this needs to be done BEFORE reading the midi file if transpose is None: transpose = gbl.transpose riffTranspose = -gbl.transpose else: riffTranspose = 0 octAdjust += transpose # this takes care of octave and transpose mf = MidiData() mf.octaveAdjust = octAdjust mf.velocityAdjust = velAdjust mf.ignorePC = ignorePC try: mf.readFile(filename) except RuntimeError as e: error("MidiInc: %s" % e) if mf.beatDivision != gbl.BperQ: warning("MIDI file '%s' tick/beat of %s differs from MMA's " "%s. Will try to compensate" % (filename, mf.beatDivision, gbl.BperQ)) mf.adjustBeats( gbl.BperQ / float(mf.beatDivision)) if report or verbose: # try to be helpful print("MIDI File %s successfully read." % filename) print("Total Text events: %s" % len(mf.textEvents)) print("Total Lyric events: %s" % len(mf.lyricEvents)) print('\n') for ch in sorted(mf.events.keys()): if not mf.events[ch]: continue if verbose and not report: # in verbose mode only list info for tracks we're using doit = 0 for z in channels: if z[1] == ch: doit = 1 break if not doit: continue fnote = fevent = 0xffffff ncount = 0 for ev in mf.events[ch]: delta = ev[0] if delta < fevent: fevent = delta if ev[1] >> 4 == 0x9: if delta < fnote: fnote = delta if ev[3]: ncount += 1 msg = ["Channel %2s: First event %-8s" % (ch+1, fevent)] if ncount: msg.append("First Note %-8s Total Notes %-4s" % (fnote, ncount)) print(' '.join(msg)) if report: print("\nNo data generated!") sys.exit(0) if not channels: if doLyric or doText: warning("MidiInc: no import channels specified, " "only text or lyrics imported") else: error("MidiInc: A channel to import and a destination " "track must be specified") if (istart >= iend) or (istart < 0) or (iend < 0): error("MidiInc: Range invalid, start=%s, end=%s" % (istart, iend)) if gbl.debug: print("MidiInc: file=%s, Volume=%s, Octave=%s, Transpose=%s, Lyric=%s, " "Text=%s, Range=%s..%s StripSilence=%s Verbose=%s" % (filename, velAdjust, octAdjust, transpose, doLyric, doText, istart, iend, stripSilence, verbose)) msg = [] for t, ch, riffmode, riffprint in channels: o = '' if riffmode == 1: o = ',riff' elif riffmode == 2: o = ',sequence' elif printriff: o += ',print' msg.append("MidiInc: Channel %s-->%s%s" % (ch+1, t, o)) print(' '.join(msg)) if stretch: if verbose: print("Applying stretch to all events. Deltas will be multiplied by %s" % stretch) for tr in mf.events: for e in mf.events[tr]: e[0] = int(e[0] * stretch) # e[0] is the offset for e in mf.textEvents: e[0] = int(e[0] * stretch) for e in mf.lyricEvents: e[0] = int(e[0] * stretch) # Midi file parsed, add selected events to mma data if stripSilence == 0: if verbose: print("Firstnote offset was %s. Being reset to start of file by StripSilence=Off." % mf.firstNote) mf.firstNote = 0 if verbose: print("First note offset: %s" % mf.firstNote) if doText: inst = 0 disc = 0 if verbose: print("Scanning %s textevents." % len(mf.textEvents)) for tm, tx in mf.textEvents: delta = tm-mf.firstNote if delta >= istart and delta <= iend: gbl.mtrks[0].addText(gbl.tickOffset + delta, tx) inst += 1 else: disc += 1 if gbl.debug: print("MidiInc text events: %s inserted, %s out of range." % (inst, disc)) if doLyric: inst = 0 disc = 0 if verbose: print("Scanning %s LyricEvents." % len(mf.lyricEvents)) for tm, tx in mf.lyricEvents: delta = tm-mf.firstNote if delta >= istart and delta <= iend: gbl.mtrks[0].addLyric(gbl.tickOffset + delta, tx) inst += 1 else: disc += 1 if gbl.debug: print("MidiInc lyric events: %s inserted, %s out of range." % (inst, disc)) for n, c, riffmode, printriff in channels: if not len(mf.events[c]): warning("No data to assign from imported channel %s to track %s" % (c+1, n)) inst = 0 disc = 0 for tr, ch, riffmode, printriff in channels: onNotes = [] if gbl.tnames[tr].disable: # skip if disabled track if verbose: print("Skipping import of channel %s since track %s is disabled." % (ch, tr)) continue t = gbl.tnames[tr] if not t.channel: t.setChannel() if riffmode: riff = [] if t.vtype not in ('MELODY', 'SOLO'): error("MidiInc: Riff only works on Melody/Solo tracks, not '%s'." % t.name) t.clearPending() if t.voice[0] != t.ssvoice: gbl.mtrks[t.channel].addProgChange(gbl.tickOffset, t.voice[0], t.ssvoice) channel = t.channel track = gbl.mtrks[channel] if verbose: print("Parsing imported file. Channel=%s Track=%s MIDI Channel=%s" % (ch, tr, channel)) if len(mf.events[ch]): print(" Total events: %s; Event range: %s %s; Start/End Range: %s %s" % (len(mf.events[ch]), mf.events[ch][0][0], mf.events[ch][-1][0], istart, iend)) else: print("No events in Channel %s" % ch) # If we're processing midi voice changes (ignorePC=off) and there # are events BEFORE the first note, w eneed to insert # them before the notes. We put them all at the current midi offset. if ignorePC==0: for ev in mf.events[ch]: if ev[0] > mf.firstNote: break if ev[1] >> 4 == 0xc: track.addToTrack(gbl.tickOffset, packBytes(ev[1] | channel-1, *ev[2:])) inst += 1 disc -= 1 for ev in mf.events[ch]: delta = ev[0]-mf.firstNote if delta >= istart and delta <= iend: if riffmode: offset = delta-istart x = ev[1] >> 4 if x != 0x09 and x != 0x08: # skip non note events continue pitch = ev[2] velocity = ev[3] if x == 0x8: velocity = 0 riff.append([offset, pitch, velocity]) else: offset = gbl.tickOffset + (delta-istart) # add note on/off, key pressure, etc. track.addToTrack(offset, packBytes(ev[1] | channel-1, *ev[2:])) # Track on/off events to avoid stuck midi notes. x = ev[1] >> 4 if x == 0x09 and ev[3] and ev[2] not in onNotes: onNotes.append(ev[2]) # append this pitch to onNotes if x == 0x09 and not ev[3] or x == 0x08 and ev[2] in onNotes: onNotes.remove(ev[2]) # remove this as being ON inst += 1 else: disc += 1 if onNotes: for x in onNotes: track.addToTrack(offset, packBytes(0x90 | channel-1, [x,0])) warning("MidiINC: Stuck MIDI note(s) '%s' turned off in %s." % (', '.join([str(x) for x in onNotes]), tr)) if riffmode: evlist = createRiff(riff, tr, riffTranspose) if riffmode == 2: txt = [] for a in sorted(evlist): if printriff and riffmode == 1: print("%s Riff %s" % (tr, evlist[a])) elif riffmode == 2: # sequence mode, create sequence line and push into input txt.append("{%s}" % evlist[a]) else: # riffmode==1, printriff=0 - just add to the riff stack gbl.tnames[tr].setRiff(evlist[a]) if riffmode == 2 and txt: if printriff: print("%s Sequence %s" % (tr, ' '.join(txt))) else: MMA.sequence.trackSequence(tr, txt) if gbl.debug: print("MidiInc events: %s inserted, %s out of range." % (inst, disc))
def writeMidiTrack(self, out): """ Create/write the MIDI track. We convert timing offsets to midi-deltas. """ tr = self.miditrk """ If the -1 command line option is set we need to add a terminate to the end of each track. This is done to make looping software like seq24 happy. We do this by truncating all data in the file past the current tick pointer and inserting an all-notes-off at that position. """ if gbl.endsync and self.channel >= 0: eof = gbl.tickOffset for offset in tr.keys(): if offset > eof: del tr[offset] self.addToTrack(eof, packBytes((0xb0 | self.channel, 0x7b, 0))) """ To every MIDI track we generate we add (if the -0 flag was set) an on/off beep at offset 0. This makes for easier sync in multi-tracks. """ if gbl.synctick and self.channel >= 0: t, v = syncTone self.addToTrack(0, packBytes((0x90 | self.channel, t, v))) self.addToTrack(1, packBytes((0x90 | self.channel, t, 0))) if gbl.debug: ttl = 0 lg = 1 for t in tr: a = len(tr[t]) if a > lg: lg = a ttl += a if self.channel == -1: nm = "META" else: nm = self.trackname print( "<%s> Unique ts: %s; Ttl events %s; Average ev/ts %.2f" % (nm, len(tr), ttl, float(ttl)/len(tr))) last = 0 # Convert all events to MIDI deltas and store in # the track array/list tdata = [] # empty track container lastSts = None # Running status tracker for a in sorted(tr.keys()): delta = a-last if not tr[a]: continue # this skips the delta offset update! for d in tr[a]: """ Running status check. For each packet compare the first byte with the first byte of the previous packet. If it is can be converted to running status we strip out byte 0. Note that valid running status byte are 0x80..0xef. 0xfx are system messages and are not suitable for running status. """ if len(d) > 1: if d[0] == lastSts: d = d[1:] else: lastSts = d[0] if lastSts < 0x80 or lastSts > 0xef or not gbl.runningStatus: lastSts = None tdata.extend([intToVarNumber(delta), d]) delta = 0 last = a # Add an EOF to the track (included in total track size) tdata.append(intToVarNumber(0)) tdata.append(packBytes((0xff, 0x2f, 0x00))) tdata = bytearray(b'').join(tdata) totsize = len(tdata) out.write(b"MTrk") out.write(intToLong(totsize)) out.write(tdata)