mkiv.py 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914
  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. '''
  7. import xml.etree.ElementTree as ET
  8. import midi
  9. import sys
  10. import os
  11. import optparse
  12. import math
  13. TRACKS = object()
  14. PROGRAMS = object()
  15. parser = optparse.OptionParser()
  16. parser.add_option('-s', '--channel-split', dest='chansplit', action='store_true', help='Split MIDI channels into independent tracks (as far as -T is concerned)')
  17. parser.add_option('-S', '--split-out', dest='chansfname', help='Store the split-format MIDI back into the specified file')
  18. 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)')
  19. parser.add_option('-T', '--track-split', dest='tracks', action='append_const', const=TRACKS, help='Ensure all tracks are on non-mutual streams')
  20. parser.add_option('-t', '--track', dest='tracks', action='append', help='Reserve an exclusive set of streams for certain conditions (try --help-conds)')
  21. parser.add_option('--help-conds', dest='help_conds', action='store_true', help='Print help on filter conditions for streams')
  22. parser.add_option('-a', '--artp', dest='artp', action='append', help='Add articulation parameters to matching events (try --help-artp)')
  23. parser.add_option('--help-artp', dest='help_artp', action='store_true', help='Print help on articulation filters for events')
  24. 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)')
  25. parser.add_option('-P', '--percussion', dest='perc', help='Which percussion standard to use to automatically filter to "perc" (GM, GM2, or none)')
  26. parser.add_option('-f', '--fuckit', dest='fuckit', action='store_true', help='Use the Python Error Steamroller when importing MIDIs (useful for extended formats)')
  27. parser.add_option('-v', '--verbose', dest='verbose', action='store_true', help='Be verbose; show important parts about the MIDI scheduling process')
  28. parser.add_option('-q', '--quiet', dest='quiet', action='store_true', help='Be quiet; don\'t log certain high-volume outputs')
  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('--string-res', dest='stringres', type='float', help='(Fractional) seconds by which to resolve string models (0 to disable)')
  38. parser.add_option('--string-max', dest='stringmax', type='int', help='Maximum number of events to generate per single input event')
  39. 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')
  40. 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')
  41. parser.add_option('--string-threshold', dest='stringthres', type='float', help='Amplitude (as fraction of original) at which point the string model event is terminated')
  42. parser.add_option('--tempo', dest='tempo', help='Adjust interpretation of tempo (try "f1"/"global", "f2"/"track")')
  43. 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)')
  44. parser.add_option('--slack', dest='slack', type='float', help='Inflate the duration of events by this much when scheduling them--this is for clients which need time to release their streams')
  45. parser.add_option('--real-slack', dest='real_slack', type='float', help='Add this much time to each real note with the intent of causing small overlaps, allowing clients to maintain phase consistency at high event rates')
  46. parser.add_option('--vol-pow', dest='vol_pow', type='float', help='Exponent to raise volume changes (adjusts energy per delta volume)')
  47. parser.add_option('-0', '--keep-empty', dest='keepempty', action='store_true', help='Keep (do not cull) events with 0 duration in the output file')
  48. parser.add_option('--no-text', dest='no_text', action='store_true', help='Disable text streams (useful for unusual text encodings)')
  49. parser.add_option('--no-wav', dest='no_wav', action='store_true', help='Disable processing of WAVE files')
  50. parser.add_option('--wav-winf', dest='wav_winf', help='Window function (on numpy) to use for FFT calculation')
  51. parser.add_option('--wav-frames', dest='wav_frames', type='int', help='Number of frames to read per FFT calculation')
  52. parser.add_option('--wav-window', dest='wav_window', type='int', help='Size of the FFT window')
  53. parser.add_option('--wav-streams', dest='wav_streams', type='int', help='Number of output streams to generate for the interval file')
  54. parser.add_option('--wav-log-width', dest='wav_log_width', type='float', help='Width of the correcting exponent--positive prefers high frequencies, negative prefers lower')
  55. parser.add_option('--wav-log-base', dest='wav_log_base', type='float', help='Base of the logarithm used to scale low frequencies')
  56. parser.add_option('--compression', dest='compression', help='Type of compression to use')
  57. parser.add_option('--compressions', dest='compressions', action='store_true', help='List compressions that are supported')
  58. parser.set_defaults(tracks=[], artp=[], 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, slack=0.0, real_slack=0.001, vol_pow=2, wav_winf='ones', wav_frames=512, wav_window=2048, wav_streams=16, wav_log_width=0.0, wav_log_base=2.0, compression='gzip')
  59. options, args = parser.parse_args()
  60. if options.tempo == 'f1':
  61. options.tempo == 'global'
  62. elif options.tempo == 'f2':
  63. options.tempo == 'track'
  64. if options.help_conds:
  65. print '''Filter conditions are used to route events to groups of streams.
  66. Every filter is an expression; internally, this expression is evaluated as the body of a "lambda ev: ".
  67. The "ev" object will be a MergeEvent with the following properties:
  68. -ev.tidx: the originating track index (starting at 0)
  69. -ev.abstime: the real time in seconds of this event relative to the beginning of playback
  70. -ev.bank: the selected bank (all bits)
  71. -ev.prog: the selected program
  72. -ev.mw: the modwheel value
  73. -ev.ev: a midi.NoteOnEvent:
  74. -ev.ev.pitch: the MIDI pitch
  75. -ev.ev.velocity: the MIDI velocity
  76. -ev.ev.channel: the MIDI channel
  77. All valid Python expressions are accepted. Take care to observe proper shell escaping.
  78. Specifying a -t <group>=<filter> will group all streams under a filter; if the <group> part is omitted, no group will be added.
  79. For example:
  80. mkiv -t bass=ev.ev.pitch<35 -t treble=ev.ev.pitch>75 -T -t ev.abstime<10
  81. will cause these groups to be made:
  82. -A group "bass" with all notes with pitch less than 35;
  83. -Of those not in "bass", a group in "treble" with pitch>75;
  84. -Of what is not yet consumed, a series of groups "trkN" where N is the track index (starting at 0), which consumes the rest.
  85. -An (unfortunately empty) unnamed group with events prior to ten real seconds.
  86. As can be seen, order of specification is important. Equally important is the location of -T, which should be at the end.
  87. NoteOffEvents are always matched to the stream which has their corresponding NoteOnEvent (in track, pitch, and channel), and so are
  88. not affected or observed by filters.
  89. If the filters specified are not a complete cover, an anonymous group will be created with no filter to contain the rest. If
  90. it is desired to force this group to have a name, use -t <group>=True. This should be placed at the end.
  91. -T behaves exactly as if:
  92. -t trk0=ev.tidx==0 -t trk1=ev.tidx==1 -t trk2=ev.tidx==2 [...]
  93. had been specified in its place, though it is automatically sized to the number of tracks. Similarly, -P operates as if
  94. -t prg31=ev.prog==31 -t prg81=ev.prog==81 [...]
  95. had been specified, again containing only the programs that were observed in the piece.
  96. Groups for which no streams are generated are not written to the resulting file.'''
  97. exit()
  98. if options.help_artp:
  99. print '''Articulation filters are used to attach articulations to various events.
  100. An articulation filter is a pair idx:expr, where idx is an integer and expr is a Python expression compiled as the tail of "lambda ev: ". `ev` is a DurationEvents possessing all the properties of MergeEvent (see --help-conds), as well as:
  101. - ev.duration: the duration, in seconds, of the note, as considered by the scheduler (including slack);
  102. - ev.real_duration: the duration, in seconds, that this note will play on a client;
  103. - ev.pitch: the MIDI pitch after resolving various modulation events (fractional);
  104. - ev.modwheel: the value of the MIDI modwheel at the time of this event;
  105. - ev.ampl: the amplitude (0.0-1.0) of this event.
  106. Each filter is applied in the order encountered on the command line. The expression may return None (in which case no parameter change is attached), or a float value, which is inserted before the event in the notestream, or a singleton of (float,), which will cause a global articulation (GARTS vs. LARTS--see client.py).
  107. This section is TODO.'''
  108. exit()
  109. COMPRESSIONS = {}
  110. def compression(name, desc='Not described.'):
  111. def inner(f):
  112. COMPRESSIONS[name] = (f, desc)
  113. return inner
  114. @compression('none', 'No compression (default .iv format)')
  115. def comp_none(bn):
  116. return open(bn + '.iv', 'wb')
  117. @compression('gzip', 'GZip compression (.ivz format)')
  118. def comp_gzip(bn):
  119. import gzip
  120. return gzip.open(bn + '.ivz', 'wb')
  121. @compression('bz2', 'BZip2 compression (.ivb format)')
  122. def comp_bz2(bn):
  123. import bz2
  124. return bz2.BZ2File(bn + '.ivb', 'w')
  125. if options.compressions:
  126. for nm, tp in COMPRESSIONS.iteritems():
  127. f, desc = tp
  128. print nm, ':', desc
  129. exit()
  130. if not args:
  131. parser.print_usage()
  132. exit()
  133. opener = COMPRESSIONS[options.compression][0]
  134. if options.fuckit:
  135. import fuckit
  136. midi.read_midifile = fuckit(midi.read_midifile)
  137. for fname in args:
  138. if fname.endswith('.wav') and not options.no_wav:
  139. import wave, struct
  140. import numpy as np
  141. wf = wave.open(fname, 'rb')
  142. chan, width, rate, frames, cmptype, cmpname = wf.getparams()
  143. print fname, ': WAV file, ', chan, 'channels,', width, 'sample width,', rate, 'sample rate,', frames, 'total frames,', cmpname
  144. sty = [None, np.int8, np.int16, None, np.int32][width]
  145. window = np.zeros((options.wav_window,))
  146. cnt = 0
  147. freqs = []
  148. amps = []
  149. winf = getattr(np, options.wav_winf)(options.wav_window)
  150. freqwin = np.fft.rfftfreq(options.wav_window, 1.0 / rate)[1:]
  151. logwin = np.logspace(-options.wav_log_width, options.wav_log_width, len(freqwin), True, options.wav_log_base)
  152. while True:
  153. sampsraw = wf.readframes(options.wav_frames)
  154. cnt += len(sampsraw) / (width * chan)
  155. if len(sampsraw) < options.wav_frames * chan * width:
  156. break
  157. window = np.concatenate((window, np.frombuffer(sampsraw, dtype=sty)[::chan] / float(1 << (width * 8 - 1))))[-options.wav_window:]
  158. spect = logwin * (np.abs(np.fft.rfft(winf * window)) / options.wav_window)[1:]
  159. amspect = np.argsort(spect)[:-(options.wav_streams + 1):-1]
  160. freqs.append(freqwin[amspect])
  161. amps.append(spect[amspect] * (options.wav_window / float(options.wav_streams)))
  162. print 'Processed', cnt, 'frames'
  163. period = options.wav_frames / float(rate)
  164. print 'Period:', period, 'sec'
  165. iv = ET.Element('iv', version='1', src=os.path.basename(fname), wav='1')
  166. ivstreams = ET.SubElement(iv, 'streams')
  167. streams = [ET.SubElement(ivstreams, 'stream', type='ns') for i in range(options.wav_streams)]
  168. t = 0
  169. for fs, ams in zip(freqs, amps):
  170. if options.debug:
  171. print 'Sample at t={}: {}'.format(t, list(zip(fs, ams)))
  172. for stm, frq, amp in zip(streams, fs, ams):
  173. ivnote = ET.SubElement(stm, 'note', pitch=str(12*math.log(frq/440.0, 2)+69), amp=str(amp), vel=str(int(amp * 127.0)), time=str(t), dur=str(period))
  174. t += period
  175. print 'Writing...'
  176. open(os.path.splitext(os.path.basename(fname))[0] + '.iv', 'wb').write(ET.tostring(iv, 'UTF-8'))
  177. print 'Done.'
  178. continue
  179. try:
  180. pat = midi.read_midifile(fname)
  181. except Exception:
  182. import traceback
  183. traceback.print_exc()
  184. print fname, ': Exception occurred, skipping...'
  185. continue
  186. if pat is None:
  187. print fname, ': Too fucked to continue'
  188. continue
  189. iv = ET.Element('iv')
  190. iv.set('version', '1.1')
  191. iv.set('src', os.path.basename(fname))
  192. print fname, ': MIDI format,', len(pat), 'tracks'
  193. if options.verbose:
  194. print fname, ': MIDI Parameters:', pat.resolution, 'PPQN,', pat.format, 'format'
  195. if options.chansplit:
  196. print 'Splitting channels...'
  197. old_pat = pat
  198. pat = midi.Pattern(resolution=old_pat.resolution)
  199. for track in old_pat:
  200. chan_map = {}
  201. last_abstick = {}
  202. absticks = 0
  203. for ev in track:
  204. absticks += ev.tick
  205. if isinstance(ev, midi.Event):
  206. tick = absticks - last_abstick.get(ev.channel, 0)
  207. last_abstick[ev.channel] = absticks
  208. if options.chanskeep:
  209. newev = ev.copy(tick = tick)
  210. else:
  211. newev = ev.copy(channel=1, tick = tick)
  212. chan_map.setdefault(ev.channel, midi.Track()).append(newev)
  213. else: # MetaEvent
  214. for trk in chan_map.itervalues():
  215. trk.append(ev)
  216. items = chan_map.items()
  217. items.sort(key=lambda pair: pair[0])
  218. for chn, trk in items:
  219. pat.append(trk)
  220. print 'Split', len(old_pat), 'tracks into', len(pat), 'tracks by channel'
  221. if options.chansfname:
  222. midi.write_midifile(options.chansfname, pat)
  223. ##### Merge events from all tracks into one master list, annotated with track and absolute times #####
  224. print 'Merging events...'
  225. class SortEvent(object):
  226. __slots__ = ['ev', 'tidx', 'abstick']
  227. def __init__(self, ev, tidx, abstick):
  228. self.ev = ev
  229. self.tidx = tidx
  230. self.abstick = abstick
  231. sorted_events = []
  232. for tidx, track in enumerate(pat):
  233. absticks = 0
  234. for ev in track:
  235. absticks += ev.tick
  236. sorted_events.append(SortEvent(ev, tidx, absticks))
  237. sorted_events.sort(key=lambda x: x.abstick)
  238. if options.tempo == 'global':
  239. bpm_at = [{0: 120}]
  240. else:
  241. bpm_at = [{0: 120} for i in pat]
  242. print 'Computing tempos...'
  243. for sev in sorted_events:
  244. if isinstance(sev.ev, midi.SetTempoEvent):
  245. if options.debug:
  246. print fname, ': SetTempo at', sev.abstick, 'to', sev.ev.bpm, ':', sev.ev
  247. bpm_at[sev.tidx if options.tempo == 'track' else 0][sev.abstick] = sev.ev.bpm
  248. if options.verbose:
  249. print fname, ': Events:', len(sorted_events)
  250. print fname, ': Resolved global BPM:', bpm_at
  251. if options.debug:
  252. if options.tempo == 'track':
  253. for tidx, bpms in enumerate(bpm_at):
  254. print fname, ': Tempos in track', tidx
  255. btimes = bpms.keys()
  256. for i in range(len(btimes) - 1):
  257. fev = filter(lambda sev: sev.tidx == tidx and sev.abstick >= btimes[i] and sev.abstick < btimes[i+1], sorted_events)
  258. print fname, ': BPM partition', i, 'contains', len(fev), 'events'
  259. else:
  260. btimes = bpm_at[0].keys()
  261. for i in range(len(btimes) - 1):
  262. fev = filter(lambda sev: sev.abstick >= btimes[i] and sev.abstick < btimes[i+1], sorted_events)
  263. print fname, ': BPM partition', i, 'contains', len(fev), 'events'
  264. def at2rt(abstick, bpms):
  265. bpm_segs = bpms.items()
  266. bpm_segs.sort(key=lambda pair: pair[0])
  267. bpm_segs = filter(lambda pair: pair[0] <= abstick, bpm_segs)
  268. rt = 0
  269. atick = 0
  270. if not bpm_segs:
  271. rt = 0
  272. else:
  273. ctick, bpm = bpm_segs[0]
  274. rt = (60.0 * ctick) / (bpm * pat.resolution)
  275. for idx in range(1, len(bpm_segs)):
  276. dt = bpm_segs[idx][0] - bpm_segs[idx-1][0]
  277. bpm = bpm_segs[idx-1][1]
  278. rt += (60.0 * dt) / (bpm * pat.resolution)
  279. if not bpm_segs:
  280. bpm = 120
  281. ctick = 0
  282. else:
  283. ctick, bpm = bpm_segs[-1]
  284. if options.debug:
  285. print 'seg through', bpm_segs, 'final seg', (abstick - ctick, bpm)
  286. rt += (60.0 * (abstick - ctick)) / (bpm * pat.resolution)
  287. return rt
  288. class MergeEvent(object):
  289. __slots__ = ['ev', 'tidx', 'abstime', 'bank', 'prog', 'mw', 'par']
  290. def __init__(self, ev, tidx, abstime, bank=0, prog=0, mw=0, par=None):
  291. self.ev = ev
  292. self.tidx = tidx
  293. self.abstime = abstime
  294. self.bank = bank
  295. self.prog = prog
  296. self.mw = mw
  297. self.par = par
  298. def copy(self, **kwargs):
  299. args = {'ev': self.ev, 'tidx': self.tidx, 'abstime': self.abstime, 'bank': self.bank, 'prog': self.prog, 'mw': self.mw, 'par': self.par}
  300. args.update(kwargs)
  301. return MergeEvent(**args)
  302. def __repr__(self):
  303. return '<ME %r in %d on (%d:%d) MW:%d @%f par %r>'%(self.ev, self.tidx, self.bank, self.prog, self.mw, self.abstime, None if self.par is None else id(self.par))
  304. vol_at = [[{0: 0x3FFF} for i in range(16)] for j in range(len(pat))]
  305. events = []
  306. cur_mw = [[0 for i in range(16)] for j in range(len(pat))]
  307. cur_bank = [[0 for i in range(16)] for j in range(len(pat))]
  308. cur_prog = [[0 for i in range(16)] for j in range(len(pat))]
  309. chg_mw = [[0 for i in range(16)] for j in range(len(pat))]
  310. chg_bank = [[0 for i in range(16)] for j in range(len(pat))]
  311. chg_prog = [[0 for i in range(16)] for j in range(len(pat))]
  312. chg_vol = [[0 for i in range(16)] for j in range(len(pat))]
  313. ev_cnts = [[0 for i in range(16)] for j in range(len(pat))]
  314. tnames = [''] * len(pat)
  315. progs = set([0])
  316. for tidx, track in enumerate(pat):
  317. abstime = 0
  318. absticks = 0
  319. lastbpm = 120
  320. for ev in track:
  321. absticks += ev.tick
  322. abstime = at2rt(absticks, bpm_at[tidx if options.tempo == 'track' else 0])
  323. if options.debug:
  324. print 'tick', absticks, 'realtime', abstime
  325. if isinstance(ev, midi.TrackNameEvent):
  326. tnames[tidx] = ev.text
  327. if isinstance(ev, midi.ProgramChangeEvent):
  328. cur_prog[tidx][ev.channel] = ev.value
  329. progs.add(ev.value)
  330. chg_prog[tidx][ev.channel] += 1
  331. elif isinstance(ev, midi.ControlChangeEvent):
  332. if ev.control == 0: # Bank -- MSB
  333. cur_bank[tidx][ev.channel] = (0x3F & cur_bank[tidx][ev.channel]) | (ev.value << 7)
  334. chg_bank[tidx][ev.channel] += 1
  335. elif ev.control == 32: # Bank -- LSB
  336. cur_bank[tidx][ev.channel] = (0x3F80 & cur_bank[tidx][ev.channel]) | ev.value
  337. chg_bank[tidx][ev.channel] += 1
  338. elif ev.control == 1: # ModWheel -- MSB
  339. cur_mw[tidx][ev.channel] = (0x3F & cur_mw[tidx][ev.channel]) | (ev.value << 7)
  340. chg_mw[tidx][ev.channel] += 1
  341. elif ev.control == 33: # ModWheel -- LSB
  342. cur_mw[tidx][ev.channel] = (0x3F80 & cur_mw[tidx][ev.channel]) | ev.value
  343. chg_mw[tidx][ev.channel] += 1
  344. elif ev.control == 7: # Volume -- MSB
  345. lvtime, lvol = sorted(vol_at[tidx][ev.channel].items(), key = lambda pair: pair[0])[-1]
  346. vol_at[tidx][ev.channel][abstime] = (0x3F & lvol) | (ev.value << 7)
  347. chg_vol[tidx][ev.channel] += 1
  348. elif ev.control == 39: # Volume -- LSB
  349. lvtime, lvol = sorted(vol_at[tidx][ev.channel].items(), key = lambda pair: pair[0])[-1]
  350. vol_at[tidx][ev.channel][abstime] = (0x3F80 & lvol) | ev.value
  351. chg_vol[tidx][ev.channel] += 1
  352. events.append(MergeEvent(ev, tidx, abstime, cur_bank[tidx][ev.channel], cur_prog[tidx][ev.channel], cur_mw[tidx][ev.channel], events[-1] if events else None))
  353. ev_cnts[tidx][ev.channel] += 1
  354. elif isinstance(ev, midi.MetaEventWithText):
  355. events.append(MergeEvent(ev, tidx, abstime))
  356. elif isinstance(ev, midi.Event):
  357. if isinstance(ev, midi.NoteOnEvent) and ev.velocity == 0:
  358. ev.__class__ = midi.NoteOffEvent #XXX Oww
  359. events.append(MergeEvent(ev, tidx, abstime, cur_bank[tidx][ev.channel], cur_prog[tidx][ev.channel], cur_mw[tidx][ev.channel]))
  360. ev_cnts[tidx][ev.channel] += 1
  361. print 'Track name, event count, final banks, bank changes, final programs, program changes, final modwheel, modwheel changes, volume changes:'
  362. for tidx, tname in enumerate(tnames):
  363. 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]))
  364. print 'All programs observed:', progs
  365. print 'Sorting events...'
  366. events.sort(key = lambda ev: ev.abstime)
  367. ##### Use merged events to construct a set of streams with non-overlapping durations #####
  368. print 'Generating streams...'
  369. class DurationEvent(MergeEvent):
  370. __slots__ = ['duration', 'real_duration', 'pitch', 'modwheel', 'ampl']
  371. def __init__(self, me, pitch, ampl, dur, modwheel=0, par=None):
  372. MergeEvent.__init__(self, me.ev, me.tidx, me.abstime, me.bank, me.prog, me.mw, par)
  373. self.pitch = pitch
  374. self.ampl = ampl
  375. self.duration = dur
  376. self.real_duration = dur
  377. self.modwheel = modwheel
  378. def __repr__(self):
  379. return '<DE %s P:%f A:%f D:%f W:%f>'%(MergeEvent.__repr__(self), self.pitch, self.ampl, self.duration, self.modwheel)
  380. class NoteStream(object):
  381. __slots__ = ['history', 'active', 'bentpitch', 'modwheel', 'prevparent']
  382. def __init__(self):
  383. self.history = []
  384. self.active = None
  385. self.bentpitch = None
  386. self.modwheel = 0
  387. self.prevparent = None
  388. def IsActive(self):
  389. return self.active is not None
  390. def Activate(self, mev, bentpitch=None, modwheel=None, parent=None):
  391. if bentpitch is None:
  392. bentpitch = mev.ev.pitch
  393. self.active = mev
  394. self.bentpitch = bentpitch
  395. if modwheel is not None:
  396. self.modwheel = modwheel
  397. self.prevparent = parent
  398. def Deactivate(self, mev):
  399. self.history.append(DurationEvent(self.active, self.bentpitch, self.active.ev.velocity / 127.0, mev.abstime - self.active.abstime, self.modwheel, self.prevparent))
  400. self.active = None
  401. self.bentpitch = None
  402. self.modwheel = 0
  403. self.prevparent = None
  404. def WouldDeactivate(self, mev):
  405. if not self.IsActive():
  406. return False
  407. if isinstance(mev.ev, midi.NoteOffEvent):
  408. return mev.ev.pitch == self.active.ev.pitch and mev.tidx == self.active.tidx and mev.ev.channel == self.active.ev.channel
  409. if isinstance(mev.ev, midi.PitchWheelEvent):
  410. return mev.tidx == self.active.tidx and mev.ev.channel == self.active.ev.channel
  411. if isinstance(mev.ev, midi.ControlChangeEvent):
  412. return mev.tidx == self.active.tidx and mev.ev.channel == self.active.ev.channel
  413. raise TypeError('Tried to deactivate with bad type %r'%(type(mev.ev),))
  414. class NSGroup(object):
  415. __slots__ = ['streams', 'filter', 'name']
  416. def __init__(self, filter=None, name=None):
  417. self.streams = []
  418. self.filter = (lambda mev: True) if filter is None else filter
  419. self.name = name
  420. def Accept(self, mev):
  421. if not self.filter(mev):
  422. return False
  423. for stream in self.streams:
  424. if not stream.IsActive():
  425. stream.Activate(mev)
  426. break
  427. else:
  428. stream = NoteStream()
  429. self.streams.append(stream)
  430. stream.Activate(mev)
  431. return True
  432. notegroups = []
  433. auxstream = []
  434. textstream = []
  435. if options.perc and options.perc != 'none':
  436. if options.perc == 'GM':
  437. notegroups.append(NSGroup(filter = lambda mev: mev.ev.channel == 9, name='perc'))
  438. elif options.perc == 'GM2':
  439. notegroups.append(NSGroup(filter = lambda mev: mev.bank == 15360, name='perc'))
  440. else:
  441. print 'Unrecognized --percussion option %r; should be GM, GM2, or none'%(options.perc,)
  442. for spec in options.tracks:
  443. if spec is TRACKS:
  444. for tidx in xrange(len(pat)):
  445. notegroups.append(NSGroup(filter = lambda mev, tidx=tidx: mev.tidx == tidx, name = 'trk%d'%(tidx,)))
  446. elif spec is PROGRAMS:
  447. for prog in progs:
  448. notegroups.append(NSGroup(filter = lambda mev, prog=prog: mev.prog == prog, name = 'prg%d'%(prog,)))
  449. else:
  450. if '=' in spec:
  451. name, _, spec = spec.partition('=')
  452. else:
  453. name = None
  454. notegroups.append(NSGroup(filter = eval("lambda ev: "+spec), name = name))
  455. if options.verbose:
  456. print 'Initial group mappings:'
  457. for group in notegroups:
  458. print ('<anonymous>' if group.name is None else group.name)
  459. for mev in events:
  460. if isinstance(mev.ev, midi.MetaEventWithText):
  461. textstream.append(mev)
  462. elif isinstance(mev.ev, midi.NoteOnEvent):
  463. for group in notegroups:
  464. if group.Accept(mev):
  465. break
  466. else:
  467. group = NSGroup()
  468. group.Accept(mev)
  469. notegroups.append(group)
  470. elif isinstance(mev.ev, midi.NoteOffEvent):
  471. for group in notegroups:
  472. found = False
  473. for stream in group.streams:
  474. if stream.WouldDeactivate(mev):
  475. stream.Deactivate(mev)
  476. found = True
  477. break
  478. if found:
  479. break
  480. else:
  481. if not options.quiet:
  482. print 'WARNING: Did not match %r with any stream deactivation.'%(mev,)
  483. if options.verbose:
  484. print ' Current state:'
  485. for group in notegroups:
  486. print ' Group %r:'%(group.name,)
  487. for stream in group.streams:
  488. print ' Stream: %r'%(stream.active,)
  489. elif options.deviation > 0 and isinstance(mev.ev, midi.PitchWheelEvent):
  490. found = False
  491. for group in notegroups:
  492. for stream in group.streams:
  493. if stream.WouldDeactivate(mev):
  494. old = stream.active
  495. base = old.copy(abstime=mev.abstime)
  496. stream.Deactivate(mev)
  497. stream.Activate(base, base.ev.pitch + options.deviation * (mev.ev.pitch / float(0x2000)), parent=old)
  498. found = True
  499. if not found:
  500. if not options.quiet:
  501. print 'WARNING: Did not find any matching active streams for %r'%(mev,)
  502. if options.verbose:
  503. print ' Current state:'
  504. for group in notegroups:
  505. print ' Group %r:'%(group.name,)
  506. for stream in group.streams:
  507. print ' Stream: %r'%(stream.active,)
  508. elif options.modres > 0 and isinstance(mev.ev, midi.ControlChangeEvent):
  509. found = False
  510. for group in notegroups:
  511. for stream in group.streams:
  512. if stream.WouldDeactivate(mev):
  513. old = stream.active
  514. base = old.copy(abstime=mev.abstime)
  515. stream.Deactivate(mev)
  516. stream.Activate(base, stream.bentpitch, mev.mw, parent=old)
  517. found = True
  518. if not found:
  519. if not options.quiet:
  520. print 'WARNING: Did not find any matching active streams for %r'%(mev,)
  521. if options.verbose:
  522. print ' Current state:'
  523. for group in notegroups:
  524. print ' Group %r:'%(group.name,)
  525. for stream in group.streams:
  526. print ' Stream: %r'%(stream.active,)
  527. else:
  528. auxstream.append(mev)
  529. lastabstime = events[-1].abstime
  530. for group in notegroups:
  531. for ns in group.streams:
  532. if ns.IsActive():
  533. print 'WARNING: Active notes at end of playback.'
  534. ns.Deactivate(MergeEvent(ns.active, ns.active.tidx, lastabstime))
  535. if options.slack > 0:
  536. print 'Adding slack time...'
  537. slack_evs = []
  538. for group in notegroups:
  539. for ns in group.streams:
  540. for dev in ns.history:
  541. dev.duration += options.slack
  542. slack_evs.append(dev)
  543. print 'Resorting all streams...'
  544. for group in notegroups:
  545. group.streams = []
  546. for dev in slack_evs:
  547. for group in notegroups:
  548. if not group.filter(dev):
  549. continue
  550. for ns in group.streams:
  551. if dev.abstime >= ns.history[-1].abstime + ns.history[-1].duration:
  552. ns.history.append(dev)
  553. break
  554. else:
  555. group.streams.append(NoteStream())
  556. group.streams[-1].history.append(dev)
  557. break
  558. else:
  559. print 'WARNING: No stream accepts event', dev
  560. if options.modres > 0:
  561. print 'Resolving modwheel events...'
  562. ev_cnt = 0
  563. for group in notegroups:
  564. for ns in group.streams:
  565. i = 0
  566. while i < len(ns.history):
  567. dev = ns.history[i]
  568. if dev.modwheel > 0:
  569. realpitch = dev.pitch
  570. realamp = dev.ampl
  571. mwamp = float(dev.modwheel) / 0x3FFF
  572. dt = 0.0
  573. origtime = dev.abstime
  574. events = []
  575. while dt < dev.duration:
  576. dev.abstime = origtime + dt
  577. if options.modcont:
  578. t = origtime
  579. else:
  580. t = dt
  581. 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, dev))
  582. dt += options.modres
  583. ns.history[i:i+1] = events
  584. i += len(events)
  585. ev_cnt += len(events)
  586. if options.verbose:
  587. print 'Event', i, 'note', dev, 'in group', group.name, 'resolved to', len(events), 'events'
  588. if options.debug:
  589. for ev in events:
  590. print '\t', ev
  591. else:
  592. i += 1
  593. print '...resolved', ev_cnt, 'events'
  594. if options.stringres:
  595. print 'Resolving string models...'
  596. st_cnt = sum(sum(len(ns.history) for ns in group.streams) for group in notegroups)
  597. in_cnt = 0
  598. ex_cnt = 0
  599. ev_cnt = 0
  600. dev_grps = []
  601. for group in notegroups:
  602. for ns in group.streams:
  603. i = 0
  604. while i < len(ns.history):
  605. dev = ns.history[i]
  606. ntime = float('inf')
  607. if i + 1 < len(ns.history):
  608. ntime = ns.history[i+1].abstime
  609. dt = 0.0
  610. ampf = 1.0
  611. origtime = dev.abstime
  612. events = []
  613. while dt < dev.duration and ampf * dev.ampl >= options.stringthres:
  614. dev.abstime = origtime + dt
  615. events.append(DurationEvent(dev, dev.pitch, ampf * dev.ampl, min(options.stringres, dev.duration - dt), dev.modwheel, dev))
  616. if len(events) > options.stringmax:
  617. print 'WARNING: Exceeded maximum string model events for event', i
  618. if options.verbose:
  619. print 'Final ampf', ampf, 'dt', dt
  620. break
  621. ampf *= options.stringrateon ** options.stringres
  622. dt += options.stringres
  623. in_cnt += 1
  624. dt = dev.duration
  625. while ampf * dev.ampl >= options.stringthres:
  626. dev.abstime = origtime + dt
  627. events.append(DurationEvent(dev, dev.pitch, ampf * dev.ampl, options.stringres, dev.modwheel, dev))
  628. if len(events) > options.stringmax:
  629. print 'WARNING: Exceeded maximum string model events for event', i
  630. if options.verbose:
  631. print 'Final ampf', ampf, 'dt', dt
  632. break
  633. ampf *= options.stringrateoff ** options.stringres
  634. dt += options.stringres
  635. ex_cnt += 1
  636. if events:
  637. for j in xrange(len(events) - 1):
  638. cur, next = events[j], events[j + 1]
  639. if abs(cur.abstime + cur.duration - next.abstime) > options.epsilon:
  640. print 'WARNING: String model events cur: ', cur, 'next:', next, 'have gap/overrun of', next.abstime - (cur.abstime + cur.duration)
  641. dev_grps.append(events)
  642. else:
  643. print 'WARNING: Event', i, 'note', dev, ': No events?'
  644. if options.verbose:
  645. print 'Event', i, 'note', dev, 'in group', group.name, 'resolved to', len(events), 'events'
  646. if options.debug:
  647. for ev in events:
  648. print '\t', ev
  649. i += 1
  650. ev_cnt += len(events)
  651. print '...resolved', ev_cnt, 'events (+', ev_cnt - st_cnt, ',', in_cnt, 'inside', ex_cnt, 'extra), resorting streams...'
  652. for group in notegroups:
  653. group.streams = []
  654. dev_grps.sort(key = lambda evg: evg[0].abstime)
  655. for devgr in dev_grps:
  656. dev = devgr[0]
  657. for group in notegroups:
  658. if group.filter(dev):
  659. grp = group
  660. break
  661. else:
  662. grp = NSGroup()
  663. notegroups.append(grp)
  664. for ns in grp.streams:
  665. if not ns.history:
  666. ns.history.extend(devgr)
  667. break
  668. last = ns.history[-1]
  669. if dev.abstime >= last.abstime + last.duration - 1e-3:
  670. ns.history.extend(devgr)
  671. break
  672. else:
  673. ns = NoteStream()
  674. grp.streams.append(ns)
  675. ns.history.extend(devgr)
  676. scnt = 0
  677. for group in notegroups:
  678. for ns in group.streams:
  679. scnt += 1
  680. print 'Final sort:', len(notegroups), 'groups with', scnt, 'streams'
  681. if not options.keepempty:
  682. print 'Culling empty events...'
  683. ev_cnt = 0
  684. for group in notegroups:
  685. for ns in group.streams:
  686. i = 0
  687. while i < len(ns.history):
  688. if ns.history[i].duration == 0.0:
  689. del ns.history[i]
  690. ev_cnt += 1
  691. else:
  692. i += 1
  693. print '...culled', ev_cnt, 'events'
  694. print 'Culling empty streams...'
  695. st_cnt = 0
  696. for group in notegroups:
  697. torem = set()
  698. for ns in group.streams:
  699. if not ns.history:
  700. torem.add(ns)
  701. st_cnt += len(torem)
  702. for rem in torem:
  703. group.streams.remove(rem)
  704. print '...culled', st_cnt, 'empty streams'
  705. if options.verbose:
  706. print 'Final group mappings:'
  707. for group in notegroups:
  708. print ('<anonymous>' if group.name is None else group.name), '<=', '(', len(group.streams), 'streams)'
  709. print 'Final volume resolution...'
  710. for group in notegroups:
  711. for ns in group.streams:
  712. for ev in ns.history:
  713. t, vol = sorted(filter(lambda pair: pair[0] <= ev.abstime, vol_at[ev.tidx][ev.ev.channel].items()), key=lambda pair: pair[0])[-1]
  714. ev.ampl *= (float(vol) / 0x3FFF) ** options.vol_pow
  715. print 'Checking consistency...'
  716. for group in notegroups:
  717. if options.verbose:
  718. print 'Group', '<None>' if group.name is None else group.name, 'with', len(group.streams), 'streams...',
  719. ecnt = 0
  720. for ns in group.streams:
  721. for i in xrange(len(ns.history) - 1):
  722. cur, next = ns.history[i], ns.history[i + 1]
  723. if cur.abstime + cur.duration > next.abstime + options.epsilon:
  724. print 'WARNING: event', i, 'collides with next event (@', cur.abstime, '+', cur.duration, 'next @', next.abstime, ';', next.abstime - (cur.abstime + cur.duration), 'overlap)'
  725. ecnt += 1
  726. if cur.abstime > next.abstime:
  727. print 'WARNING: event', i + 1, 'out of sort order (@', cur.abstime, 'next @', next.abstime, ';', cur.abstime - next.abstime, 'underlap)'
  728. ecnt += 1
  729. if options.verbose:
  730. if ecnt > 0:
  731. print '...', ecnt, 'errors occured'
  732. else:
  733. print 'ok'
  734. print 'Applying articulation parameters...'
  735. class Articulation(object):
  736. __slots__ = ['tm', 'index', 'value', 'global_']
  737. def __init__(self, tm, index, value, global_=False):
  738. self.tm = tm
  739. self.index = index
  740. self.value = value
  741. self.global_ = global_
  742. for artpex in options.artp:
  743. cnt = 0
  744. idx, _, artfex = artpex.partition(':')
  745. idx = int(idx)
  746. artf = eval('lambda ev: '+artfex)
  747. for group in notegroups:
  748. for ns in group.streams:
  749. i = 0
  750. while i < len(ns.history):
  751. ev = ns.history[i]
  752. if ev.__class__ is Articulation:
  753. i += 1
  754. continue
  755. val = artf(ev)
  756. if val is not None:
  757. global_ = False
  758. try:
  759. val = val[0]
  760. except TypeError:
  761. pass
  762. else:
  763. global_ = True
  764. ns.history.insert(i, Articulation(ev.abstime, idx, val, global_))
  765. i += 2
  766. cnt += 1
  767. else:
  768. i += 1
  769. print 'Articulation parameter', idx, 'attached to', cnt, 'events'
  770. print 'Generated %d streams in %d groups'%(sum(map(lambda x: len(x.streams), notegroups)), len(notegroups))
  771. print 'Playtime:', lastabstime, 'seconds'
  772. ##### Write to XML and exit #####
  773. ivmeta = ET.SubElement(iv, 'meta')
  774. abstime = 0
  775. prevticks = 0
  776. prev_bpm = 120
  777. for tidx, bpms in enumerate(bpm_at):
  778. ivbpms = ET.SubElement(ivmeta, 'bpms', track=str(tidx))
  779. for absticks, bpm in sorted(bpms.items(), key = lambda pair: pair[0]):
  780. abstime += ((absticks - prevticks) * 60.0) / (prev_bpm * pat.resolution)
  781. prevticks = absticks
  782. ivbpm = ET.SubElement(ivbpms, 'bpm')
  783. ivbpm.set('bpm', str(bpm))
  784. ivbpm.set('ticks', str(absticks))
  785. ivbpm.set('time', str(abstime))
  786. ivstreams = ET.SubElement(iv, 'streams')
  787. for group in notegroups:
  788. for ns in group.streams:
  789. ivns = ET.SubElement(ivstreams, 'stream')
  790. ivns.set('type', 'ns')
  791. if group.name is not None:
  792. ivns.set('group', group.name)
  793. for note in ns.history:
  794. if note.__class__ is Articulation:
  795. ivart = ET.SubElement(ivns, 'art', time=str(note.tm), index=str(note.index), value=str(note.value))
  796. if note.global_:
  797. ivart.set('global', '1')
  798. continue
  799. ivnote = ET.SubElement(ivns, 'note', id=str(id(note)))
  800. ivnote.set('pitch', str(note.pitch))
  801. ivnote.set('vel', str(int(note.ampl * 127.0)))
  802. ivnote.set('ampl', str(note.ampl))
  803. ivnote.set('time', str(note.abstime))
  804. ivnote.set('dur', str(note.real_duration + options.real_slack))
  805. if note.par:
  806. ivnote.set('par', str(id(note.par)))
  807. if not options.no_text:
  808. ivtext = ET.SubElement(ivstreams, 'stream', type='text')
  809. for tev in textstream:
  810. text = tev.ev.text
  811. try:
  812. text = text.decode('utf8')
  813. except UnicodeDecodeError:
  814. text = 'base64:' + text.encode('base64')
  815. ivev = ET.SubElement(ivtext, 'text', time=str(tev.abstime), type=type(tev.ev).__name__, text=text)
  816. ivaux = ET.SubElement(ivstreams, 'stream')
  817. ivaux.set('type', 'aux')
  818. fw = midi.FileWriter()
  819. fw.RunningStatus = None # XXX Hack
  820. for mev in auxstream:
  821. ivev = ET.SubElement(ivaux, 'ev')
  822. ivev.set('time', str(mev.abstime))
  823. ivev.set('data', repr(fw.encode_midi_event(mev.ev)))
  824. ivargs = ET.SubElement(ivmeta, 'args')
  825. ivargs.text = ' '.join('%r' % (i,) for i in sys.argv[1:])
  826. ivapp = ET.SubElement(ivmeta, 'app')
  827. ivapp.text = 'mkiv'
  828. print 'Done; writing with', options.compression, 'compression...'
  829. txt = ET.tostring(iv, 'UTF-8')
  830. opener(os.path.splitext(os.path.basename(fname))[0]).write(txt)