Package jazzparser :: Package utils :: Module midi
[hide private]
[frames] | no frames]

Source Code for Module jazzparser.utils.midi

  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   
41 -def note_on_similarity(midi0, midi1):
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 # Go through each note-on in the first file and look for a matching 77 # one in the second 78 for note_on in base: 79 # Try to find a note-on with the same note at the same time 80 while len(comp) and comp[0].tick < note_on.tick: 81 comp.pop(0) 82 if len(comp) == 0: 83 # No more matches to find 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 # Work out the similarity both ways so the measure is symetric 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
99 -def trim_intro(mid):
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 # Find the time of the first played note that's not a drum note 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 # Shift everything backwards 125 to_remove = [] 126 intro_events = [] 127 intro_length = 0 128 for track in mid.tracklist.values(): 129 # Keep all the events in the right order, but remove as much 130 # empty time as possible 131 for ev in track: 132 intro_times = {} 133 if ev.tick < first_note: 134 # Remove all note-ons and -offs 135 if isinstance(ev, (NoteOnEvent,NoteOffEvent)): 136 to_remove.append(ev) 137 else: 138 # Put the rest as close to the beginning as possible 139 intro_times.setdefault(ev.tick, []).append(ev) 140 intro_events.append(id(ev)) 141 # Give each distinct time only a single midi tick 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 # Now get rid of the pre-start notes 148 for ev in to_remove: 149 mid.remove_event_instance(ev) 150 151 # Shift everything back as for as we can 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
159 -def play_stream(stream, block=False):
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 # Stop looking for text once the notes start 208 # (this prevents us getting lyrics) 209 break 210 211 # Get track name events in order of tracks 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 # There should only be one of these 217 break 218 return u"\n".join(lines)
219
220 -def first_note_tick(stream):
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 # Empty stream to which we'll add the events we don't filter 276 new_stream = EventStream() 277 new_stream.resolution = stream.resolution 278 new_stream.format = stream.format 279 280 # Work out when the first note starts in the input stream 281 input_start = first_note_tick(stream) 282 283 # Filter track by track 284 for track in stream: 285 track_events = [] 286 for ev in sorted(track): 287 # Don't add EOTs - they get added automatically 288 if type(ev) == EndOfTrackEvent: 289 continue 290 ev = copy.deepcopy(ev) 291 # Each filter may modify the event or continue to filter it altogether 292 293 if remove_drums: 294 # Filter out any channel 10 events, which is typically 295 # reserved for drums 296 if ev.channel == 9 and \ 297 type(ev) in (NoteOnEvent, NoteOffEvent): 298 continue 299 if remove_pc: 300 # Filter out any program change events 301 if type(ev) == ProgramChangeEvent: 302 continue 303 if remove_all_text: 304 # Filter out any types of text event 305 if type(ev) in (TextEvent, CopyrightEvent, TrackNameEvent, 306 LyricsEvent, CuePointEvent, MarkerEvent): 307 continue 308 if remove_tempo: 309 # Filter out any tempo events 310 if type(ev) == SetTempoEvent: 311 continue 312 if remove_control: 313 # Filter out any control change events 314 if type(ev) == ControlChangeEvent: 315 continue 316 if remove_misc_control: 317 # Filter out various types of control events 318 if type(ev) in (AfterTouchEvent, ChannelAfterTouchEvent, 319 ChannelAfterTouchEvent, PitchWheelEvent, 320 SysExEvent, PortEvent): 321 continue 322 if real_note_offs: 323 # Replace 0-velocity note-ons with note-offs 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 # If there are events left in the track, add them all as a new track 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 # Work out when the first note happens now 347 result_start = first_note_tick(new_stream) 348 # Move all events after and including this sooner so the music 349 # starts at the same point it did before 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 # This event happened in a region that no longer contains notes 358 # Move it back to before what's now the first note 359 ev.tick = before_start 360 361 new_stream.trackpool.sort() 362 363 if remove_duplicates: 364 # Get rid of now duplicate events 365 remove_duplicate_notes(new_stream, replay=True) 366 367 return new_stream
368
369 -def remove_duplicate_notes(stream, replay=False):
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 # Note on 399 if ev.channel in notes_on and \ 400 ev.pitch in notes_on[ev.channel]: 401 # Note is already being played 402 previous = last_instance[ev.channel][ev.pitch] 403 if not replay or previous.tick == ev.tick: 404 # Simultaneous duplicate, or we don't want to replay 405 # resounded notes 406 # Remove this one 407 to_remove.append(ev) 408 else: 409 # Replay: insert a note-off 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 # Increase the count of instances of this note being played 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 # Note off 423 if ev.channel not in notes_on or \ 424 ev.pitch not in notes_on[ev.channel]: 425 # Note is not currently being played 426 # Remove this note off 427 to_remove.append(ev) 428 else: 429 # Decrease the count of instances of this note being played 430 notes_on[ev.channel][ev.pitch] -= 1 431 if notes_on[ev.channel][ev.pitch] == 0: 432 # Note was only being played once 433 # Leave the note off in there 434 del notes_on[ev.channel][ev.pitch] 435 else: 436 # Note was being played multiple times 437 # Decrease the count, but don't include this note off 438 to_remove.append(ev) 439 440 # Remove all events scheduled for removal 441 stream.remove_event_instances(to_remove) 442 # Add all events scheduled to addition 443 for trk,evs in to_add.items(): 444 stream.curtrack = trk 445 for ev in evs: 446 stream.add_event(ev)
447
448 -def remove_channels(stream, channels=[]):
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
459 -def note_ons(stream):
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