| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713 |
- '''
- itl_chorus -- ITL Chorus Suite
- mkiv -- Make Intervals
- This simple script (using python-midi) reads a MIDI file and makes an interval
- (.iv) file (actually XML) that contains non-overlapping notes.
- '''
- import xml.etree.ElementTree as ET
- import midi
- import sys
- import os
- import optparse
- import math
- TRACKS = object()
- PROGRAMS = object()
- parser = optparse.OptionParser()
- parser.add_option('-s', '--channel-split', dest='chansplit', action='store_true', help='Split MIDI channels into independent tracks (as far as -T is concerned)')
- parser.add_option('-S', '--split-out', dest='chansfname', help='Store the split-format MIDI back into the specified file')
- parser.add_option('-c', '--preserve-channels', dest='chanskeep', action='store_true', help='Keep the channel number when splitting channels to tracks (default is to set it to 1)')
- parser.add_option('-T', '--track-split', dest='tracks', action='append_const', const=TRACKS, help='Ensure all tracks are on non-mutual streams')
- parser.add_option('-t', '--track', dest='tracks', action='append', help='Reserve an exclusive set of streams for certain conditions (try --help-conds)')
- parser.add_option('--help-conds', dest='help_conds', action='store_true', help='Print help on filter conditions for streams')
- parser.add_option('-p', '--program-split', dest='tracks', action='append_const', const=PROGRAMS, help='Ensure all programs are on non-mutual streams (overrides -T presently)')
- parser.add_option('-P', '--percussion', dest='perc', help='Which percussion standard to use to automatically filter to "perc" (GM, GM2, or none)')
- parser.add_option('-f', '--fuckit', dest='fuckit', action='store_true', help='Use the Python Error Steamroller when importing MIDIs (useful for extended formats)')
- parser.add_option('-v', '--verbose', dest='verbose', action='store_true', help='Be verbose; show important parts about the MIDI scheduling process')
- parser.add_option('-d', '--debug', dest='debug', action='store_true', help='Debugging output; show excessive output about the MIDI scheduling process (please use less or write to a file)')
- parser.add_option('-D', '--deviation', dest='deviation', type='int', help='Amount (in semitones/MIDI pitch units) by which a fully deflected pitchbend modifies the base pitch (0 disables pitchbend processing)')
- parser.add_option('-M', '--modwheel-freq-dev', dest='modfdev', type='float', help='Amount (in semitones/MIDI pitch unites) by which a fully-activated modwheel modifies the base pitch')
- parser.add_option('--modwheel-freq-freq', dest='modffreq', type='float', help='Frequency of modulation periods (sinusoids) of the modwheel acting on the base pitch')
- parser.add_option('--modwheel-amp-dev', dest='modadev', type='float', help='Deviation [0, 1] by which a fully-activated modwheel affects the amplitude as a factor of that amplitude')
- parser.add_option('--modwheel-amp-freq', dest='modafreq', type='float', help='Frequency of modulation periods (sinusoids) of the modwheel acting on amplitude')
- parser.add_option('--modwheel-res', dest='modres', type='float', help='(Fractional) seconds by which to resolve modwheel events (0 to disable)')
- parser.add_option('--modwheel-continuous', dest='modcont', action='store_true', help='Keep phase continuous in global time (don\'t reset to 0 for each note)')
- parser.add_option('--string-res', dest='stringres', type='float', help='(Fractional) seconds by which to resolve string models (0 to disable)')
- parser.add_option('--string-max', dest='stringmax', type='int', help='Maximum number of events to generate per single input event')
- parser.add_option('--string-rate-on', dest='stringonrate', type='float', help='Rate (amplitude / sec) by which to exponentially decay in the string model while a note is active')
- parser.add_option('--string-rate-off', dest='stringoffrate', type='float', help='Rate (amplitude / sec) by which to exponentially decay in the string model after a note ends')
- parser.add_option('--string-threshold', dest='stringthres', type='float', help='Amplitude (as fraction of original) at which point the string model event is terminated')
- parser.add_option('--tempo', dest='tempo', help='Adjust interpretation of tempo (try "f1"/"global", "f2"/"track")')
- parser.add_option('--epsilon', dest='epsilon', type='float', help='Don\'t consider overlaps smaller than this number of seconds (which regularly happen due to precision loss)')
- parser.add_option('--vol-pow', dest='vol_pow', type='float', help='Exponent to raise volume changes (adjusts energy per delta volume)')
- parser.add_option('-0', '--keep-empty', dest='keepempty', action='store_true', help='Keep (do not cull) events with 0 duration in the output file')
- parser.add_option('--no-text', dest='no_text', action='store_true', help='Disable text streams (useful for unusual text encodings)')
- parser.set_defaults(tracks=[], perc='GM', deviation=2, tempo='global', modres=0.005, modfdev=2.0, modffreq=8.0, modadev=0.5, modafreq=8.0, stringres=0, stringmax=1024, stringrateon=0.7, stringrateoff=0.4, stringthres=0.02, epsilon=1e-12, vol_pow=2)
- options, args = parser.parse_args()
- if options.tempo == 'f1':
- options.tempo == 'global'
- elif options.tempo == 'f2':
- options.tempo == 'track'
- if options.help_conds:
- print '''Filter conditions are used to route events to groups of streams.
- Every filter is an expression; internally, this expression is evaluated as the body of a "lambda ev: ".
- The "ev" object will be a MergeEvent with the following properties:
- -ev.tidx: the originating track index (starting at 0)
- -ev.abstime: the real time in seconds of this event relative to the beginning of playback
- -ev.bank: the selected bank (all bits)
- -ev.prog: the selected program
- -ev.mw: the modwheel value
- -ev.ev: a midi.NoteOnEvent:
- -ev.ev.pitch: the MIDI pitch
- -ev.ev.velocity: the MIDI velocity
- -ev.ev.channel: the MIDI channel
- All valid Python expressions are accepted. Take care to observe proper shell escaping.
- Specifying a -t <group>=<filter> will group all streams under a filter; if the <group> part is omitted, no group will be added.
- For example:
- mkiv -t bass=ev.ev.pitch<35 -t treble=ev.ev.pitch>75 -T -t ev.abstime<10
- will cause these groups to be made:
- -A group "bass" with all notes with pitch less than 35;
- -Of those not in "bass", a group in "treble" with pitch>75;
- -Of what is not yet consumed, a series of groups "trkN" where N is the track index (starting at 0), which consumes the rest.
- -An (unfortunately empty) unnamed group with events prior to ten real seconds.
- As can be seen, order of specification is important. Equally important is the location of -T, which should be at the end.
- NoteOffEvents are always matched to the stream which has their corresponding NoteOnEvent (in track, pitch, and channel), and so are
- not affected or observed by filters.
- If the filters specified are not a complete cover, an anonymous group will be created with no filter to contain the rest. If
- it is desired to force this group to have a name, use -t <group>=True. This should be placed at the end.
- -T behaves exactly as if:
- -t trk0=ev.tidx==0 -t trk1=ev.tidx==1 -t trk2=ev.tidx==2 [...]
- had been specified in its place, though it is automatically sized to the number of tracks. Similarly, -P operates as if
- -t prg31=ev.prog==31 -t prg81=ev.prog==81 [...]
- had been specified, again containing only the programs that were observed in the piece.
- Groups for which no streams are generated are not written to the resulting file.'''
- exit()
- if not args:
- parser.print_usage()
- exit()
- if options.fuckit:
- import fuckit
- midi.read_midifile = fuckit(midi.read_midifile)
- for fname in args:
- try:
- pat = midi.read_midifile(fname)
- except Exception:
- import traceback
- traceback.print_exc()
- print fname, ': Exception occurred, skipping...'
- continue
- if pat is None:
- print fname, ': Too fucked to continue'
- continue
- iv = ET.Element('iv')
- iv.set('version', '1')
- iv.set('src', os.path.basename(fname))
- print fname, ': MIDI format,', len(pat), 'tracks'
- if options.verbose:
- print fname, ': MIDI Parameters:', pat.resolution, 'PPQN,', pat.format, 'format'
- if options.chansplit:
- print 'Splitting channels...'
- old_pat = pat
- pat = midi.Pattern(resolution=old_pat.resolution)
- for track in old_pat:
- chan_map = {}
- last_abstick = {}
- absticks = 0
- for ev in track:
- absticks += ev.tick
- if isinstance(ev, midi.Event):
- tick = absticks - last_abstick.get(ev.channel, 0)
- last_abstick[ev.channel] = absticks
- if options.chanskeep:
- newev = ev.copy(tick = tick)
- else:
- newev = ev.copy(channel=1, tick = tick)
- chan_map.setdefault(ev.channel, midi.Track()).append(newev)
- else: # MetaEvent
- for trk in chan_map.itervalues():
- trk.append(ev)
- items = chan_map.items()
- items.sort(key=lambda pair: pair[0])
- for chn, trk in items:
- pat.append(trk)
- print 'Split', len(old_pat), 'tracks into', len(pat), 'tracks by channel'
- if options.chansfname:
- midi.write_midifile(options.chansfname, pat)
- ##### Merge events from all tracks into one master list, annotated with track and absolute times #####
- print 'Merging events...'
- class SortEvent(object):
- __slots__ = ['ev', 'tidx', 'abstick']
- def __init__(self, ev, tidx, abstick):
- self.ev = ev
- self.tidx = tidx
- self.abstick = abstick
- sorted_events = []
- for tidx, track in enumerate(pat):
- absticks = 0
- for ev in track:
- absticks += ev.tick
- sorted_events.append(SortEvent(ev, tidx, absticks))
- sorted_events.sort(key=lambda x: x.abstick)
- if options.tempo == 'global':
- bpm_at = [{0: 120}]
- else:
- bpm_at = [{0: 120} for i in pat]
- print 'Computing tempos...'
- for sev in sorted_events:
- if isinstance(sev.ev, midi.SetTempoEvent):
- if options.debug:
- print fname, ': SetTempo at', sev.abstick, 'to', sev.ev.bpm, ':', sev.ev
- bpm_at[sev.tidx if options.tempo == 'track' else 0][sev.abstick] = sev.ev.bpm
- if options.verbose:
- print fname, ': Events:', len(sorted_events)
- print fname, ': Resolved global BPM:', bpm_at
- if options.debug:
- if options.tempo == 'track':
- for tidx, bpms in enumerate(bpm_at):
- print fname, ': Tempos in track', tidx
- btimes = bpms.keys()
- for i in range(len(btimes) - 1):
- fev = filter(lambda sev: sev.tidx == tidx and sev.abstick >= btimes[i] and sev.abstick < btimes[i+1], sorted_events)
- print fname, ': BPM partition', i, 'contains', len(fev), 'events'
- else:
- btimes = bpm_at[0].keys()
- for i in range(len(btimes) - 1):
- fev = filter(lambda sev: sev.abstick >= btimes[i] and sev.abstick < btimes[i+1], sorted_events)
- print fname, ': BPM partition', i, 'contains', len(fev), 'events'
- def at2rt(abstick, bpms):
- bpm_segs = bpms.items()
- bpm_segs.sort(key=lambda pair: pair[0])
- bpm_segs = filter(lambda pair: pair[0] <= abstick, bpm_segs)
- rt = 0
- atick = 0
- if not bpm_segs:
- rt = 0
- else:
- ctick, bpm = bpm_segs[0]
- rt = (60.0 * ctick) / (bpm * pat.resolution)
- for idx in range(1, len(bpm_segs)):
- dt = bpm_segs[idx][0] - bpm_segs[idx-1][0]
- bpm = bpm_segs[idx-1][1]
- rt += (60.0 * dt) / (bpm * pat.resolution)
- if not bpm_segs:
- bpm = 120
- ctick = 0
- else:
- ctick, bpm = bpm_segs[-1]
- if options.debug:
- print 'seg through', bpm_segs, 'final seg', (abstick - ctick, bpm)
- rt += (60.0 * (abstick - ctick)) / (bpm * pat.resolution)
- return rt
- class MergeEvent(object):
- __slots__ = ['ev', 'tidx', 'abstime', 'bank', 'prog', 'mw']
- def __init__(self, ev, tidx, abstime, bank=0, prog=0, mw=0):
- self.ev = ev
- self.tidx = tidx
- self.abstime = abstime
- self.bank = bank
- self.prog = prog
- self.mw = mw
- def copy(self, **kwargs):
- args = {'ev': self.ev, 'tidx': self.tidx, 'abstime': self.abstime, 'bank': self.bank, 'prog': self.prog, 'mw': self.mw}
- args.update(kwargs)
- return MergeEvent(**args)
- def __repr__(self):
- return '<ME %r in %d on (%d:%d) MW:%d @%f>'%(self.ev, self.tidx, self.bank, self.prog, self.mw, self.abstime)
- vol_at = [[{0: 0x3FFF} for i in range(16)] for j in range(len(pat))]
- events = []
- cur_mw = [[0 for i in range(16)] for j in range(len(pat))]
- cur_bank = [[0 for i in range(16)] for j in range(len(pat))]
- cur_prog = [[0 for i in range(16)] for j in range(len(pat))]
- chg_mw = [[0 for i in range(16)] for j in range(len(pat))]
- chg_bank = [[0 for i in range(16)] for j in range(len(pat))]
- chg_prog = [[0 for i in range(16)] for j in range(len(pat))]
- chg_vol = [[0 for i in range(16)] for j in range(len(pat))]
- ev_cnts = [[0 for i in range(16)] for j in range(len(pat))]
- tnames = [''] * len(pat)
- progs = set([0])
- for tidx, track in enumerate(pat):
- abstime = 0
- absticks = 0
- lastbpm = 120
- for ev in track:
- absticks += ev.tick
- abstime = at2rt(absticks, bpm_at[tidx if options.tempo == 'track' else 0])
- if options.debug:
- print 'tick', absticks, 'realtime', abstime
- if isinstance(ev, midi.TrackNameEvent):
- tnames[tidx] = ev.text
- if isinstance(ev, midi.ProgramChangeEvent):
- cur_prog[tidx][ev.channel] = ev.value
- progs.add(ev.value)
- chg_prog[tidx][ev.channel] += 1
- elif isinstance(ev, midi.ControlChangeEvent):
- if ev.control == 0: # Bank -- MSB
- cur_bank[tidx][ev.channel] = (0x3F & cur_bank[tidx][ev.channel]) | (ev.value << 7)
- chg_bank[tidx][ev.channel] += 1
- elif ev.control == 32: # Bank -- LSB
- cur_bank[tidx][ev.channel] = (0x3F80 & cur_bank[tidx][ev.channel]) | ev.value
- chg_bank[tidx][ev.channel] += 1
- elif ev.control == 1: # ModWheel -- MSB
- cur_mw[tidx][ev.channel] = (0x3F & cur_mw[tidx][ev.channel]) | (ev.value << 7)
- chg_mw[tidx][ev.channel] += 1
- elif ev.control == 33: # ModWheel -- LSB
- cur_mw[tidx][ev.channel] = (0x3F80 & cur_mw[tidx][ev.channel]) | ev.value
- chg_mw[tidx][ev.channel] += 1
- elif ev.control == 7: # Volume -- MSB
- lvtime, lvol = sorted(vol_at[tidx][ev.channel].items(), key = lambda pair: pair[0])[-1]
- vol_at[tidx][ev.channel][abstime] = (0x3F & lvol) | (ev.value << 7)
- chg_vol[tidx][ev.channel] += 1
- elif ev.control == 39: # Volume -- LSB
- lvtime, lvol = sorted(vol_at[tidx][ev.channel].items(), key = lambda pair: pair[0])[-1]
- vol_at[tidx][ev.channel][abstime] = (0x3F80 & lvol) | ev.value
- chg_vol[tidx][ev.channel] += 1
- events.append(MergeEvent(ev, tidx, abstime, cur_bank[tidx][ev.channel], cur_prog[tidx][ev.channel], cur_mw[tidx][ev.channel]))
- ev_cnts[tidx][ev.channel] += 1
- elif isinstance(ev, midi.MetaEventWithText):
- events.append(MergeEvent(ev, tidx, abstime))
- elif isinstance(ev, midi.Event):
- if isinstance(ev, midi.NoteOnEvent) and ev.velocity == 0:
- ev.__class__ = midi.NoteOffEvent #XXX Oww
- events.append(MergeEvent(ev, tidx, abstime, cur_bank[tidx][ev.channel], cur_prog[tidx][ev.channel], cur_mw[tidx][ev.channel]))
- ev_cnts[tidx][ev.channel] += 1
- print 'Track name, event count, final banks, bank changes, final programs, program changes, final modwheel, modwheel changes, volume changes:'
- for tidx, tname in enumerate(tnames):
- print tidx, ':', tname, ',', ','.join(map(str, ev_cnts[tidx])), ',', ','.join(map(str, cur_bank[tidx])), ',', ','.join(map(str, chg_bank[tidx])), ',', ','.join(map(str, cur_prog[tidx])), ',', ','.join(map(str, chg_prog[tidx])), ',', ','.join(map(str, cur_mw[tidx])), ',', ','.join(map(str, chg_mw[tidx])), ',', ','.join(map(str, chg_vol[tidx]))
- print 'All programs observed:', progs
- print 'Sorting events...'
- events.sort(key = lambda ev: ev.abstime)
- ##### Use merged events to construct a set of streams with non-overlapping durations #####
- print 'Generating streams...'
- class DurationEvent(MergeEvent):
- __slots__ = ['duration', 'pitch', 'modwheel', 'ampl']
- def __init__(self, me, pitch, ampl, dur, modwheel=0):
- MergeEvent.__init__(self, me.ev, me.tidx, me.abstime, me.bank, me.prog, me.mw)
- self.pitch = pitch
- self.ampl = ampl
- self.duration = dur
- self.modwheel = modwheel
- def __repr__(self):
- return '<NE %s P:%f A:%f D:%f W:%f>'%(MergeEvent.__repr__(self), self.pitch, self.ampl, self.duration, self.modwheel)
- class NoteStream(object):
- __slots__ = ['history', 'active', 'bentpitch', 'modwheel']
- def __init__(self):
- self.history = []
- self.active = None
- self.bentpitch = None
- self.modwheel = 0
- def IsActive(self):
- return self.active is not None
- def Activate(self, mev, bentpitch=None, modwheel=None):
- if bentpitch is None:
- bentpitch = mev.ev.pitch
- self.active = mev
- self.bentpitch = bentpitch
- if modwheel is not None:
- self.modwheel = modwheel
- def Deactivate(self, mev):
- self.history.append(DurationEvent(self.active, self.bentpitch, self.active.ev.velocity / 127.0, mev.abstime - self.active.abstime, self.modwheel))
- self.active = None
- self.bentpitch = None
- self.modwheel = 0
- def WouldDeactivate(self, mev):
- if not self.IsActive():
- return False
- if isinstance(mev.ev, midi.NoteOffEvent):
- return mev.ev.pitch == self.active.ev.pitch and mev.tidx == self.active.tidx and mev.ev.channel == self.active.ev.channel
- if isinstance(mev.ev, midi.PitchWheelEvent):
- return mev.tidx == self.active.tidx and mev.ev.channel == self.active.ev.channel
- if isinstance(mev.ev, midi.ControlChangeEvent):
- return mev.tidx == self.active.tidx and mev.ev.channel == self.active.ev.channel
- raise TypeError('Tried to deactivate with bad type %r'%(type(mev.ev),))
- class NSGroup(object):
- __slots__ = ['streams', 'filter', 'name']
- def __init__(self, filter=None, name=None):
- self.streams = []
- self.filter = (lambda mev: True) if filter is None else filter
- self.name = name
- def Accept(self, mev):
- if not self.filter(mev):
- return False
- for stream in self.streams:
- if not stream.IsActive():
- stream.Activate(mev)
- break
- else:
- stream = NoteStream()
- self.streams.append(stream)
- stream.Activate(mev)
- return True
- notegroups = []
- auxstream = []
- textstream = []
- if options.perc and options.perc != 'none':
- if options.perc == 'GM':
- notegroups.append(NSGroup(filter = lambda mev: mev.ev.channel == 9, name='perc'))
- elif options.perc == 'GM2':
- notegroups.append(NSGroup(filter = lambda mev: mev.bank == 15360, name='perc'))
- else:
- print 'Unrecognized --percussion option %r; should be GM, GM2, or none'%(options.perc,)
- for spec in options.tracks:
- if spec is TRACKS:
- for tidx in xrange(len(pat)):
- notegroups.append(NSGroup(filter = lambda mev, tidx=tidx: mev.tidx == tidx, name = 'trk%d'%(tidx,)))
- elif spec is PROGRAMS:
- for prog in progs:
- notegroups.append(NSGroup(filter = lambda mev, prog=prog: mev.prog == prog, name = 'prg%d'%(prog,)))
- else:
- if '=' in spec:
- name, _, spec = spec.partition('=')
- else:
- name = None
- notegroups.append(NSGroup(filter = eval("lambda ev: "+spec), name = name))
- if options.verbose:
- print 'Initial group mappings:'
- for group in notegroups:
- print ('<anonymous>' if group.name is None else group.name)
- for mev in events:
- if isinstance(mev.ev, midi.MetaEventWithText):
- textstream.append(mev)
- elif isinstance(mev.ev, midi.NoteOnEvent):
- for group in notegroups:
- if group.Accept(mev):
- break
- else:
- group = NSGroup()
- group.Accept(mev)
- notegroups.append(group)
- elif isinstance(mev.ev, midi.NoteOffEvent):
- for group in notegroups:
- found = False
- for stream in group.streams:
- if stream.WouldDeactivate(mev):
- stream.Deactivate(mev)
- found = True
- break
- if found:
- break
- else:
- print 'WARNING: Did not match %r with any stream deactivation.'%(mev,)
- if options.verbose:
- print ' Current state:'
- for group in notegroups:
- print ' Group %r:'%(group.name,)
- for stream in group.streams:
- print ' Stream: %r'%(stream.active,)
- elif options.deviation > 0 and isinstance(mev.ev, midi.PitchWheelEvent):
- found = False
- for group in notegroups:
- for stream in group.streams:
- if stream.WouldDeactivate(mev):
- base = stream.active.copy(abstime=mev.abstime)
- stream.Deactivate(mev)
- stream.Activate(base, base.ev.pitch + options.deviation * (mev.ev.pitch / float(0x2000)))
- found = True
- if not found:
- print 'WARNING: Did not find any matching active streams for %r'%(mev,)
- if options.verbose:
- print ' Current state:'
- for group in notegroups:
- print ' Group %r:'%(group.name,)
- for stream in group.streams:
- print ' Stream: %r'%(stream.active,)
- elif options.modres > 0 and isinstance(mev.ev, midi.ControlChangeEvent):
- found = False
- for group in notegroups:
- for stream in group.streams:
- if stream.WouldDeactivate(mev):
- base = stream.active.copy(abstime=mev.abstime)
- stream.Deactivate(mev)
- stream.Activate(base, stream.bentpitch, mev.mw)
- found = True
- if not found:
- print 'WARNING: Did not find any matching active streams for %r'%(mev,)
- if options.verbose:
- print ' Current state:'
- for group in notegroups:
- print ' Group %r:'%(group.name,)
- for stream in group.streams:
- print ' Stream: %r'%(stream.active,)
- else:
- auxstream.append(mev)
- lastabstime = events[-1].abstime
- for group in notegroups:
- for ns in group.streams:
- if ns.IsActive():
- print 'WARNING: Active notes at end of playback.'
- ns.Deactivate(MergeEvent(ns.active, ns.active.tidx, lastabstime))
- if options.modres > 0:
- print 'Resolving modwheel events...'
- ev_cnt = 0
- for group in notegroups:
- for ns in group.streams:
- i = 0
- while i < len(ns.history):
- dev = ns.history[i]
- if dev.modwheel > 0:
- realpitch = dev.pitch
- realamp = dev.ampl
- mwamp = float(dev.modwheel) / 0x3FFF
- dt = 0.0
- origtime = dev.abstime
- events = []
- while dt < dev.duration:
- dev.abstime = origtime + dt
- if options.modcont:
- t = origtime
- else:
- t = dt
- events.append(DurationEvent(dev, realpitch + mwamp * options.modfdev * math.sin(2 * math.pi * options.modffreq * t), realamp + mwamp * options.modadev * (math.sin(2 * math.pi * options.modafreq * t) - 1.0) / 2.0, min(options.modres, dev.duration - dt), dev.modwheel))
- dt += options.modres
- ns.history[i:i+1] = events
- i += len(events)
- ev_cnt += len(events)
- if options.verbose:
- print 'Event', i, 'note', dev, 'in group', group.name, 'resolved to', len(events), 'events'
- if options.debug:
- for ev in events:
- print '\t', ev
- else:
- i += 1
- print '...resolved', ev_cnt, 'events'
- if options.stringres:
- print 'Resolving string models...'
- st_cnt = sum(sum(len(ns.history) for ns in group.streams) for group in notegroups)
- in_cnt = 0
- ex_cnt = 0
- ev_cnt = 0
- dev_grps = []
- for group in notegroups:
- for ns in group.streams:
- i = 0
- while i < len(ns.history):
- dev = ns.history[i]
- ntime = float('inf')
- if i + 1 < len(ns.history):
- ntime = ns.history[i+1].abstime
- dt = 0.0
- ampf = 1.0
- origtime = dev.abstime
- events = []
- while dt < dev.duration and ampf * dev.ampl >= options.stringthres:
- dev.abstime = origtime + dt
- events.append(DurationEvent(dev, dev.pitch, ampf * dev.ampl, min(options.stringres, dev.duration - dt), dev.modwheel))
- if len(events) > options.stringmax:
- print 'WARNING: Exceeded maximum string model events for event', i
- if options.verbose:
- print 'Final ampf', ampf, 'dt', dt
- break
- ampf *= options.stringrateon ** options.stringres
- dt += options.stringres
- in_cnt += 1
- dt = dev.duration
- while ampf * dev.ampl >= options.stringthres:
- dev.abstime = origtime + dt
- events.append(DurationEvent(dev, dev.pitch, ampf * dev.ampl, options.stringres, dev.modwheel))
- if len(events) > options.stringmax:
- print 'WARNING: Exceeded maximum string model events for event', i
- if options.verbose:
- print 'Final ampf', ampf, 'dt', dt
- break
- ampf *= options.stringrateoff ** options.stringres
- dt += options.stringres
- ex_cnt += 1
- if events:
- for j in xrange(len(events) - 1):
- cur, next = events[j], events[j + 1]
- if abs(cur.abstime + cur.duration - next.abstime) > options.epsilon:
- print 'WARNING: String model events cur: ', cur, 'next:', next, 'have gap/overrun of', next.abstime - (cur.abstime + cur.duration)
- dev_grps.append(events)
- else:
- print 'WARNING: Event', i, 'note', dev, ': No events?'
- if options.verbose:
- print 'Event', i, 'note', dev, 'in group', group.name, 'resolved to', len(events), 'events'
- if options.debug:
- for ev in events:
- print '\t', ev
- i += 1
- ev_cnt += len(events)
- print '...resolved', ev_cnt, 'events (+', ev_cnt - st_cnt, ',', in_cnt, 'inside', ex_cnt, 'extra), resorting streams...'
- for group in notegroups:
- group.streams = []
- dev_grps.sort(key = lambda evg: evg[0].abstime)
- for devgr in dev_grps:
- dev = devgr[0]
- for group in notegroups:
- if group.filter(dev):
- grp = group
- break
- else:
- grp = NSGroup()
- notegroups.append(grp)
- for ns in grp.streams:
- if not ns.history:
- ns.history.extend(devgr)
- break
- last = ns.history[-1]
- if dev.abstime >= last.abstime + last.duration - 1e-3:
- ns.history.extend(devgr)
- break
- else:
- ns = NoteStream()
- grp.streams.append(ns)
- ns.history.extend(devgr)
- scnt = 0
- for group in notegroups:
- for ns in group.streams:
- scnt += 1
- print 'Final sort:', len(notegroups), 'groups with', scnt, 'streams'
- if not options.keepempty:
- print 'Culling empty events...'
- ev_cnt = 0
- for group in notegroups:
- for ns in group.streams:
- i = 0
- while i < len(ns.history):
- if ns.history[i].duration == 0.0:
- del ns.history[i]
- ev_cnt += 1
- else:
- i += 1
- print '...culled', ev_cnt, 'events'
- if options.verbose:
- print 'Final group mappings:'
- for group in notegroups:
- print ('<anonymous>' if group.name is None else group.name), '<=', '(', len(group.streams), 'streams)'
- print 'Final volume resolution...'
- for group in notegroups:
- for ns in group.streams:
- for ev in ns.history:
- t, vol = sorted(filter(lambda pair: pair[0] <= ev.abstime, vol_at[ev.tidx][ev.ev.channel].items()), key=lambda pair: pair[0])[-1]
- ev.ampl *= (float(vol) / 0x3FFF) ** options.vol_pow
- print 'Checking consistency...'
- for group in notegroups:
- if options.verbose:
- print 'Group', '<None>' if group.name is None else group.name, 'with', len(group.streams), 'streams...',
- ecnt = 0
- for ns in group.streams:
- for i in xrange(len(ns.history) - 1):
- cur, next = ns.history[i], ns.history[i + 1]
- if cur.abstime + cur.duration > next.abstime + options.epsilon:
- print 'WARNING: event', i, 'collides with next event (@', cur.abstime, '+', cur.duration, 'next @', next.abstime, ';', next.abstime - (cur.abstime + cur.duration), 'overlap)'
- ecnt += 1
- if cur.abstime > next.abstime:
- print 'WARNING: event', i + 1, 'out of sort order (@', cur.abstime, 'next @', next.abstime, ';', cur.abstime - next.abstime, 'underlap)'
- ecnt += 1
- if options.verbose:
- if ecnt > 0:
- print '...', ecnt, 'errors occured'
- else:
- print 'ok'
- print 'Generated %d streams in %d groups'%(sum(map(lambda x: len(x.streams), notegroups)), len(notegroups))
- print 'Playtime:', lastabstime, 'seconds'
- ##### Write to XML and exit #####
- ivmeta = ET.SubElement(iv, 'meta')
- abstime = 0
- prevticks = 0
- prev_bpm = 120
- for tidx, bpms in enumerate(bpm_at):
- ivbpms = ET.SubElement(ivmeta, 'bpms', track=str(tidx))
- for absticks, bpm in sorted(bpms.items(), key = lambda pair: pair[0]):
- abstime += ((absticks - prevticks) * 60.0) / (prev_bpm * pat.resolution)
- prevticks = absticks
- ivbpm = ET.SubElement(ivbpms, 'bpm')
- ivbpm.set('bpm', str(bpm))
- ivbpm.set('ticks', str(absticks))
- ivbpm.set('time', str(abstime))
- ivstreams = ET.SubElement(iv, 'streams')
- for group in notegroups:
- for ns in group.streams:
- ivns = ET.SubElement(ivstreams, 'stream')
- ivns.set('type', 'ns')
- if group.name is not None:
- ivns.set('group', group.name)
- for note in ns.history:
- ivnote = ET.SubElement(ivns, 'note')
- ivnote.set('pitch', str(note.pitch))
- ivnote.set('vel', str(int(note.ampl * 127.0)))
- ivnote.set('ampl', str(note.ampl))
- ivnote.set('time', str(note.abstime))
- ivnote.set('dur', str(note.duration))
- if not options.no_text:
- ivtext = ET.SubElement(ivstreams, 'stream', type='text')
- for tev in textstream:
- text = tev.ev.text
- text = text.decode('utf8')
- ivev = ET.SubElement(ivtext, 'text', time=str(tev.abstime), type=type(tev.ev).__name__, text=text)
- ivaux = ET.SubElement(ivstreams, 'stream')
- ivaux.set('type', 'aux')
- fw = midi.FileWriter()
- fw.RunningStatus = None # XXX Hack
- for mev in auxstream:
- ivev = ET.SubElement(ivaux, 'ev')
- ivev.set('time', str(mev.abstime))
- ivev.set('data', repr(fw.encode_midi_event(mev.ev)))
- ivargs = ET.SubElement(ivmeta, 'args')
- ivargs.text = ' '.join('%r' % (i,) for i in sys.argv[1:])
- print 'Done.'
- txt = ET.tostring(iv, 'UTF-8')
- open(os.path.splitext(os.path.basename(fname))[0]+'.iv', 'wb').write(txt)
|