mkiv.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  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. -Reserve channels by track
  8. -Reserve channels by MIDI channel
  9. -Pitch limits for channels
  10. -MIDI Control events
  11. '''
  12. import xml.etree.ElementTree as ET
  13. import midi
  14. import sys
  15. import os
  16. import optparse
  17. TRACKS = 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.set_defaults(tracks=[])
  26. options, args = parser.parse_args()
  27. if options.help_conds:
  28. print '''Filter conditions are used to route events to groups of streams.
  29. Every filter is an expression; internally, this expression is evaluated as the body of a "lambda ev: ".
  30. The "ev" object will be a MergeEvent with the following properties:
  31. -ev.tidx: the originating track index (starting at 0)
  32. -ev.abstime: the real time in seconds of this event relative to the beginning of playback
  33. -ev.ev: a midi.NoteOnEvent:
  34. -ev.ev.pitch: the MIDI pitch
  35. -ev.ev.velocity: the MIDI velocity
  36. Specifying a -t <group>=<filter> will group all streams under a filter; if the <group> part is omitted, no group will be added.
  37. For example:
  38. mkiv -t bass=ev.ev.pitch<35 -t treble=ev.ev.pitch>75 -T -t ev.abstime<10
  39. will cause these groups to be made:
  40. -A group "bass" with all notes with pitch less than 35;
  41. -Of those not in "bass", a group in "treble" with pitch>75;
  42. -Of what is not yet consumed, a series of groups "trkN" where N is the track index (starting at 0), which consumes the rest.
  43. -An (unfortunately empty) unnamed group with events prior to ten real seconds.
  44. As can be seen, order of specification is important. Equally important is the location of -T, which should be at the end.
  45. NoteOffEvents are always matched to the stream which has their corresponding NoteOnEvent (in track and pitch), and so are
  46. not affected or observed by filters.
  47. If the filters specified are not a complete cover, an anonymous group will be created with no filter to contain the rest. If
  48. it is desired to force this group to have a name, use -t <group>=True.'''
  49. exit()
  50. if not args:
  51. parser.print_usage()
  52. exit()
  53. for fname in args:
  54. pat = midi.read_midifile(fname)
  55. iv = ET.Element('iv')
  56. iv.set('version', '1')
  57. iv.set('src', os.path.basename(fname))
  58. print fname, ': MIDI format,', len(pat), 'tracks'
  59. if options.chansplit:
  60. print 'Splitting channels...'
  61. old_pat = pat
  62. pat = midi.Pattern(resolution=old_pat.resolution)
  63. for track in old_pat:
  64. chan_map = {}
  65. for ev in track:
  66. if isinstance(ev, midi.Event):
  67. if options.chanskeep:
  68. newev = ev.copy()
  69. else:
  70. newev = ev.copy(channel=1)
  71. chan_map.setdefault(ev.channel, midi.Track()).append(newev)
  72. else: # MetaEvent
  73. for trk in chan_map.itervalues():
  74. trk.append(ev)
  75. items = chan_map.items()
  76. items.sort(key=lambda pair: pair[0])
  77. for chn, trk in items:
  78. pat.append(trk)
  79. print 'Split', len(old_pat), 'tracks into', len(pat), 'tracks by channel'
  80. if options.chansfname:
  81. midi.write_midifile(options.chansfname, pat)
  82. ##### Merge events from all tracks into one master list, annotated with track and absolute times #####
  83. print 'Merging events...'
  84. class MergeEvent(object):
  85. __slots__ = ['ev', 'tidx', 'abstime']
  86. def __init__(self, ev, tidx, abstime):
  87. self.ev = ev
  88. self.tidx = tidx
  89. self.abstime = abstime
  90. def __repr__(self):
  91. return '<ME %r in %d @%f>'%(self.ev, self.tidx, self.abstime)
  92. events = []
  93. bpm_at = {0: 120}
  94. for tidx, track in enumerate(pat):
  95. abstime = 0
  96. absticks = 0
  97. for ev in track:
  98. if isinstance(ev, midi.SetTempoEvent):
  99. absticks += ev.tick
  100. bpm_at[absticks] = ev.bpm
  101. else:
  102. if isinstance(ev, midi.NoteOnEvent) and ev.velocity == 0:
  103. ev.__class__ = midi.NoteOffEvent #XXX Oww
  104. bpm = filter(lambda pair: pair[0] <= absticks, bpm_at.items())[-1][1]
  105. abstime += (60.0 * ev.tick) / (bpm * pat.resolution)
  106. absticks += ev.tick
  107. events.append(MergeEvent(ev, tidx, abstime))
  108. print 'Sorting events...'
  109. events.sort(key = lambda ev: ev.abstime)
  110. ##### Use merged events to construct a set of streams with non-overlapping durations #####
  111. print 'Generating streams...'
  112. class DurationEvent(MergeEvent):
  113. __slots__ = ['duration']
  114. def __init__(self, me, dur):
  115. MergeEvent.__init__(self, me.ev, me.tidx, me.abstime)
  116. self.duration = dur
  117. class NoteStream(object):
  118. __slots__ = ['history', 'active']
  119. def __init__(self):
  120. self.history = []
  121. self.active = None
  122. def IsActive(self):
  123. return self.active is not None
  124. def Activate(self, mev):
  125. self.active = mev
  126. def Deactivate(self, mev):
  127. self.history.append(DurationEvent(self.active, mev.abstime - self.active.abstime))
  128. self.active = None
  129. def WouldDeactivate(self, mev):
  130. if not self.IsActive():
  131. return False
  132. return mev.ev.pitch == self.active.ev.pitch and mev.tidx == self.active.tidx
  133. class NSGroup(object):
  134. __slots__ = ['streams', 'filter', 'name']
  135. def __init__(self, filter=None, name=None):
  136. self.streams = []
  137. self.filter = (lambda mev: True) if filter is None else filter
  138. self.name = name
  139. def Accept(self, mev):
  140. if not self.filter(mev):
  141. return False
  142. for stream in self.streams:
  143. if not stream.IsActive():
  144. stream.Activate(mev)
  145. break
  146. else:
  147. stream = NoteStream()
  148. self.streams.append(stream)
  149. stream.Activate(mev)
  150. return True
  151. notegroups = []
  152. auxstream = []
  153. for spec in options.tracks:
  154. if spec is TRACKS:
  155. for tidx in xrange(len(pat)):
  156. notegroups.append(NSGroup(filter = lambda mev, tidx=tidx: mev.tidx == tidx, name = 'trk%d'%(tidx,)))
  157. else:
  158. if '=' in spec:
  159. name, _, spec = spec.partition('=')
  160. else:
  161. name = None
  162. notegroups.append(NSGroup(filter = eval("lambda ev: "+spec), name = name))
  163. print 'Initial group mappings:'
  164. for group in notegroups:
  165. print ('<anonymous>' if group.name is None else group.name), '<=', group.filter
  166. for mev in events:
  167. if isinstance(mev.ev, midi.NoteOnEvent):
  168. for group in notegroups:
  169. if group.Accept(mev):
  170. break
  171. else:
  172. group = NSGroup()
  173. group.Accept(mev)
  174. notegroups.append(group)
  175. elif isinstance(mev.ev, midi.NoteOffEvent):
  176. for group in notegroups:
  177. found = False
  178. for stream in group.streams:
  179. if stream.WouldDeactivate(mev):
  180. stream.Deactivate(mev)
  181. found = True
  182. break
  183. if found:
  184. break
  185. else:
  186. print 'WARNING: Did not match %r with any stream deactivation.'%(mev,)
  187. else:
  188. auxstream.append(mev)
  189. lastabstime = events[-1].abstime
  190. for group in notegroups:
  191. for ns in group.streams:
  192. if ns.IsActive():
  193. print 'WARNING: Active notes at end of playback.'
  194. ns.Deactivate(MergeEvent(ns.active, ns.active.tidx, lastabstime))
  195. print 'Final group mappings:'
  196. for group in notegroups:
  197. print ('<anonymous>' if group.name is None else group.name), '<=', group.filter, '(', len(group.streams), 'streams)'
  198. print 'Generated %d streams in %d groups'%(sum(map(lambda x: len(x.streams), notegroups)), len(notegroups))
  199. ##### Write to XML and exit #####
  200. ivmeta = ET.SubElement(iv, 'meta')
  201. ivbpms = ET.SubElement(ivmeta, 'bpms')
  202. abstime = 0
  203. prevticks = 0
  204. prev_bpm = 120
  205. for absticks, bpm in sorted(bpm_at.items(), key = lambda pair: pair[0]):
  206. abstime += ((absticks - prevticks) * 60.0) / (prev_bpm * pat.resolution)
  207. prevticks = absticks
  208. ivbpm = ET.SubElement(ivbpms, 'bpm')
  209. ivbpm.set('bpm', str(bpm))
  210. ivbpm.set('ticks', str(absticks))
  211. ivbpm.set('time', str(abstime))
  212. ivstreams = ET.SubElement(iv, 'streams')
  213. for group in notegroups:
  214. for ns in group.streams:
  215. ivns = ET.SubElement(ivstreams, 'stream')
  216. ivns.set('type', 'ns')
  217. if group.name is not None:
  218. ivns.set('group', group.name)
  219. for note in ns.history:
  220. ivnote = ET.SubElement(ivns, 'note')
  221. ivnote.set('pitch', str(note.ev.pitch))
  222. ivnote.set('vel', str(note.ev.velocity))
  223. ivnote.set('time', str(note.abstime))
  224. ivnote.set('dur', str(note.duration))
  225. ivaux = ET.SubElement(ivstreams, 'stream')
  226. ivaux.set('type', 'aux')
  227. fw = midi.FileWriter()
  228. fw.RunningStatus = None # XXX Hack
  229. for mev in auxstream:
  230. ivev = ET.SubElement(ivaux, 'ev')
  231. ivev.set('time', str(mev.abstime))
  232. ivev.set('data', repr(fw.encode_midi_event(mev.ev)))
  233. print 'Done.'
  234. open(os.path.splitext(os.path.basename(fname))[0]+'.iv', 'w').write(ET.tostring(iv))