mirror of
https://github.com/microtherion/VocalEasel.git
synced 2024-12-22 11:14:00 +00:00
780 lines
24 KiB
Python
780 lines
24 KiB
Python
# midi.py
|
|
|
|
"""
|
|
This module is an integeral part of the program
|
|
MMA - Musical Midi Accompaniment.
|
|
|
|
This program is free software; you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation; either version 2 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program; if not, write to the Free Software
|
|
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
|
|
Bob van der Poel <bob@mellowood.ca>
|
|
"""
|
|
|
|
|
|
from MMA.midiM import intToWord, intTo3Byte, intToLong, intToVarNumber
|
|
import MMA.midiC
|
|
|
|
import gbl
|
|
from MMA.common import *
|
|
|
|
splitChannels = []
|
|
|
|
# some constants we use to catorgize event types
|
|
MIDI_NOTE = 1
|
|
MIDI_PRG = 2
|
|
|
|
def setSplitChannels(ln):
|
|
""" Parser routine, sets up list of track to split. Overwrites existing. """
|
|
|
|
global splitChannels
|
|
|
|
splitChannels = []
|
|
|
|
for a in ln:
|
|
a = stoi(a)
|
|
if a < 1 or a >16:
|
|
error("SplitChannels: Expecting value 1 to 16, not %s" % a)
|
|
splitChannels.append(a)
|
|
|
|
if gbl.debug:
|
|
print "SplitChannels: ",
|
|
printList(splitChannels)
|
|
|
|
|
|
####################
|
|
|
|
def writeTracks(out):
|
|
""" Write the accumulated MIDI tracks to file. """
|
|
|
|
keys=gbl.mtrks.keys()
|
|
keys.sort()
|
|
|
|
""" For type 0 MIDI files all data is contained in 1 track.
|
|
We take all our tracks and copy them to track 0, then
|
|
set up keys[] so that only track 0 remains.
|
|
"""
|
|
|
|
if gbl.midiFileType == 0:
|
|
trk0=gbl.mtrks[0].miditrk
|
|
for n in keys[1:]:
|
|
trk=gbl.mtrks[n].miditrk
|
|
for k,v in trk.items():
|
|
if k in trk0:
|
|
trk0[k].extend(v)
|
|
else:
|
|
trk0[k]=v
|
|
keys=[0]
|
|
|
|
# Write header
|
|
|
|
tcount = len(keys)
|
|
out.write( mkHeader(tcount, gbl.BperQ, gbl.midiFileType) )
|
|
|
|
if gbl.barRange: # compensate for -B/-b options
|
|
stripRange()
|
|
|
|
# Write data chunks for each track
|
|
|
|
for n in keys:
|
|
if gbl.debug:
|
|
print "Writing <%s> ch=%s;" % \
|
|
(gbl.mtrks[n].trackname, n),
|
|
|
|
if n in splitChannels and gbl.midiFileType:
|
|
tcount += writeSplitTrack(n, out)
|
|
else:
|
|
gbl.mtrks[n].writeMidiTrack(out)
|
|
|
|
""" We may have changed the track count! So, we need to
|
|
fix the file header. This is offset 10/11 which contains
|
|
the number of tracks. The counter tcount has been
|
|
tracking this, so just seek, replace and seek back.
|
|
"""
|
|
|
|
if tcount != len(keys):
|
|
out.seek(0)
|
|
out.write( mkHeader(tcount, gbl.BperQ, gbl.midiFileType) )
|
|
out.seek(0, 2) # return to eof
|
|
|
|
|
|
def writeSplitTrack(channel, out):
|
|
""" Split a track. In drum tracks this puts different instruments
|
|
into individual tracks (useful!); for instrument tracks it puts
|
|
each pitch into a track (probably not useful).
|
|
"""
|
|
|
|
tr = gbl.mtrks[channel].miditrk # track to split
|
|
|
|
""" A dict to store the split midi tracks. We'll end out with
|
|
a track for each pitch which appears in the track and
|
|
a track (labeled -1) to store every other-than-note-on data.
|
|
"""
|
|
|
|
notes={}
|
|
|
|
onEvent = 0x90 + (channel-1)
|
|
offEvent = 0x80 + (channel-1)
|
|
|
|
for offset in tr.keys():
|
|
for x in range(len(tr[offset])-1, -1, -1):
|
|
ev = tr[offset][x]
|
|
if len(ev) == 3 and ( ord(ev[0]) in (onEvent, offEvent)):
|
|
n = ord(ev[1])
|
|
else:
|
|
n = -1 # special value for non-note on events
|
|
|
|
if not n in notes: # create a new mtrk if needed
|
|
notes[n]=Mtrk(10)
|
|
|
|
if offset in notes[n].miditrk: # copy event to new track
|
|
notes[n].miditrk[offset].append(ev)
|
|
else:
|
|
notes[n].miditrk[offset]=[ev]
|
|
|
|
if gbl.debug:
|
|
print " Data has been split into %s tracks." % len(notes)
|
|
|
|
# Insert a channel name in all the new tracks.
|
|
|
|
for a in notes.keys():
|
|
if a == -1:
|
|
continue
|
|
if channel == 10:
|
|
m = "%s" % MMA.midiC.valueToDrum(a)
|
|
else:
|
|
m = "%s-%s" % (gbl.mtrks[channel].trackname, a)
|
|
|
|
notes[a].addTrkName(0, m)
|
|
|
|
for a in sorted(notes.keys()):
|
|
notes[a].writeMidiTrack(out)
|
|
|
|
""" The split tracks have been written. Return the number of additional tracks
|
|
so that the caller can properly update the midi file header. Note that
|
|
len(notes)-1 IS CORRECT ... we've already figured on writing 1 track.
|
|
"""
|
|
|
|
return len(notes)-1
|
|
|
|
|
|
def mkHeader(count, tempo, Mtype):
|
|
|
|
return "MThd" + intToLong(6) + intToWord(Mtype) + intToWord(count) + intToWord(tempo)
|
|
|
|
|
|
""" Midi track class. All the midi creation is done here.
|
|
We create a class instance for each track. mtrks{}.
|
|
"""
|
|
|
|
class Mtrk:
|
|
|
|
def __init__(self, channel):
|
|
self.miditrk={}
|
|
self.channel = channel-1
|
|
self.trackname = ''
|
|
self.lastEvent = [None] * 129
|
|
self.lastPrg = 0
|
|
|
|
def delDup(self, offset, cmd):
|
|
"""Delete a duplicate event. Used by timesig, etc. """
|
|
|
|
tr=self.miditrk
|
|
lg=len(cmd)
|
|
if offset in tr:
|
|
for i,a in enumerate(tr[offset]):
|
|
if a[0:lg] == cmd:
|
|
del tr[offset][i]
|
|
|
|
|
|
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 = chr(0xff) + chr(0x58)
|
|
self.delDup(offset, cmd) # NEEDED???
|
|
self.addToTrack(offset, cmd + chr(0x04) + \
|
|
chr(nn) + chr(dd) + chr(cc) + chr(bb) )
|
|
|
|
|
|
def addKeySig(self, offset, n, mi):
|
|
""" Set the midi key signature. """
|
|
|
|
cmd = chr(0xff) + chr(0x59)
|
|
self.delDup(offset, cmd)
|
|
self.addToTrack(offset, cmd + chr(0x02) + chr(n) + chr(mi) )
|
|
|
|
|
|
def addMarker(self, offset, msg):
|
|
""" Create a midi MARKER event."""
|
|
|
|
self.addToTrack(offset, chr(0xff) + chr(0x06) + intToVarNumber(len(msg)) + msg )
|
|
|
|
|
|
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.
|
|
|
|
ev=chr(0xff) + chr(0x02) + intToVarNumber(len(msg)) + msg
|
|
tr=self.miditrk
|
|
|
|
if hasattr(self, 'ipoint'):
|
|
self.ipoint += 1
|
|
else:
|
|
self.ipoint=0
|
|
|
|
if offset in tr:
|
|
tr[offset].insert(self.ipoint, ev)
|
|
else:
|
|
tr[offset]=[ev]
|
|
|
|
|
|
def addText(self, offset, msg):
|
|
""" Create a midi TextEvent."""
|
|
|
|
self.addToTrack( offset, chr(0xff) + chr(0x01) + intToVarNumber(len(msg)) + msg )
|
|
|
|
def addLyric(self, offset, msg):
|
|
""" Create a midi lyric event. """
|
|
|
|
self.addToTrack( offset,
|
|
chr(0xff) + chr(0x05) + intToVarNumber(len(msg)) + msg )
|
|
|
|
def addCuePoint(self, offset, msg):
|
|
""" Create a MIDI cue pointr event. """
|
|
|
|
self.addToTrack( offset, chr(0xff) + chr(0x07) + intToVarNumber(len(msg)) + msg )
|
|
|
|
|
|
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 = chr(0xff) + chr(0x03)
|
|
self.delDup(offset, cmd)
|
|
self.addToTrack(offset, cmd + intToVarNumber(len(msg)) + msg )
|
|
|
|
def addProgChange( self, offset, program, oldprg):
|
|
""" Create a midi program change (handles extended voicing).
|
|
|
|
program - The MIDI program (voice) value
|
|
oldprg - existing MIDI program
|
|
"""
|
|
|
|
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, chr(0xb0 | self.channel) + chr(0x20) + chr(msb2) )
|
|
|
|
if lsb1 != lsb2: # only if CTRL0 has changed
|
|
self.addToTrack(offset, chr(0xb0 | self.channel) + chr(0x00) + chr(lsb2) )
|
|
|
|
# Always do voice change. Maybe not necessary, but let's be safe.
|
|
|
|
self.addToTrack(offset, chr(0xc0 | self.channel) + chr(v2), MIDI_PRG )
|
|
|
|
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, chr(0xb0 | self.channel) + chr(0x41) + chr(0x00) )
|
|
|
|
else:
|
|
self.addToTrack(offset, chr(0xb0 | self.channel) + chr(0x41) + chr(0x7f) )
|
|
self.addToTrack(offset, chr(0xb0 | self.channel) + chr(0x05) + chr(v) )
|
|
|
|
|
|
def addPan(self, offset, v):
|
|
""" Set the lsb of the pan setting."""
|
|
|
|
self.addToTrack(offset, chr(0xb0 | self.channel) + chr(0x0a) + chr(v) )
|
|
|
|
|
|
def addCtl(self, offset, l):
|
|
""" Add arbitary control sequence to track."""
|
|
|
|
self.addToTrack(offset, chr(0xb0 | self.channel) + l)
|
|
|
|
|
|
def addNoteOff(self, offset):
|
|
""" Insert a "All Note Off" into the midi stream.
|
|
|
|
Called from the cutTrack() function.
|
|
"""
|
|
|
|
self.addToTrack(offset,
|
|
chr(0xb0 | self.channel) + chr(0x7b) + chr(0), 1 )
|
|
|
|
|
|
def addChannelVol(self, offset, v):
|
|
""" Set the midi channel volume."""
|
|
|
|
self.addToTrack(offset,
|
|
chr(0xb0 | self.channel) + chr(0x07) + chr(v) )
|
|
|
|
|
|
def addTempo(self, offset, beats):
|
|
""" Create a midi tempo meta event.
|
|
|
|
beats - beats per second
|
|
|
|
Return - packed midi string
|
|
"""
|
|
|
|
cmd = chr(0xff) + chr(0x51)
|
|
self.delDup(offset, cmd)
|
|
self.addToTrack( offset, cmd + chr(0x03) + intTo3Byte(60000000/beats) )
|
|
|
|
|
|
def writeMidiTrack(self, out):
|
|
""" Create/write the MIDI track.
|
|
|
|
We convert timing offsets to midi-deltas.
|
|
"""
|
|
|
|
tr=self.miditrk
|
|
|
|
""" If the -1 flag 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, chr(0xb0 | self.channel) + chr(0x7b) + chr(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:
|
|
self.addToTrack(0, chr(0x90 | self.channel) + chr(80) + chr(90) )
|
|
self.addToTrack(1, chr(0x90 | self.channel) + chr(80) + chr(0) )
|
|
|
|
|
|
if gbl.debug:
|
|
ttl = 0
|
|
lg=1
|
|
for t in tr:
|
|
a=len(tr[t])
|
|
if a > lg:
|
|
lg = a
|
|
ttl += a
|
|
print "Unique ts: %s; Ttl events %s; Average ev/ts %.2f" % \
|
|
(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
|
|
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 note suitable for running status.
|
|
"""
|
|
|
|
if len(d) > 1:
|
|
if d[0] == lastSts:
|
|
d=d[1:]
|
|
else:
|
|
lastSts = d[0]
|
|
s=ord(lastSts)
|
|
if s < 0x80 or s > 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( chr(0xff) + chr(0x2f) + chr(0x00) )
|
|
|
|
tdata = ''.join(tdata)
|
|
totsize = len(tdata)
|
|
|
|
out.write("MTrk")
|
|
out.write(intToLong(totsize))
|
|
out.write( tdata )
|
|
|
|
|
|
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)
|
|
offOffset = onOffset + duration
|
|
|
|
# ON/OFF events
|
|
|
|
onEvent = chr(0x90 | self.channel) + chr(note) + chr(v)
|
|
offEvent = onEvent[:-1] + chr(0)
|
|
|
|
""" Check for overlap on last event set for this track and
|
|
do some ugly trickry.
|
|
|
|
- The noOnFlag is set if we don't want to have the main
|
|
routine add in the ON event. This is set when UNIFY is
|
|
set and we have an overlap.
|
|
|
|
- We set F to the stored event time for this note and,
|
|
if it's in the same event range as the current event
|
|
we loop though the saved events for this track. We are
|
|
looking for a NOTE OFF event.
|
|
|
|
- If we get a matching event we then delete it from the
|
|
track. This requires 2 statements: one for an event
|
|
list with only 1 event, a 2nd for multiple events.
|
|
|
|
- If UNIFY is NOT set we insert a NOTE OFF at the current
|
|
on time. This replaces the OFF we just deleted.
|
|
|
|
- If UNIFY is SET we skip the above step, and we set the
|
|
noOnFlag so that the ON event isn't set.
|
|
|
|
"""
|
|
|
|
noOnFlag = None
|
|
|
|
f=self.lastEvent[note]
|
|
if f >= onOffset and f <= offOffset:
|
|
tr=self.miditrk
|
|
for i in range(len(tr[f])):
|
|
if tr[f][i] == offEvent:
|
|
if len(tr[f]) == 1:
|
|
del(tr[f])
|
|
else:
|
|
del(tr[f][i])
|
|
if not unify:
|
|
self.addToTrack(onOffset, offEvent, MIDI_NOTE)
|
|
else:
|
|
noOnFlag=1
|
|
break
|
|
|
|
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.lastEvent[note] = offOffset
|
|
|
|
|
|
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 = chr(0x90 | self.channel) + chr(note) + chr(v)
|
|
|
|
self.addToTrack(onOffset, onEvent, MIDI_NOTE )
|
|
|
|
|
|
|
|
def zapRangeTrack(self, start, end):
|
|
""" Clear NoteOn events from track in range: start ... end.
|
|
|
|
This is called from the fermata function.
|
|
|
|
We delete the entire event list (3 bytes) from the buffer. This
|
|
can result in empty directory enteries, but that isn't a problem.
|
|
"""
|
|
|
|
trk=self.miditrk
|
|
for a in trk:
|
|
if a>=start and a<=end:
|
|
for i in range(len(trk[a])-1, -1, -1):
|
|
e = trk[a][i]
|
|
if len(e)==3 and ord(e[0]) & 0xF0 == 0x90 and ord(e[2]):
|
|
del trk[a][i]
|
|
|
|
|
|
def addToTrack(self, offset, event, evType=None):
|
|
""" Add an event to a track.
|
|
|
|
MIDI data is saved as created in track structures.
|
|
Each track has a miditrk dictionary entry which used
|
|
the time offsets and keys and has the various events
|
|
as data. Each event is a packed string of bytes and
|
|
the events are stored as a list in the order they are
|
|
created. Our storage looks like:
|
|
|
|
miditrk[OFFSET_VALUE] = [event1, event2, ...]
|
|
|
|
evType is an optional arg. Two values are used:
|
|
MIDI_PR - a program (voice) change. Save the timestamp.
|
|
MIDI_NOTE - note on/off ... check to see it doesn't happen
|
|
before the last program change.
|
|
|
|
"""
|
|
|
|
if evType == MIDI_NOTE and offset < self.lastPrg:
|
|
offset = self.lastPrg
|
|
elif evType == MIDI_PRG:
|
|
self.lastPrg=offset
|
|
|
|
if offset < 0:
|
|
offset = 0
|
|
|
|
tr=self.miditrk
|
|
|
|
if offset in tr:
|
|
tr[offset].append(event)
|
|
else:
|
|
tr[offset]=[event]
|
|
|
|
|
|
class TimeSig:
|
|
""" Track and set the current time signature.
|
|
|
|
Timesigs are completely optional and are inserted into
|
|
the MIDI file by addTimeSig(). MMA routines ignore timesig
|
|
settings.
|
|
"""
|
|
|
|
def __init__(self):
|
|
""" Initialze to null value, user will never set to this."""
|
|
|
|
self.lastsig = (None,None)
|
|
|
|
def set(self, nn, dd):
|
|
""" Set timesig. If no change from last value, ignore. """
|
|
|
|
if self.lastsig != (nn, dd):
|
|
gbl.mtrks[0].addTimeSig(gbl.tickOffset, nn, dd, 48, 8)
|
|
self.lastsig = (nn, dd)
|
|
|
|
def get(self):
|
|
""" Return existing timesig. """
|
|
|
|
return self.lastsig
|
|
|
|
|
|
timeSig = TimeSig()
|
|
|
|
|
|
|
|
def stripRange():
|
|
""" Strip out range limited data. """
|
|
|
|
bp = gbl.barPtrs # list generated at compile time
|
|
|
|
if gbl.barRange[-1] == 'ABS':
|
|
gbl.barRange.pop() # delete abs marker
|
|
for a in bp: # convert comment numbers to abs numbers
|
|
bp[a][0] = str(a)
|
|
validRange = []
|
|
|
|
for a in gbl.barRange: # list of bars we want to produce
|
|
for b in bp:
|
|
if a == bp[b][0]:
|
|
validRange.append([bp[b][1], bp[b][2]])
|
|
|
|
if not validRange:
|
|
print " Range directive -b/B would result in empty file."
|
|
print " Entire file is being created. Check the range."
|
|
return
|
|
|
|
# Collaspe/merge the valid range pointers
|
|
validRange.sort() # barptrs was dict, so this list is not in order
|
|
tmp = []
|
|
a,b = validRange[0]
|
|
for i in range(1, len(validRange)):
|
|
if b+1 == validRange[i][0]:
|
|
b = validRange[i][1]
|
|
else:
|
|
tmp.append( [a,b] )
|
|
if i < len(validRange):
|
|
a,b = validRange[i]
|
|
tmp.append( [a,b] )
|
|
validRange = tmp # list of event times to keep
|
|
|
|
""" Create list of event times to discard. Each item in the list:
|
|
is [ start-time, end-time]
|
|
"""
|
|
|
|
disList = []
|
|
lowestEv = bp[1][1]
|
|
for a in validRange:
|
|
disList.append( [lowestEv, a[0]-1] )
|
|
lowestEv = a[1]
|
|
|
|
""" Determine the last event time in the buffer. This is not nesc. the
|
|
same as the last barptr ... tempos and midinote can write past the
|
|
last bar. We need to test each track and find the highest event time.
|
|
"""
|
|
|
|
lastev = -1
|
|
for a in gbl.mtrks:
|
|
z = sorted(gbl.mtrks[a].miditrk)
|
|
if z[-1]>lastev:
|
|
lastev = z[-1]
|
|
|
|
disList.append( [validRange[-1][1]+1, lastev] )
|
|
|
|
""" 1st pass. For each track take all the cut parts and adjust their
|
|
offsets to the end offset of the previous keep section. Strip out
|
|
all note on events, lyric and text events in the effected bits.
|
|
"""
|
|
|
|
for start, end in disList:
|
|
for n in gbl.mtrks:
|
|
tr=gbl.mtrks[n].miditrk
|
|
newEvents = []
|
|
for ev in sorted(tr.keys()): # sort is important!
|
|
if ev >= start and ev <=end:
|
|
for e in tr[ev]:
|
|
# skip note on events
|
|
if ord(e[0]) & 0xf0 == 0x90 and ord(e[2]):
|
|
continue
|
|
# skip lyric and text events
|
|
if ord(e[0]) == 0xff:
|
|
if ord(e[1]) == 0x05 or ord(e[1]) == 0x01:
|
|
continue
|
|
# skip if event is note off and it already is present
|
|
if ord(e[0]) & 0xf0 == 0x90 and ord(e[2]) == 0:
|
|
for x in newEvents:
|
|
if x == e:
|
|
e=None
|
|
break
|
|
if e:
|
|
newEvents.append(e)
|
|
del (tr[ev])
|
|
|
|
""" We now have a new list of events for the 'start' offset (it might
|
|
just be an empty list) AND we have deleted the events for 'start'
|
|
to 'end'. Just a matter of inserted a new event list.
|
|
"""
|
|
|
|
if newEvents:
|
|
tr[start] = newEvents
|
|
|
|
""" 2nd pass. Adjust offsets of stuff to keep. """
|
|
|
|
offset = 1
|
|
for vals in range(len(validRange)): # each valid range
|
|
start = validRange[vals][0]
|
|
end = validRange[vals][1]+1
|
|
offset += (start - disList[vals][0])-1
|
|
for a in gbl.mtrks: # each track
|
|
tr = gbl.mtrks[a].miditrk
|
|
for ev in sorted(tr.keys()): # each event list
|
|
if ev >= start and ev <= end:
|
|
newoffset = ev-offset
|
|
if ev != newoffset: # don't append/copy creating duplicates
|
|
if newoffset in tr:
|
|
tr[newoffset].extend(tr[ev])
|
|
else:
|
|
tr[newoffset] = tr[ev]
|
|
del tr[ev]
|
|
|
|
""" After all this compressing the end of file marker is past the
|
|
end of the real data. This could bugger up the -1 option, so fix
|
|
it. Go though all the tracks again and find the end.
|
|
"""
|
|
|
|
lastev = -1
|
|
for a in gbl.mtrks:
|
|
z = sorted(gbl.mtrks[a].miditrk)
|
|
if z and z[-1]>lastev:
|
|
lastev = z[-1]
|
|
gbl.tickOffset = lastev
|
|
|
|
print " File has been truncated for -b/-B option. Bar count/time incorrect."
|