mkiv.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  1. '''
  2. itl_chorus -- ITL Chorus Suite
  3. mkiv -- Make Intervals
  4. This simple script (using python-midi) reads a MIDI file and makes an interval
  5. (.iv) file (actually XML) that contains non-overlapping notes.
  6. TODO:
  7. -MIDI Control events
  8. -Percussion
  9. '''
  10. import xml.etree.ElementTree as ET
  11. import midi
  12. import sys
  13. import os
  14. import optparse
  15. import math
  16. TRACKS = object()
  17. PROGRAMS = object()
  18. parser = optparse.OptionParser()
  19. parser.add_option('-s', '--channel-split', dest='chansplit', action='store_true', help='Split MIDI channels into independent tracks (as far as -T is concerned)')
  20. parser.add_option('-S', '--split-out', dest='chansfname', help='Store the split-format MIDI back into the specified file')
  21. 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)')
  22. parser.add_option('-T', '--track-split', dest='tracks', action='append_const', const=TRACKS, help='Ensure all tracks are on non-mutual streams')
  23. parser.add_option('-t', '--track', dest='tracks', action='append', help='Reserve an exclusive set of streams for certain conditions (try --help-conds)')
  24. parser.add_option('--help-conds', dest='help_conds', action='store_true', help='Print help on filter conditions for streams')
  25. 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)')
  26. parser.add_option('-P', '--percussion', dest='perc', help='Which percussion standard to use to automatically filter to "perc" (GM, GM2, or none)')
  27. parser.add_option('-f', '--fuckit', dest='fuckit', action='store_true', help='Use the Python Error Steamroller when importing MIDIs (useful for extended formats)')
  28. parser.add_option('-v', '--verbose', dest='verbose', action='store_true', help='Be verbose; show important parts about the MIDI scheduling process')
  29. 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)')
  30. 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)')
  31. 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')
  32. parser.add_option('--modwheel-freq-freq', dest='modffreq', type='float', help='Frequency of modulation periods (sinusoids) of the modwheel acting on the base pitch')
  33. 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')
  34. parser.add_option('--modwheel-amp-freq', dest='modafreq', type='float', help='Frequency of modulation periods (sinusoids) of the modwheel acting on amplitude')
  35. parser.add_option('--modwheel-res', dest='modres', type='float', help='(Fractional) seconds by which to resolve modwheel events (0 to disable)')
  36. 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)')
  37. parser.add_option('--tempo', dest='tempo', help='Adjust interpretation of tempo (try "f1"/"global", "f2"/"track")')
  38. parser.add_option('-0', '--keep-empty', dest='keepempty', action='store_true', help='Keep (do not cull) events with 0 duration in the output file')
  39. 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)
  40. options, args = parser.parse_args()
  41. if options.tempo == 'f1':
  42. options.tempo == 'global'
  43. elif options.tempo == 'f2':
  44. options.tempo == 'track'
  45. if options.help_conds:
  46. print '''Filter conditions are used to route events to groups of streams.
  47. Every filter is an expression; internally, this expression is evaluated as the body of a "lambda ev: ".
  48. The "ev" object will be a MergeEvent with the following properties:
  49. -ev.tidx: the originating track index (starting at 0)
  50. -ev.abstime: the real time in seconds of this event relative to the beginning of playback
  51. -ev.bank: the selected bank (all bits)
  52. -ev.prog: the selected program
  53. -ev.mw: the modwheel value
  54. -ev.ev: a midi.NoteOnEvent:
  55. -ev.ev.pitch: the MIDI pitch
  56. -ev.ev.velocity: the MIDI velocity
  57. -ev.ev.channel: the MIDI channel
  58. All valid Python expressions are accepted. Take care to observe proper shell escaping.
  59. Specifying a -t <group>=<filter> will group all streams under a filter; if the <group> part is omitted, no group will be added.
  60. For example:
  61. mkiv -t bass=ev.ev.pitch<35 -t treble=ev.ev.pitch>75 -T -t ev.abstime<10
  62. will cause these groups to be made:
  63. -A group "bass" with all notes with pitch less than 35;
  64. -Of those not in "bass", a group in "treble" with pitch>75;
  65. -Of what is not yet consumed, a series of groups "trkN" where N is the track index (starting at 0), which consumes the rest.
  66. -An (unfortunately empty) unnamed group with events prior to ten real seconds.
  67. As can be seen, order of specification is important. Equally important is the location of -T, which should be at the end.
  68. NoteOffEvents are always matched to the stream which has their corresponding NoteOnEvent (in track, pitch, and channel), and so are
  69. not affected or observed by filters.
  70. If the filters specified are not a complete cover, an anonymous group will be created with no filter to contain the rest. If
  71. it is desired to force this group to have a name, use -t <group>=True. This should be placed at the end.
  72. -T behaves exactly as if:
  73. -t trk0=ev.tidx==0 -t trk1=ev.tidx==1 -t trk2=ev.tidx==2 [...]
  74. had been specified in its place, though it is automatically sized to the number of tracks. Similarly, -P operates as if
  75. -t prg31=ev.prog==31 -t prg81=ev.prog==81 [...]
  76. had been specified, again containing only the programs that were observed in the piece.
  77. Groups for which no streams are generated are not written to the resulting file.'''
  78. exit()
  79. if not args:
  80. parser.print_usage()
  81. exit()
  82. if options.fuckit:
  83. import fuckit
  84. midi.read_midifile = fuckit(midi.read_midifile)
  85. for fname in args:
  86. try:
  87. pat = midi.read_midifile(fname)
  88. except Exception:
  89. import traceback
  90. traceback.print_exc()
  91. print fname, ': Exception occurred, skipping...'
  92. continue
  93. if pat is None:
  94. print fname, ': Too fucked to continue'
  95. continue
  96. iv = ET.Element('iv')
  97. iv.set('version', '1')
  98. iv.set('src', os.path.basename(fname))
  99. print fname, ': MIDI format,', len(pat), 'tracks'
  100. if options.verbose:
  101. print fname, ': MIDI Parameters:', pat.resolution, 'PPQN,', pat.format, 'format'
  102. if options.chansplit:
  103. print 'Splitting channels...'
  104. old_pat = pat
  105. pat = midi.Pattern(resolution=old_pat.resolution)
  106. for track in old_pat:
  107. chan_map = {}
  108. last_abstick = {}
  109. absticks = 0
  110. for ev in track:
  111. absticks += ev.tick
  112. if isinstance(ev, midi.Event):
  113. tick = absticks - last_abstick.get(ev.channel, 0)
  114. last_abstick[ev.channel] = absticks
  115. if options.chanskeep:
  116. newev = ev.copy(tick = tick)
  117. else:
  118. newev = ev.copy(channel=1, tick = tick)
  119. chan_map.setdefault(ev.channel, midi.Track()).append(newev)
  120. else: # MetaEvent
  121. for trk in chan_map.itervalues():
  122. trk.append(ev)
  123. items = chan_map.items()
  124. items.sort(key=lambda pair: pair[0])
  125. for chn, trk in items:
  126. pat.append(trk)
  127. print 'Split', len(old_pat), 'tracks into', len(pat), 'tracks by channel'
  128. if options.chansfname:
  129. midi.write_midifile(options.chansfname, pat)
  130. ##### Merge events from all tracks into one master list, annotated with track and absolute times #####
  131. print 'Merging events...'
  132. class SortEvent(object):
  133. __slots__ = ['ev', 'tidx', 'abstick']
  134. def __init__(self, ev, tidx, abstick):
  135. self.ev = ev
  136. self.tidx = tidx
  137. self.abstick = abstick
  138. sorted_events = []
  139. for tidx, track in enumerate(pat):
  140. absticks = 0
  141. for ev in track:
  142. absticks += ev.tick
  143. sorted_events.append(SortEvent(ev, tidx, absticks))
  144. sorted_events.sort(key=lambda x: x.abstick)
  145. if options.tempo == 'global':
  146. bpm_at = [{0: 120}]
  147. else:
  148. bpm_at = [{0: 120} for i in pat]
  149. print 'Computing tempos...'
  150. for sev in sorted_events:
  151. if isinstance(sev.ev, midi.SetTempoEvent):
  152. if options.debug:
  153. print fname, ': SetTempo at', sev.abstick, 'to', sev.ev.bpm, ':', sev.ev
  154. bpm_at[sev.tidx if options.tempo == 'track' else 0][sev.abstick] = sev.ev.bpm
  155. if options.verbose:
  156. print fname, ': Events:', len(sorted_events)
  157. print fname, ': Resolved global BPM:', bpm_at
  158. if options.debug:
  159. if options.tempo == 'track':
  160. for tidx, bpms in enumerate(bpm_at):
  161. print fname, ': Tempos in track', tidx
  162. btimes = bpms.keys()
  163. for i in range(len(btimes) - 1):
  164. fev = filter(lambda sev: sev.tidx == tidx and sev.abstick >= btimes[i] and sev.abstick < btimes[i+1], sorted_events)
  165. print fname, ': BPM partition', i, 'contains', len(fev), 'events'
  166. else:
  167. btimes = bpm_at[0].keys()
  168. for i in range(len(btimes) - 1):
  169. fev = filter(lambda sev: sev.abstick >= btimes[i] and sev.abstick < btimes[i+1], sorted_events)
  170. print fname, ': BPM partition', i, 'contains', len(fev), 'events'
  171. def at2rt(abstick, bpms):
  172. bpm_segs = bpms.items()
  173. bpm_segs.sort(key=lambda pair: pair[0])
  174. bpm_segs = filter(lambda pair: pair[0] <= abstick, bpm_segs)
  175. rt = 0
  176. atick = 0
  177. if not bpm_segs:
  178. rt = 0
  179. else:
  180. ctick, bpm = bpm_segs[0]
  181. rt = (60.0 * ctick) / (bpm * pat.resolution)
  182. for idx in range(1, len(bpm_segs)):
  183. dt = bpm_segs[idx][0] - bpm_segs[idx-1][0]
  184. bpm = bpm_segs[idx-1][1]
  185. rt += (60.0 * dt) / (bpm * pat.resolution)
  186. if not bpm_segs:
  187. bpm = 120
  188. ctick = 0
  189. else:
  190. ctick, bpm = bpm_segs[-1]
  191. if options.debug:
  192. print 'seg through', bpm_segs, 'final seg', (abstick - ctick, bpm)
  193. rt += (60.0 * (abstick - ctick)) / (bpm * pat.resolution)
  194. return rt
  195. class MergeEvent(object):
  196. __slots__ = ['ev', 'tidx', 'abstime', 'bank', 'prog', 'mw']
  197. def __init__(self, ev, tidx, abstime, bank=0, prog=0, mw=0):
  198. self.ev = ev
  199. self.tidx = tidx
  200. self.abstime = abstime
  201. self.bank = bank
  202. self.prog = prog
  203. self.mw = mw
  204. def copy(self, **kwargs):
  205. args = {'ev': self.ev, 'tidx': self.tidx, 'abstime': self.abstime, 'bank': self.bank, 'prog': self.prog, 'mw': self.mw}
  206. args.update(kwargs)
  207. return MergeEvent(**args)
  208. def __repr__(self):
  209. return '<ME %r in %d on (%d:%d) MW:%d @%f>'%(self.ev, self.tidx, self.bank, self.prog, self.mw, self.abstime)
  210. events = []
  211. cur_mw = [[0 for i in range(16)] for j in range(len(pat))]
  212. cur_bank = [[0 for i in range(16)] for j in range(len(pat))]
  213. cur_prog = [[0 for i in range(16)] for j in range(len(pat))]
  214. chg_mw = [[0 for i in range(16)] for j in range(len(pat))]
  215. chg_bank = [[0 for i in range(16)] for j in range(len(pat))]
  216. chg_prog = [[0 for i in range(16)] for j in range(len(pat))]
  217. ev_cnts = [[0 for i in range(16)] for j in range(len(pat))]
  218. tnames = [''] * len(pat)
  219. progs = set([0])
  220. for tidx, track in enumerate(pat):
  221. abstime = 0
  222. absticks = 0
  223. lastbpm = 120
  224. for ev in track:
  225. absticks += ev.tick
  226. abstime = at2rt(absticks, bpm_at[tidx if options.tempo == 'track' else 0])
  227. if options.debug:
  228. print 'tick', absticks, 'realtime', abstime
  229. if isinstance(ev, midi.TrackNameEvent):
  230. tnames[tidx] = ev.text
  231. if isinstance(ev, midi.ProgramChangeEvent):
  232. cur_prog[tidx][ev.channel] = ev.value
  233. progs.add(ev.value)
  234. chg_prog[tidx][ev.channel] += 1
  235. elif isinstance(ev, midi.ControlChangeEvent):
  236. if ev.control == 0: # Bank -- MSB
  237. cur_bank[tidx][ev.channel] = (0x3F & cur_bank[tidx][ev.channel]) | (ev.value << 7)
  238. chg_bank[tidx][ev.channel] += 1
  239. elif ev.control == 32: # Bank -- LSB
  240. cur_bank[tidx][ev.channel] = (0x3F80 & cur_bank[tidx][ev.channel]) | ev.value
  241. chg_bank[tidx][ev.channel] += 1
  242. elif ev.control == 1: # ModWheel -- MSB
  243. cur_mw[tidx][ev.channel] = (0x3F & cur_mw[tidx][ev.channel]) | (ev.value << 7)
  244. chg_mw[tidx][ev.channel] += 1
  245. elif ev.control == 33: # ModWheel -- LSB
  246. cur_mw[tidx][ev.channel] = (0x3F80 & cur_mw[tidx][ev.channel]) | ev.value
  247. chg_mw[tidx][ev.channel] += 1
  248. events.append(MergeEvent(ev, tidx, abstime, cur_bank[tidx][ev.channel], cur_prog[tidx][ev.channel], cur_mw[tidx][ev.channel]))
  249. ev_cnts[tidx][ev.channel] += 1
  250. elif isinstance(ev, midi.MetaEventWithText):
  251. events.append(MergeEvent(ev, tidx, abstime))
  252. elif isinstance(ev, midi.Event):
  253. if isinstance(ev, midi.NoteOnEvent) and ev.velocity == 0:
  254. ev.__class__ = midi.NoteOffEvent #XXX Oww
  255. events.append(MergeEvent(ev, tidx, abstime, cur_bank[tidx][ev.channel], cur_prog[tidx][ev.channel], cur_mw[tidx][ev.channel]))
  256. ev_cnts[tidx][ev.channel] += 1
  257. if options.verbose:
  258. print 'Track name, event count, final banks, bank changes, final programs, program changes, final modwheel, modwheel changes:'
  259. for tidx, tname in enumerate(tnames):
  260. 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]))
  261. print 'All programs observed:', progs
  262. print 'Sorting events...'
  263. events.sort(key = lambda ev: ev.abstime)
  264. ##### Use merged events to construct a set of streams with non-overlapping durations #####
  265. print 'Generating streams...'
  266. class DurationEvent(MergeEvent):
  267. __slots__ = ['duration', 'pitch', 'modwheel', 'ampl']
  268. def __init__(self, me, pitch, ampl, dur, modwheel=0):
  269. MergeEvent.__init__(self, me.ev, me.tidx, me.abstime, me.bank, me.prog, me.mw)
  270. self.pitch = pitch
  271. self.ampl = ampl
  272. self.duration = dur
  273. self.modwheel = modwheel
  274. def __repr__(self):
  275. return '<NE %s P:%f A:%f D:%f W:%f>'%(MergeEvent.__repr__(self), self.pitch, self.ampl, self.duration, self.modwheel)
  276. class NoteStream(object):
  277. __slots__ = ['history', 'active', 'bentpitch', 'modwheel']
  278. def __init__(self):
  279. self.history = []
  280. self.active = None
  281. self.bentpitch = None
  282. self.modwheel = 0
  283. def IsActive(self):
  284. return self.active is not None
  285. def Activate(self, mev, bentpitch=None, modwheel=None):
  286. if bentpitch is None:
  287. bentpitch = mev.ev.pitch
  288. self.active = mev
  289. self.bentpitch = bentpitch
  290. if modwheel is not None:
  291. self.modwheel = modwheel
  292. def Deactivate(self, mev):
  293. self.history.append(DurationEvent(self.active, self.bentpitch, self.active.ev.velocity / 127.0, mev.abstime - self.active.abstime, self.modwheel))
  294. self.active = None
  295. self.bentpitch = None
  296. self.modwheel = 0
  297. def WouldDeactivate(self, mev):
  298. if not self.IsActive():
  299. return False
  300. if isinstance(mev.ev, midi.NoteOffEvent):
  301. return mev.ev.pitch == self.active.ev.pitch and mev.tidx == self.active.tidx and mev.ev.channel == self.active.ev.channel
  302. if isinstance(mev.ev, midi.PitchWheelEvent):
  303. return mev.tidx == self.active.tidx and mev.ev.channel == self.active.ev.channel
  304. if isinstance(mev.ev, midi.ControlChangeEvent):
  305. return mev.tidx == self.active.tidx and mev.ev.channel == self.active.ev.channel
  306. raise TypeError('Tried to deactivate with bad type %r'%(type(mev.ev),))
  307. class NSGroup(object):
  308. __slots__ = ['streams', 'filter', 'name']
  309. def __init__(self, filter=None, name=None):
  310. self.streams = []
  311. self.filter = (lambda mev: True) if filter is None else filter
  312. self.name = name
  313. def Accept(self, mev):
  314. if not self.filter(mev):
  315. return False
  316. for stream in self.streams:
  317. if not stream.IsActive():
  318. stream.Activate(mev)
  319. break
  320. else:
  321. stream = NoteStream()
  322. self.streams.append(stream)
  323. stream.Activate(mev)
  324. return True
  325. notegroups = []
  326. auxstream = []
  327. textstream = []
  328. if options.perc and options.perc != 'none':
  329. if options.perc == 'GM':
  330. notegroups.append(NSGroup(filter = lambda mev: mev.ev.channel == 9, name='perc'))
  331. elif options.perc == 'GM2':
  332. notegroups.append(NSGroup(filter = lambda mev: mev.bank == 15360, name='perc'))
  333. else:
  334. print 'Unrecognized --percussion option %r; should be GM, GM2, or none'%(options.perc,)
  335. for spec in options.tracks:
  336. if spec is TRACKS:
  337. for tidx in xrange(len(pat)):
  338. notegroups.append(NSGroup(filter = lambda mev, tidx=tidx: mev.tidx == tidx, name = 'trk%d'%(tidx,)))
  339. elif spec is PROGRAMS:
  340. for prog in progs:
  341. notegroups.append(NSGroup(filter = lambda mev, prog=prog: mev.prog == prog, name = 'prg%d'%(prog,)))
  342. else:
  343. if '=' in spec:
  344. name, _, spec = spec.partition('=')
  345. else:
  346. name = None
  347. notegroups.append(NSGroup(filter = eval("lambda ev: "+spec), name = name))
  348. if options.verbose:
  349. print 'Initial group mappings:'
  350. for group in notegroups:
  351. print ('<anonymous>' if group.name is None else group.name)
  352. for mev in events:
  353. if isinstance(mev.ev, midi.MetaEventWithText):
  354. textstream.append(mev)
  355. elif isinstance(mev.ev, midi.NoteOnEvent):
  356. for group in notegroups:
  357. if group.Accept(mev):
  358. break
  359. else:
  360. group = NSGroup()
  361. group.Accept(mev)
  362. notegroups.append(group)
  363. elif isinstance(mev.ev, midi.NoteOffEvent):
  364. for group in notegroups:
  365. found = False
  366. for stream in group.streams:
  367. if stream.WouldDeactivate(mev):
  368. stream.Deactivate(mev)
  369. found = True
  370. break
  371. if found:
  372. break
  373. else:
  374. print 'WARNING: Did not match %r with any stream deactivation.'%(mev,)
  375. if options.verbose:
  376. print ' Current state:'
  377. for group in notegroups:
  378. print ' Group %r:'%(group.name,)
  379. for stream in group.streams:
  380. print ' Stream: %r'%(stream.active,)
  381. elif options.deviation > 0 and isinstance(mev.ev, midi.PitchWheelEvent):
  382. found = False
  383. for group in notegroups:
  384. for stream in group.streams:
  385. if stream.WouldDeactivate(mev):
  386. base = stream.active.copy(abstime=mev.abstime)
  387. stream.Deactivate(mev)
  388. stream.Activate(base, base.ev.pitch + options.deviation * (mev.ev.pitch / float(0x2000)))
  389. found = True
  390. if not found:
  391. print 'WARNING: Did not find any matching active streams for %r'%(mev,)
  392. if options.verbose:
  393. print ' Current state:'
  394. for group in notegroups:
  395. print ' Group %r:'%(group.name,)
  396. for stream in group.streams:
  397. print ' Stream: %r'%(stream.active,)
  398. elif options.modres > 0 and isinstance(mev.ev, midi.ControlChangeEvent):
  399. found = False
  400. for group in notegroups:
  401. for stream in group.streams:
  402. if stream.WouldDeactivate(mev):
  403. base = stream.active.copy(abstime=mev.abstime)
  404. stream.Deactivate(mev)
  405. stream.Activate(base, stream.bentpitch, mev.mw)
  406. found = True
  407. if not found:
  408. print 'WARNING: Did not find any matching active streams for %r'%(mev,)
  409. if options.verbose:
  410. print ' Current state:'
  411. for group in notegroups:
  412. print ' Group %r:'%(group.name,)
  413. for stream in group.streams:
  414. print ' Stream: %r'%(stream.active,)
  415. else:
  416. auxstream.append(mev)
  417. lastabstime = events[-1].abstime
  418. for group in notegroups:
  419. for ns in group.streams:
  420. if ns.IsActive():
  421. print 'WARNING: Active notes at end of playback.'
  422. ns.Deactivate(MergeEvent(ns.active, ns.active.tidx, lastabstime))
  423. if options.modres > 0:
  424. print 'Resolving modwheel events...'
  425. ev_cnt = 0
  426. for group in notegroups:
  427. for ns in group.streams:
  428. i = 0
  429. while i < len(ns.history):
  430. dev = ns.history[i]
  431. if dev.modwheel > 0:
  432. realpitch = dev.pitch
  433. realamp = dev.ampl
  434. mwamp = float(dev.modwheel) / 0x3FFF
  435. dt = 0.0
  436. events = []
  437. while dt < dev.duration:
  438. if options.modcont:
  439. t = dev.abstime + dt
  440. else:
  441. t = dt
  442. 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(dt, dev.duration - dt), dev.modwheel))
  443. dt += options.modres
  444. ns.history[i:i+1] = events
  445. i += len(events)
  446. ev_cnt += len(events)
  447. if options.verbose:
  448. print 'Event', i, 'note', dev, 'in group', group.name, 'resolved to', len(events), 'events'
  449. if options.debug:
  450. for ev in events:
  451. print '\t', ev
  452. else:
  453. i += 1
  454. print '...resolved', ev_cnt, 'events'
  455. if not options.keepempty:
  456. print 'Culling empty events...'
  457. ev_cnt = 0
  458. for group in notegroups:
  459. for ns in group.streams:
  460. i = 0
  461. while i < len(ns.history):
  462. if ns.history[i].duration == 0.0:
  463. del ns.history[i]
  464. ev_cnt += 1
  465. else:
  466. i += 1
  467. print '...culled', ev_cnt, 'events'
  468. if options.verbose:
  469. print 'Final group mappings:'
  470. for group in notegroups:
  471. print ('<anonymous>' if group.name is None else group.name), '<=', '(', len(group.streams), 'streams)'
  472. print 'Generated %d streams in %d groups'%(sum(map(lambda x: len(x.streams), notegroups)), len(notegroups))
  473. print 'Playtime:', lastabstime, 'seconds'
  474. ##### Write to XML and exit #####
  475. ivmeta = ET.SubElement(iv, 'meta')
  476. abstime = 0
  477. prevticks = 0
  478. prev_bpm = 120
  479. for tidx, bpms in enumerate(bpm_at):
  480. ivbpms = ET.SubElement(ivmeta, 'bpms', track=str(tidx))
  481. for absticks, bpm in sorted(bpms.items(), key = lambda pair: pair[0]):
  482. abstime += ((absticks - prevticks) * 60.0) / (prev_bpm * pat.resolution)
  483. prevticks = absticks
  484. ivbpm = ET.SubElement(ivbpms, 'bpm')
  485. ivbpm.set('bpm', str(bpm))
  486. ivbpm.set('ticks', str(absticks))
  487. ivbpm.set('time', str(abstime))
  488. ivstreams = ET.SubElement(iv, 'streams')
  489. for group in notegroups:
  490. for ns in group.streams:
  491. ivns = ET.SubElement(ivstreams, 'stream')
  492. ivns.set('type', 'ns')
  493. if group.name is not None:
  494. ivns.set('group', group.name)
  495. for note in ns.history:
  496. ivnote = ET.SubElement(ivns, 'note')
  497. ivnote.set('pitch', str(note.pitch))
  498. ivnote.set('vel', str(int(note.ampl * 127.0)))
  499. ivnote.set('ampl', str(note.ampl))
  500. ivnote.set('time', str(note.abstime))
  501. ivnote.set('dur', str(note.duration))
  502. ivtext = ET.SubElement(ivstreams, 'stream', type='text')
  503. for tev in textstream:
  504. ivev = ET.SubElement(ivtext, 'text', time=str(tev.abstime), type=type(tev.ev).__name__, text=tev.ev.text)
  505. ivaux = ET.SubElement(ivstreams, 'stream')
  506. ivaux.set('type', 'aux')
  507. fw = midi.FileWriter()
  508. fw.RunningStatus = None # XXX Hack
  509. for mev in auxstream:
  510. ivev = ET.SubElement(ivaux, 'ev')
  511. ivev.set('time', str(mev.abstime))
  512. ivev.set('data', repr(fw.encode_midi_event(mev.ev)))
  513. print 'Done.'
  514. open(os.path.splitext(os.path.basename(fname))[0]+'.iv', 'w').write(ET.tostring(iv))