mkiv.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  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. TRACKS = object()
  16. parser = optparse.OptionParser()
  17. parser.add_option('-s', '--channel-split', dest='chansplit', action='store_true', help='Split MIDI channels into independent tracks (as far as -T is concerned)')
  18. parser.add_option('-S', '--split-out', dest='chansfname', help='Store the split-format MIDI back into the specified file')
  19. 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)')
  20. parser.add_option('-T', '--track-split', dest='tracks', action='append_const', const=TRACKS, help='Ensure all tracks are on non-mutual streams')
  21. parser.add_option('-t', '--track', dest='tracks', action='append', help='Reserve an exclusive set of streams for certain conditions (try --help-conds)')
  22. parser.add_option('--help-conds', dest='help_conds', action='store_true', help='Print help on filter conditions for streams')
  23. parser.add_option('-P', '--percussion', dest='perc', help='Which percussion standard to use to automatically filter to "perc" (GM, GM2, or none)')
  24. parser.add_option('-f', '--fuckit', dest='fuckit', action='store_true', help='Use the Python Error Steamroller when importing MIDIs (useful for extended formats)')
  25. parser.add_option('-n', '--target-num', dest='repeaterNumber', type='int', help='Target count of devices')
  26. parser.add_option('-v', '--verbose', dest='verbose', action='store_true', help='Be verbose; show important parts about the MIDI scheduling process')
  27. parser.add_option('-d', '--debug', dest='debug', action='store_true', help='Debugging output; show excessive output about the MIDI scheduling process')
  28. parser.set_defaults(tracks=[], repeaterNumber=1, perc='GM')
  29. options, args = parser.parse_args()
  30. if options.help_conds:
  31. print '''Filter conditions are used to route events to groups of streams.
  32. Every filter is an expression; internally, this expression is evaluated as the body of a "lambda ev: ".
  33. The "ev" object will be a MergeEvent with the following properties:
  34. -ev.tidx: the originating track index (starting at 0)
  35. -ev.abstime: the real time in seconds of this event relative to the beginning of playback
  36. -ev.bank: the selected bank (all bits)
  37. -ev.prog: the selected program
  38. -ev.ev: a midi.NoteOnEvent:
  39. -ev.ev.pitch: the MIDI pitch
  40. -ev.ev.velocity: the MIDI velocity
  41. -ev.ev.channel: the MIDI channel
  42. All valid Python expressions are accepted. Take care to observe proper shell escaping.
  43. Specifying a -t <group>=<filter> will group all streams under a filter; if the <group> part is omitted, no group will be added.
  44. For example:
  45. mkiv -t bass=ev.ev.pitch<35 -t treble=ev.ev.pitch>75 -T -t ev.abstime<10
  46. will cause these groups to be made:
  47. -A group "bass" with all notes with pitch less than 35;
  48. -Of those not in "bass", a group in "treble" with pitch>75;
  49. -Of what is not yet consumed, a series of groups "trkN" where N is the track index (starting at 0), which consumes the rest.
  50. -An (unfortunately empty) unnamed group with events prior to ten real seconds.
  51. As can be seen, order of specification is important. Equally important is the location of -T, which should be at the end.
  52. NoteOffEvents are always matched to the stream which has their corresponding NoteOnEvent (in track and pitch), and so are
  53. not affected or observed by filters.
  54. If the filters specified are not a complete cover, an anonymous group will be created with no filter to contain the rest. If
  55. it is desired to force this group to have a name, use -t <group>=True.'''
  56. exit()
  57. if not args:
  58. parser.print_usage()
  59. exit()
  60. if options.fuckit:
  61. import fuckit
  62. midi.read_midifile = fuckit(midi.read_midifile)
  63. for fname in args:
  64. try:
  65. pat = midi.read_midifile(fname)
  66. except Exception:
  67. import traceback
  68. traceback.print_exc()
  69. print fname, ': Exception occurred, skipping...'
  70. continue
  71. if pat is None:
  72. print fname, ': Too fucked to continue'
  73. continue
  74. iv = ET.Element('iv')
  75. iv.set('version', '1')
  76. iv.set('src', os.path.basename(fname))
  77. print fname, ': MIDI format,', len(pat), 'tracks'
  78. if options.verbose:
  79. print fname, ': MIDI Parameters:', pat.resolution, 'PPQN,', pat.format, 'format'
  80. if options.chansplit:
  81. print 'Splitting channels...'
  82. old_pat = pat
  83. pat = midi.Pattern(resolution=old_pat.resolution)
  84. for track in old_pat:
  85. chan_map = {}
  86. last_abstick = {}
  87. absticks = 0
  88. for ev in track:
  89. absticks += ev.tick
  90. if isinstance(ev, midi.Event):
  91. tick = absticks - last_abstick.get(ev.channel, 0)
  92. last_abstick[ev.channel] = absticks
  93. if options.chanskeep:
  94. newev = ev.copy(tick = tick)
  95. else:
  96. newev = ev.copy(channel=1, tick = tick)
  97. chan_map.setdefault(ev.channel, midi.Track()).append(newev)
  98. else: # MetaEvent
  99. for trk in chan_map.itervalues():
  100. trk.append(ev)
  101. items = chan_map.items()
  102. items.sort(key=lambda pair: pair[0])
  103. for chn, trk in items:
  104. pat.append(trk)
  105. print 'Split', len(old_pat), 'tracks into', len(pat), 'tracks by channel'
  106. if options.chansfname:
  107. midi.write_midifile(options.chansfname, pat)
  108. ##### Merge events from all tracks into one master list, annotated with track and absolute times #####
  109. print 'Merging events...'
  110. class SortEvent(object):
  111. __slots__ = ['ev', 'tidx', 'abstick']
  112. def __init__(self, ev, tidx, abstick):
  113. self.ev = ev
  114. self.tidx = tidx
  115. self.abstick = abstick
  116. sorted_events = []
  117. for tidx, track in enumerate(pat):
  118. absticks = 0
  119. for ev in track:
  120. absticks += ev.tick
  121. sorted_events.append(SortEvent(ev, tidx, absticks))
  122. sorted_events.sort(key=lambda x: x.abstick)
  123. bpm_at = {0: 120}
  124. for sev in sorted_events:
  125. if isinstance(sev.ev, midi.SetTempoEvent):
  126. if options.debug:
  127. print fname, ': SetTempo at', sev.abstick, 'to', sev.ev.bpm, ':', sev.ev
  128. bpm_at[sev.abstick] = sev.ev.bpm
  129. if options.verbose:
  130. print fname, ': Events:', len(sorted_events)
  131. print fname, ': Resolved global BPM:', bpm_at
  132. if options.debug:
  133. btimes = bpm_at.keys()
  134. for i in range(len(btimes) - 1):
  135. fev = filter(lambda sev: sev.abstick >= btimes[i] and sev.abstick < btimes[i+1], sorted_events)
  136. print fname, ': BPM partition', i, 'contains', len(fev), 'events'
  137. class MergeEvent(object):
  138. __slots__ = ['ev', 'tidx', 'abstime', 'bank', 'prog']
  139. def __init__(self, ev, tidx, abstime, bank, prog):
  140. self.ev = ev
  141. self.tidx = tidx
  142. self.abstime = abstime
  143. self.bank = bank
  144. self.prog = prog
  145. def __repr__(self):
  146. return '<ME %r in %d @%f>'%(self.ev, self.tidx, self.abstime)
  147. events = []
  148. cur_bank = [[0 for i in range(16)] for j in range(len(pat))]
  149. cur_prog = [[0 for i in range(16)] for j in range(len(pat))]
  150. chg_bank = [[0 for i in range(16)] for j in range(len(pat))]
  151. chg_prog = [[0 for i in range(16)] for j in range(len(pat))]
  152. ev_cnts = [[0 for i in range(16)] for j in range(len(pat))]
  153. tnames = [''] * len(pat)
  154. for tidx, track in enumerate(pat):
  155. abstime = 0
  156. absticks = 0
  157. for ev in track:
  158. bpm = filter(lambda pair: pair[0] <= absticks, sorted(bpm_at.items(), key=lambda pair: pair[0]))[-1][1]
  159. if options.debug:
  160. print ev, ': bpm=', bpm
  161. absticks += ev.tick
  162. if isinstance(ev, midi.ProgramChangeEvent):
  163. cur_prog[tidx][ev.channel] = ev.value
  164. chg_prog[tidx][ev.channel] += 1
  165. elif isinstance(ev, midi.ControlChangeEvent):
  166. if ev.control == 0:
  167. cur_bank[tidx][ev.channel] = (0x3F80 & cur_bank[tidx][ev.channel]) | ev.value
  168. chg_bank[tidx][ev.channel] += 1
  169. elif ev.control == 32:
  170. cur_bank[tidx][ev.channel] = (0x3F & cur_bank[tidx][ev.channel]) | (ev.value << 7)
  171. chg_bank[tidx][ev.channel] += 1
  172. elif isinstance(ev, midi.TrackNameEvent):
  173. tnames[tidx] = ev.text
  174. elif isinstance(ev, midi.Event):
  175. if isinstance(ev, midi.NoteOnEvent) and ev.velocity == 0:
  176. ev.__class__ = midi.NoteOffEvent #XXX Oww
  177. abstime += (60.0 * ev.tick) / (bpm * pat.resolution)
  178. events.append(MergeEvent(ev, tidx, abstime, cur_bank[tidx][ev.channel], cur_prog[tidx][ev.channel]))
  179. ev_cnts[tidx][ev.channel] += 1
  180. if options.verbose:
  181. print 'Track name, event count, final banks, bank changes, final programs, program changes:'
  182. for tidx, tname in enumerate(tnames):
  183. 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]))
  184. print 'Sorting events...'
  185. events.sort(key = lambda ev: ev.abstime)
  186. ##### Use merged events to construct a set of streams with non-overlapping durations #####
  187. print 'Generating streams...'
  188. class DurationEvent(MergeEvent):
  189. __slots__ = ['duration']
  190. def __init__(self, me, dur):
  191. MergeEvent.__init__(self, me.ev, me.tidx, me.abstime, me.bank, me.prog)
  192. self.duration = dur
  193. class NoteStream(object):
  194. __slots__ = ['history', 'active']
  195. def __init__(self):
  196. self.history = []
  197. self.active = None
  198. def IsActive(self):
  199. return self.active is not None
  200. def Activate(self, mev):
  201. self.active = mev
  202. def Deactivate(self, mev):
  203. self.history.append(DurationEvent(self.active, mev.abstime - self.active.abstime))
  204. self.active = None
  205. def WouldDeactivate(self, mev):
  206. if not self.IsActive():
  207. return False
  208. return mev.ev.pitch == self.active.ev.pitch and mev.tidx == self.active.tidx
  209. class NSGroup(object):
  210. __slots__ = ['streams', 'filter', 'name']
  211. def __init__(self, filter=None, name=None):
  212. self.streams = []
  213. self.filter = (lambda mev: True) if filter is None else filter
  214. self.name = name
  215. def Accept(self, mev):
  216. if not self.filter(mev):
  217. return False
  218. for stream in self.streams:
  219. if not stream.IsActive():
  220. stream.Activate(mev)
  221. break
  222. else:
  223. stream = NoteStream()
  224. self.streams.append(stream)
  225. stream.Activate(mev)
  226. return True
  227. notegroups = []
  228. auxstream = []
  229. if options.perc and options.perc != 'none':
  230. if options.perc == 'GM':
  231. notegroups.append(NSGroup(filter = lambda mev: mev.ev.channel == 9, name='perc'))
  232. elif options.perc == 'GM2':
  233. notegroups.append(NSGroup(filter = lambda mev: mev.bank == 15360, name='perc'))
  234. else:
  235. print 'Unrecognized --percussion option %r; should be GM, GM2, or none'%(options.perc,)
  236. for spec in options.tracks:
  237. if spec is TRACKS:
  238. for tidx in xrange(len(pat)):
  239. notegroups.append(NSGroup(filter = lambda mev, tidx=tidx: mev.tidx == tidx, name = 'trk%d'%(tidx,)))
  240. else:
  241. if '=' in spec:
  242. name, _, spec = spec.partition('=')
  243. else:
  244. name = None
  245. notegroups.append(NSGroup(filter = eval("lambda ev: "+spec), name = name))
  246. if options.verbose:
  247. print 'Initial group mappings:'
  248. for group in notegroups:
  249. print ('<anonymous>' if group.name is None else group.name)
  250. for mev in events:
  251. if isinstance(mev.ev, midi.NoteOnEvent):
  252. for group in notegroups:
  253. if group.Accept(mev):
  254. break
  255. else:
  256. group = NSGroup()
  257. group.Accept(mev)
  258. notegroups.append(group)
  259. elif isinstance(mev.ev, midi.NoteOffEvent):
  260. for group in notegroups:
  261. found = False
  262. for stream in group.streams:
  263. if stream.WouldDeactivate(mev):
  264. stream.Deactivate(mev)
  265. found = True
  266. break
  267. if found:
  268. break
  269. else:
  270. print 'WARNING: Did not match %r with any stream deactivation.'%(mev,)
  271. else:
  272. auxstream.append(mev)
  273. lastabstime = events[-1].abstime
  274. for group in notegroups:
  275. for ns in group.streams:
  276. if ns.IsActive():
  277. print 'WARNING: Active notes at end of playback.'
  278. ns.Deactivate(MergeEvent(ns.active, ns.active.tidx, lastabstime))
  279. if options.verbose:
  280. print 'Final group mappings:'
  281. for group in notegroups:
  282. print ('<anonymous>' if group.name is None else group.name), '<=', '(', len(group.streams), 'streams)'
  283. print 'Generated %d streams in %d groups'%(sum(map(lambda x: len(x.streams), notegroups)), len(notegroups))
  284. print 'Playtime:', lastabstime, 'seconds'
  285. ##### Write to XML and exit #####
  286. ivmeta = ET.SubElement(iv, 'meta')
  287. ivbpms = ET.SubElement(ivmeta, 'bpms')
  288. abstime = 0
  289. prevticks = 0
  290. prev_bpm = 120
  291. for absticks, bpm in sorted(bpm_at.items(), key = lambda pair: pair[0]):
  292. abstime += ((absticks - prevticks) * 60.0) / (prev_bpm * pat.resolution)
  293. prevticks = absticks
  294. ivbpm = ET.SubElement(ivbpms, 'bpm')
  295. ivbpm.set('bpm', str(bpm))
  296. ivbpm.set('ticks', str(absticks))
  297. ivbpm.set('time', str(abstime))
  298. ivstreams = ET.SubElement(iv, 'streams')
  299. x = 0
  300. while(x<options.repeaterNumber):
  301. for group in notegroups:
  302. for ns in group.streams:
  303. ivns = ET.SubElement(ivstreams, 'stream')
  304. ivns.set('type', 'ns')
  305. if group.name is not None:
  306. ivns.set('group', group.name)
  307. for note in ns.history:
  308. ivnote = ET.SubElement(ivns, 'note')
  309. ivnote.set('pitch', str(note.ev.pitch))
  310. ivnote.set('vel', str(note.ev.velocity))
  311. ivnote.set('time', str(note.abstime))
  312. ivnote.set('dur', str(note.duration))
  313. x+=1
  314. if(x>=options.repeaterNumber and options.repeaterNumber!=1):
  315. break
  316. if(x>=options.repeaterNumber and options.repeaterNumber!=1):
  317. break
  318. if(x>=options.repeaterNumber and options.repeaterNumber!=1):
  319. break
  320. ivaux = ET.SubElement(ivstreams, 'stream')
  321. ivaux.set('type', 'aux')
  322. fw = midi.FileWriter()
  323. fw.RunningStatus = None # XXX Hack
  324. for mev in auxstream:
  325. ivev = ET.SubElement(ivaux, 'ev')
  326. ivev.set('time', str(mev.abstime))
  327. ivev.set('data', repr(fw.encode_midi_event(mev.ev)))
  328. print 'Done.'
  329. open(os.path.splitext(os.path.basename(fname))[0]+'.iv', 'w').write(ET.tostring(iv))