mkiv.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  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. pat = midi.read_midifile(sys.argv[1])
  17. iv = ET.Element('iv')
  18. iv.set('version', '1')
  19. iv.set('src', os.path.basename(sys.argv[1]))
  20. ##### Merge events from all tracks into one master list, annotated with track and absolute times #####
  21. print 'Merging events...'
  22. class MergeEvent(object):
  23. __slots__ = ['ev', 'tidx', 'abstime']
  24. def __init__(self, ev, tidx, abstime):
  25. self.ev = ev
  26. self.tidx = tidx
  27. self.abstime = abstime
  28. def __repr__(self):
  29. return '<ME %r in %d @%f>'%(self.ev, self.tidx, self.abstime)
  30. events = []
  31. bpm_at = {0: 120}
  32. for tidx, track in enumerate(pat):
  33. abstime = 0
  34. absticks = 0
  35. for ev in track:
  36. if isinstance(ev, midi.SetTempoEvent):
  37. absticks += ev.tick
  38. bpm_at[absticks] = ev.bpm
  39. else:
  40. if isinstance(ev, midi.NoteOnEvent) and ev.velocity == 0:
  41. ev.__class__ = midi.NoteOffEvent #XXX Oww
  42. bpm = filter(lambda pair: pair[0] <= absticks, bpm_at.items())[-1][1]
  43. abstime += (60.0 * ev.tick) / (bpm * pat.resolution)
  44. absticks += ev.tick
  45. events.append(MergeEvent(ev, tidx, abstime))
  46. print 'Sorting events...'
  47. events.sort(key = lambda ev: ev.abstime)
  48. ##### Use merged events to construct a set of streams with non-overlapping durations #####
  49. print 'Generating streams...'
  50. class DurationEvent(MergeEvent):
  51. __slots__ = ['duration']
  52. def __init__(self, me, dur):
  53. MergeEvent.__init__(self, me.ev, me.tidx, me.abstime)
  54. self.duration = dur
  55. class NoteStream(object):
  56. __slots__ = ['history', 'active']
  57. def __init__(self):
  58. self.history = []
  59. self.active = None
  60. def IsActive(self):
  61. return self.active is not None
  62. def Activate(self, mev):
  63. self.active = mev
  64. def Deactivate(self, mev):
  65. self.history.append(DurationEvent(self.active, mev.abstime - self.active.abstime))
  66. self.active = None
  67. def WouldDeactivate(self, mev):
  68. if not self.IsActive():
  69. return False
  70. return mev.ev.pitch == self.active.ev.pitch and mev.tidx == self.active.tidx
  71. notestreams = []
  72. auxstream = []
  73. for mev in events:
  74. if isinstance(mev.ev, midi.NoteOnEvent):
  75. for stream in notestreams:
  76. if not stream.IsActive():
  77. stream.Activate(mev)
  78. break
  79. else:
  80. stream = NoteStream()
  81. notestreams.append(stream)
  82. stream.Activate(mev)
  83. elif isinstance(mev.ev, midi.NoteOffEvent):
  84. for stream in notestreams:
  85. if stream.WouldDeactivate(mev):
  86. stream.Deactivate(mev)
  87. break
  88. else:
  89. print 'WARNING: Did not match %r with any stream deactivation.'%(mev,)
  90. else:
  91. auxstream.append(mev)
  92. lastabstime = events[-1].abstime
  93. for ns in notestreams:
  94. if not ns:
  95. print 'WARNING: Active notes at end of playback.'
  96. ns.Deactivate(MergeEvent(ns.active, ns.active.tidx, lastabstime))
  97. print 'Generated %d streams'%(len(notestreams),)
  98. ##### Write to XML and exit #####
  99. ivmeta = ET.SubElement(iv, 'meta')
  100. ivbpms = ET.SubElement(ivmeta, 'bpms')
  101. abstime = 0
  102. prevticks = 0
  103. prev_bpm = 120
  104. for absticks, bpm in sorted(bpm_at.items(), key = lambda pair: pair[0]):
  105. abstime += ((absticks - prevticks) * 60.0) / (prev_bpm * pat.resolution)
  106. prevticks = absticks
  107. ivbpm = ET.SubElement(ivbpms, 'bpm')
  108. ivbpm.set('bpm', str(bpm))
  109. ivbpm.set('ticks', str(absticks))
  110. ivbpm.set('time', str(abstime))
  111. ivstreams = ET.SubElement(iv, 'streams')
  112. for ns in notestreams:
  113. ivns = ET.SubElement(ivstreams, 'stream')
  114. ivns.set('type', 'ns')
  115. for note in ns.history:
  116. ivnote = ET.SubElement(ivns, 'note')
  117. ivnote.set('pitch', str(note.ev.pitch))
  118. ivnote.set('vel', str(note.ev.velocity))
  119. ivnote.set('time', str(note.abstime))
  120. ivnote.set('dur', str(note.duration))
  121. ivaux = ET.SubElement(ivstreams, 'stream')
  122. ivaux.set('type', 'aux')
  123. fw = midi.FileWriter()
  124. fw.RunningStatus = None # XXX Hack
  125. for mev in auxstream:
  126. ivev = ET.SubElement(ivaux, 'ev')
  127. ivev.set('time', str(mev.abstime))
  128. ivev.set('data', repr(fw.encode_midi_event(mev.ev)))
  129. print 'Done.'
  130. open(os.path.splitext(os.path.basename(sys.argv[1]))[0]+'.iv', 'w').write(ET.tostring(iv))