1 from __future__ import absolute_import
2 """Midi processing utilities
3
4 Utilities for processing MIDI data.
5
6 Note that most MIDI processing is not provided here. Anything that's
7 sufficiently generic is in the C{midi} library (which I've been
8 developing myself, so can easily add to). Things relating to retuning
9 and generation are in the L{<jazzparser.harmonical>harmonical module}.
10
11 @see: L{midi}
12 @see: L{jazzparser.harmonical}
13
14 """
15 """
16 ============================== License ========================================
17 Copyright (C) 2008, 2010-12 University of Edinburgh, Mark Granroth-Wilding
18
19 This file is part of The Jazz Parser.
20
21 The Jazz Parser is free software: you can redistribute it and/or modify
22 it under the terms of the GNU General Public License as published by
23 the Free Software Foundation, either version 3 of the License, or
24 (at your option) any later version.
25
26 The Jazz Parser is distributed in the hope that it will be useful,
27 but WITHOUT ANY WARRANTY; without even the implied warranty of
28 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29 GNU General Public License for more details.
30
31 You should have received a copy of the GNU General Public License
32 along with The Jazz Parser. If not, see <http://www.gnu.org/licenses/>.
33
34 ============================ End license ======================================
35
36 """
37 __author__ = "Mark Granroth-Wilding <mark.granroth-wilding@ed.ac.uk>"
38
39 from midi import NoteOnEvent, NoteOffEvent
40
42 """
43 Given two L{EventStreams<midi.EventStream>}, returns the similarity,
44 between 0 and 1, of the note on events.
45
46 This is useful for detecting duplicate MIDI files. Exact mirrors
47 of files can easily be found by checking for equality of the raw
48 MIDI data, but often files are redistributed with different
49 instruments, or simply different meta data.
50
51 Note that the absolute time of two MIDI events must be equal for
52 them to match, so an offset version of the same file will not
53 match.
54
55 This measure is not symmetric. C{midi0} is compared to C{midi1}
56 and vice versa and both results are returned. If you want a
57 symmetric measure, average the two.
58
59 @rtype: (float,float) pair
60 @return: a pair of similarities, the first from testing for how
61 much of C{midi0} is found in C{midi1} and the second from the
62 opposite.
63
64 """
65 from copy import copy
66
67 note_ons0 = [ev for ev in midi0.trackpool if isinstance(ev, NoteOnEvent)]
68 note_ons1 = [ev for ev in midi1.trackpool if isinstance(ev, NoteOnEvent)]
69 note_ons0.sort()
70 note_ons1.sort()
71
72 def _matches(base, comp):
73 base = copy(base)
74 comp = copy(comp)
75 matches = 0
76
77
78 for note_on in base:
79
80 while len(comp) and comp[0].tick < note_on.tick:
81 comp.pop(0)
82 if len(comp) == 0:
83
84 break
85 i = 0
86 while len(comp) > i and comp[i].tick == note_on.tick:
87 if comp[i].pitch == note_on.pitch:
88 matches += 1
89 break
90 i += 1
91 return matches
92
93
94 matches0 = _matches(note_ons0, note_ons1)
95 matches1 = _matches(note_ons1, note_ons0)
96
97 return float(matches0)/len(note_ons0), float(matches1)/len(note_ons1)
98
100 """
101 Many MIDI files begin with a count-in on a drum. Some might even
102 begin with some silence.
103
104 Given a L{midi.EventStream}, returns a version with any drum intro
105 or silence trimmed from the beginning, so that the first thing
106 heard is a non-drum note.
107
108 It is assumed that this is General MIDI data, so channel 10 is
109 necessarily a drum channel.
110
111 """
112 from midi import EndOfTrackEvent
113 import copy
114
115 mid = copy.deepcopy(mid)
116 first_note = None
117 events = iter(sorted(mid.trackpool))
118
119 while first_note is None:
120 ev = events.next()
121 if isinstance(ev, NoteOnEvent) and ev.channel != 9:
122 first_note = ev.tick
123
124
125 to_remove = []
126 intro_events = []
127 intro_length = 0
128 for track in mid.tracklist.values():
129
130
131 for ev in track:
132 intro_times = {}
133 if ev.tick < first_note:
134
135 if isinstance(ev, (NoteOnEvent,NoteOffEvent)):
136 to_remove.append(ev)
137 else:
138
139 intro_times.setdefault(ev.tick, []).append(ev)
140 intro_events.append(id(ev))
141
142 for tick,(old_tick, pre_events) in enumerate(intro_times.items()):
143 for ev in pre_events:
144 ev.tick = tick
145 intro_length = max(intro_length, len(intro_times))
146
147
148 for ev in to_remove:
149 mid.remove_event_instance(ev)
150
151
152 shift = first_note - intro_length
153 for ev in mid.trackpool:
154 if id(ev) not in intro_events:
155 old_time = ev.tick
156 ev.tick -= shift
157 return mid
158
160 """
161 Plays an event stream.
162
163 Various methods for playing midi data are provided in the L{midi}
164 library. At the time of writing, the only one that works is Timidity
165 via SDL via PyGame and this is what this function uses.
166
167 Whatever happens with the midi library, this function should
168 continue to provide some convenient way to play a L{midi.EventStream}.
169
170 If a keyboard interrupt is received, the playing will stop. The interrupt
171 will still be raised.
172
173 @rtype: L{midi.sequencer_pygame.Sequencer}
174 @return: the sequencer that's been instantiated to do the playing.
175
176 """
177 from midi.sequencer_pygame import Sequencer
178 seq = Sequencer(stream)
179 try:
180 seq.play(block=block)
181 except KeyboardInterrupt:
182 seq.stop()
183 raise
184 return seq
185
186 -def get_midi_text(stream):
187 """
188 Extracts descriptive text from a L{midi.EventStream}. This is often
189 stored in the form of copyright notices, text events, track names,
190 etc. This tries to pull out everything it can and return it all in
191 a multiline string.
192
193 Midi data can use non-ASCII characters. We assume the latin1
194 encoding is intended for these. This certainly covers the most
195 common case: the copyright symbol.
196
197 Returns a unicode string.
198
199 """
200 from midi import CopyrightEvent, TextEvent, TrackNameEvent
201 lines = []
202
203 for ev in sorted(stream.trackpool):
204 if isinstance(ev, (CopyrightEvent, TextEvent)):
205 lines.append(unicode(ev.data, 'latin1'))
206 elif isinstance(ev, NoteOnEvent):
207
208
209 break
210
211
212 for track in stream:
213 for ev in sorted(track):
214 if isinstance(ev, TrackNameEvent):
215 lines.append(u"Track: %s" % unicode(ev.data, 'latin1'))
216
217 break
218 return u"\n".join(lines)
219
221 """
222 Returns the tick time of the first note-on event in an EventStream.
223
224 """
225 for ev in sorted(stream.trackpool):
226 if type(ev) == NoteOnEvent:
227 return ev.tick
228
229 -def simplify(stream, remove_drums=False, remove_pc=False,
230 remove_all_text=False, one_track=False, remove_tempo=False,
231 remove_control=False, one_channel=False,
232 remove_misc_control=False, real_note_offs=False, remove_duplicates=False):
233 """
234 Filters a midi L{midi.EventStream} to simplify it. This is useful
235 as a preprocessing step before taking midi input to an algorithm,
236 for example, to make it clearer what the algorithm is using.
237
238 Use kwargs to determine what filters will be applied. Without any
239 kwargs, the stream will just be left as it was.
240
241 Returns a filtered copy of the stream.
242
243 @type remove_drums: bool
244 @param remove_drums: filter out all channel 10 events
245 @type remove_pc: bool
246 @param remove_pc: filter out all program change events
247 @type remove_all_text: bool
248 @param remove_all_text: filter out any text events. This includes
249 copyright, text, track name, lyrics.
250 @type one_track: bool
251 @param one_track: reduce everything to just one track
252 @type remove_tempo: bool
253 @param remove_tempo: filter out all tempo events
254 @type remove_control: bool
255 @param remove_control: filter out all control change events
256 @type one_channel: bool
257 @param one_channel: use only one channel: set the channel of
258 every event to 0
259 @type remove_misc_control: bool
260 @param remove_misc_control: filters a miscellany of device
261 control events: aftertouch, channel aftertouch, pitch wheel,
262 sysex, port
263 @type real_note_offs: bool
264 @param real_note_offs: replace 0-velocity note-ons with actual
265 note-offs. Some midi files use one, some the other
266
267 """
268 from midi import EventStream, TextEvent, ProgramChangeEvent, \
269 CopyrightEvent, TrackNameEvent, \
270 SetTempoEvent, ControlChangeEvent, AfterTouchEvent, \
271 ChannelAfterTouchEvent, PitchWheelEvent, SysExEvent, \
272 LyricsEvent, PortEvent, CuePointEvent, MarkerEvent, EndOfTrackEvent
273 import copy
274
275
276 new_stream = EventStream()
277 new_stream.resolution = stream.resolution
278 new_stream.format = stream.format
279
280
281 input_start = first_note_tick(stream)
282
283
284 for track in stream:
285 track_events = []
286 for ev in sorted(track):
287
288 if type(ev) == EndOfTrackEvent:
289 continue
290 ev = copy.deepcopy(ev)
291
292
293 if remove_drums:
294
295
296 if ev.channel == 9 and \
297 type(ev) in (NoteOnEvent, NoteOffEvent):
298 continue
299 if remove_pc:
300
301 if type(ev) == ProgramChangeEvent:
302 continue
303 if remove_all_text:
304
305 if type(ev) in (TextEvent, CopyrightEvent, TrackNameEvent,
306 LyricsEvent, CuePointEvent, MarkerEvent):
307 continue
308 if remove_tempo:
309
310 if type(ev) == SetTempoEvent:
311 continue
312 if remove_control:
313
314 if type(ev) == ControlChangeEvent:
315 continue
316 if remove_misc_control:
317
318 if type(ev) in (AfterTouchEvent, ChannelAfterTouchEvent,
319 ChannelAfterTouchEvent, PitchWheelEvent,
320 SysExEvent, PortEvent):
321 continue
322 if real_note_offs:
323
324 if type(ev) == NoteOnEvent and ev.velocity == 0:
325 new_ev = NoteOffEvent()
326 new_ev.pitch = ev.pitch
327 new_ev.channel = ev.channel
328 new_ev.tick = ev.tick
329 ev = new_ev
330 if one_channel:
331 ev.channel = 0
332
333 track_events.append(ev)
334
335
336 if len(track_events) > 1:
337 if not one_track or len(new_stream.tracklist) == 0:
338 new_stream.add_track()
339 for ev in track_events:
340 new_stream.add_event(ev)
341 track_events = []
342
343 for track in stream:
344 track.sort()
345
346
347 result_start = first_note_tick(new_stream)
348
349
350 shift = result_start - input_start
351 before_start = max(input_start-1, 0)
352 if shift > 0:
353 for ev in new_stream.trackpool:
354 if ev.tick >= result_start:
355 ev.tick -= shift
356 elif ev.tick < result_start and ev.tick >= input_start:
357
358
359 ev.tick = before_start
360
361 new_stream.trackpool.sort()
362
363 if remove_duplicates:
364
365 remove_duplicate_notes(new_stream, replay=True)
366
367 return new_stream
368
370 """
371 Some processing operations, like L{simplify}, can leave a midi file with
372 the same note being played twice at the same time.
373
374 To avoid the confusion this leads to, it's best to remove these. This
375 function will remove multiple instances of the same note being played
376 simultaneously (in the same track and channel) and insert note-off events
377 before a note is replayed that's already being played.
378
379 This can lead to some strange effects if multiple instruments have been
380 reduced to one, as in the case of L{simplify}. You may wish to keep
381 seperate instruments on separate channels to avoid this.
382
383 @type replay: bool
384 @param replay: if True, notes that are played while they're already sounding
385 while be replayed - taken off and put back on. Otherwise, such notes
386 will be ignored.
387
388 """
389 to_remove = []
390 to_add = {}
391
392 for i,track in stream.tracklist.items():
393 notes_on = {}
394 last_instance = {}
395
396 for ev in sorted(track):
397 if type(ev) == NoteOnEvent and ev.velocity > 0:
398
399 if ev.channel in notes_on and \
400 ev.pitch in notes_on[ev.channel]:
401
402 previous = last_instance[ev.channel][ev.pitch]
403 if not replay or previous.tick == ev.tick:
404
405
406
407 to_remove.append(ev)
408 else:
409
410 note_off = NoteOffEvent()
411 note_off.pitch = ev.pitch
412 note_off.channel = ev.channel
413 note_off.velocity = 127
414 note_off.tick = ev.tick-1
415 to_add.setdefault(i, []).append(note_off)
416
417 notes_on.setdefault(ev.channel, {}).setdefault(ev.pitch, 0)
418 notes_on[ev.channel][ev.pitch] += 1
419 last_instance.setdefault(ev.channel, {})[ev.pitch] = ev
420 elif type(ev) == NoteOffEvent or \
421 (type(ev) == NoteOnEvent and ev.velocity == 0):
422
423 if ev.channel not in notes_on or \
424 ev.pitch not in notes_on[ev.channel]:
425
426
427 to_remove.append(ev)
428 else:
429
430 notes_on[ev.channel][ev.pitch] -= 1
431 if notes_on[ev.channel][ev.pitch] == 0:
432
433
434 del notes_on[ev.channel][ev.pitch]
435 else:
436
437
438 to_remove.append(ev)
439
440
441 stream.remove_event_instances(to_remove)
442
443 for trk,evs in to_add.items():
444 stream.curtrack = trk
445 for ev in evs:
446 stream.add_event(ev)
447
449 """
450 Modifies a stream in place to remove all events played on certain channels.
451
452 """
453 to_remove = []
454 for ev in stream.trackpool:
455 if ev.channel in channels:
456 to_remove.append(ev)
457 stream.remove_event_instances(to_remove)
458
460 """
461 Filters the events in an event stream to return only note-on events with a
462 non-zero velocity.
463
464 """
465 return [ev for ev in stream.trackpool if isinstance(ev, NoteOnEvent) \
466 and ev.velocity > 0]
467