christopher@45: # -*- coding: utf-8 -*- christopher@45: """ christopher@45: Created on Sat Mar 21 22:19:52 2015 christopher@45: christopher@45: @author: christopherh christopher@45: """ christopher@45: christopher@45: from midiparser import MidiFile, MidiTrack, DeltaTime, MidiEvent christopher@45: #from RhythmParser import Bar christopher@45: christopher@45: from music_objects import * christopher@45: from basic_functions import * christopher@45: christopher@45: christopher@45: christopher@45: christopher@45: def read_midi_file(filename): christopher@45: """ open and read a MIDI file, return a MidiFile object """ christopher@45: christopher@45: #create a midifile object, open and read a midi file christopher@45: midiFile = MidiFile() christopher@45: midiFile.open(filename, 'rb') christopher@45: midiFile.read() christopher@45: midiFile.close() christopher@45: christopher@45: return midiFile christopher@45: christopher@45: # def get_bars(midiFile, trackindex=1): christopher@45: # """ returns a list of bar objects from a MidiFile object """ christopher@45: christopher@45: # # select a track to extract (default = 1, ignoring dummy track 0) christopher@45: # track = midiFile.tracks[trackindex] christopher@45: # eventIndex = 0 christopher@45: # numNotes = 0 christopher@45: christopher@45: # noteonlist = [] christopher@45: # noteOnFound==True christopher@45: christopher@45: # while noteOnFound==True: christopher@45: # (noteOnIndex, noteOnDelta, noteOnFound) = self.find_event(track, eventIndex, lambda e: e.type == 'NOTE_ON') christopher@45: # noteEvent = track.events[noteOnIndex] christopher@45: # eventIndex = noteOnIndex + 1 christopher@45: christopher@45: christopher@45: #find_event(x.tracks[0], 0, lambda e: (e.type == 'NOTE_ON') | (e.type == 'KEY_SIGNATURE') | (e.type == "TIME_SIGNATURE")) christopher@45: christopher@45: ''' christopher@45: #read midiFile christopher@45: christopher@45: christopher@45: christopher@45: run through selected track getting notes out to build bars christopher@45: christopher@45: ''' christopher@45: christopher@45: christopher@45: christopher@45: def get_bars_from_midi(midiFile): christopher@45: christopher@45: # couple of inner functions to tidy up getting the initial values of christopher@45: # tempo and time signature christopher@45: def get_initial_tempo(timeList): christopher@45: christopher@45: tempo = None christopher@45: i=0 christopher@45: # Find the initial time and tempo: christopher@45: while(tempo == None and i=barStartTime+barLength: christopher@45: break christopher@45: christopher@45: if event.type=="TIME_SIGNATURE" and event.time>=barStartTime: christopher@45: timesig = midi_event_to_time_signature(event) christopher@45: event.type = "USED_TIME_SIGNATURE" christopher@50: barLength = calculate_bar_ticks(timesig.get_numerator(), christopher@50: timesig.get_denominator(), christopher@50: ticksPerQuarter) christopher@45: christopher@45: if timesig==None: christopher@45: if currentTimeSignature==None: christopher@45: timesig = TimeSignature("4/4") christopher@45: else: christopher@45: timesig = currentTimeSignature christopher@45: christopher@50: return timesig,barLength christopher@45: christopher@45: def get_tempo(timeList,barStartTime, barEndTime, currentTempo = None): christopher@45: christopher@45: tempo = None christopher@45: i=0 christopher@45: # get first event: christopher@45: while(i=barEndTime: christopher@45: break christopher@45: christopher@45: # run through list until we find the most recent tempo christopher@45: # before the end of the current bar christopher@45: if event.type=="SET_TEMPO" and event.time>=barStartTime: christopher@45: tempo = midi_event_to_qpm_tempo(event) christopher@45: event.type = "USED_TEMPO" christopher@45: christopher@45: if tempo==None: christopher@45: if currentTempo==None: christopher@45: tempo = 120 christopher@45: else: christopher@45: tempo = currentTempo christopher@45: christopher@45: return tempo christopher@45: christopher@45: christopher@45: christopher@45: # get initial time sig and tempo or use defaults christopher@45: timeList = get_time_events(midiFile) christopher@45: christopher@45: # get notes from the midi file (absolute start times from start of file) christopher@45: notesList = get_notes_from_event_list(get_note_events(midiFile)) christopher@45: christopher@45: christopher@45: # get initial tempo and time signature from time list christopher@45: timesig = get_initial_time_signature(timeList) christopher@45: tempo = get_initial_tempo(timeList) christopher@45: christopher@45: christopher@45: christopher@45: christopher@45: # ticks per quarter note: christopher@45: ticksPerQuarter = midiFile.ticksPerQuarterNote christopher@45: #calculate the initial length of a bar in ticks christopher@45: barlength = calculate_bar_ticks(timesig.get_numerator(), timesig.get_denominator(), ticksPerQuarter) christopher@45: # initialise time for start and end of current bar christopher@45: barStartTime = 0 christopher@50: barEndTime = 0# barlength christopher@45: christopher@45: christopher@45: # initialise bars list christopher@45: bars = BarList() christopher@45: noteIndex = 0 christopher@45: christopher@45: note = notesList[0] christopher@45: # run through the notes list, chopping it into bars christopher@45: while noteIndex0 and note.startTime==noteslist[-1].startTime: christopher@50: if note.duration>noteslist[-1].duration: christopher@50: noteslist[-1]=note christopher@50: # otherwise add the note to the list christopher@50: else: christopher@50: noteslist.append(note) christopher@45: #found the end of the note so break out of the local loop christopher@45: break christopher@50: localindex = localindex+1 christopher@45: christopher@45: return noteslist christopher@45: christopher@45: christopher@45: christopher@45: christopher@45: christopher@45: christopher@45: christopher@45: christopher@45: christopher@45: christopher@45: def get_note_events(midiFile, trackNumber = None): christopher@45: """ christopher@45: Gets all note on and note off events from a midifile. christopher@45: christopher@45: If trackNumber is not specified, the function will check the file format christopher@45: and pick either track 0 for a type 0 (single track format) or track 1 christopher@45: for a type 1 or 2 (multi-track) midi file. christopher@45: christopher@45: """ christopher@45: christopher@45: if trackNumber==None: christopher@45: if midiFile.format==0: christopher@45: trackNumber=0 christopher@45: else: christopher@45: trackNumber=1 christopher@45: christopher@45: return get_events_of_type(midiFile, trackNumber, lambda e: (e.type == 'NOTE_ON') | (e.type == "NOTE_OFF") ) christopher@45: christopher@45: christopher@45: christopher@45: def get_time_events(midiFile): christopher@45: """ christopher@45: Gets time signature and tempo events from a MIDI file (MIDI format 0 christopher@45: or format 1) and returns a list of those events and their associated christopher@45: absolute start times. If no time signature or tempo are specified then christopher@45: defaults of 4/4 and 120QPM are assumed. christopher@45: christopher@45: From the MIDI file specification: christopher@45: christopher@45: "All MIDI Files should specify tempo and time signature. If they don't, christopher@45: the time signature is assumed to be 4/4, and the tempo 120 beats per christopher@45: minute. In format 0, these meta-events should occur at least at the christopher@45: beginning of the single multi-channel track. In format 1, these meta-events christopher@45: should be contained in the first track. In format 2, each of the temporally christopher@45: independent patterns should contain at least initial time signature and christopher@45: tempo information." christopher@45: christopher@45: """ christopher@45: return get_events_of_type(midiFile, 0, lambda e: (e.type == 'SET_TEMPO') | (e.type == "TIME_SIGNATURE") ) christopher@45: christopher@45: christopher@45: def get_events_of_type(midiFile, trackIndex, lambdaEventType): christopher@45: """ christopher@45: Filters the events in a midi track that are selected by the christopher@45: function object lambdaEventType e.g. lambda e: (e.type == 'NOTE_ON') christopher@45: Return a list containing the relevant events with appropriate christopher@45: delta times between them christopher@45: """ christopher@45: christopher@45: track = midiFile.tracks[trackIndex] christopher@45: eventIndex = 0 christopher@45: # numTimeEvents = 0 christopher@45: christopher@45: localEventList = [] christopher@45: localEventFound = True christopher@45: #accumulatedTime = 0 christopher@45: christopher@45: while localEventFound==True: christopher@45: #find the next time event from the track: christopher@45: (localEventIndex, localEventDelta, localEventFound) = find_event(track, eventIndex, lambdaEventType) christopher@45: christopher@45: if localEventFound==True: christopher@45: #get the time event object out of the track christopher@45: localEvent = track.events[localEventIndex] christopher@45: christopher@45: #update the start event to search from christopher@45: eventIndex = localEventIndex + 1 christopher@45: christopher@45: #calculate the absolute start time of the time event christopher@45: #accumulatedTime = accumulatedTime + localEventDelta christopher@45: christopher@45: localEventList.append(localEvent) christopher@45: christopher@45: return localEventList christopher@45: christopher@45: christopher@45: def midi_event_to_time_signature(midiTimeSignatureEvent): christopher@45: """ christopher@45: Extract the numerator and denominator from a midi time signature christopher@45: event and return a TimeSignature music object. Ignore clocks per christopher@45: quarternote and 32nds per quarternote elements since these are christopher@45: only for sequencer metronome settings which we won't use here. christopher@45: """ christopher@45: if midiTimeSignatureEvent.type!="TIME_SIGNATURE": christopher@45: print "Error in midi_event_to_time_signature(), event must be a midi time signature type" christopher@45: return None christopher@45: else: christopher@45: num = ord(midiTimeSignatureEvent.data[0]) christopher@45: denom = 2**ord(midiTimeSignatureEvent.data[1]) christopher@45: return TimeSignature("%d/%d" % (num, denom)) christopher@45: christopher@45: christopher@45: def midi_event_to_qpm_tempo(midiTempoEvent): christopher@45: """ christopher@45: Extract the tempo in QPM from a midi SET_TEMPO event christopher@45: """ christopher@45: if midiTempoEvent.type!="SET_TEMPO": christopher@45: print "Error in midi_event_to_qpm_tempo(), event must be a midi tempo event" christopher@45: return None christopher@45: else: christopher@45: # tempo is stored as microseconds per quarter note christopher@45: # in three bytes which we can extract as three ints: christopher@45: values = map(ord, midiTempoEvent.data) christopher@45: # first byte multiplied by 2^16, second 2^8 and third is normal units christopher@45: # giving microseconds per quarter christopher@45: microsecondsPerQuarter = values[0]*2**16 + values[1]*2**8 + values[2] christopher@45: christopher@45: # to calculate QPM, 1 minute = 60million microseconds christopher@45: # so divide 60million by micros per quarter: christopher@45: return 60000000/microsecondsPerQuarter christopher@45: christopher@45: def find_event(track, eventStartIndex, lambdaExpr): christopher@45: ''' christopher@45: From code by Csaba Sulyok: christopher@45: Finds MIDI event based on lambda expression, starting from a given index. christopher@45: Returns a tuple of the following 3 elements: christopher@45: 1. event index where the lambda expression is true christopher@45: 2. aggregate delta time from event start index until the found event christopher@45: 3. flag whether or not any value was found, or we've reached the end of the event queue christopher@45: ''' christopher@45: christopher@45: eventIndex = eventStartIndex christopher@45: deltaTime = 0 christopher@45: while eventIndex < len(track.events) and not lambdaExpr(track.events[eventIndex]): christopher@45: if track.events[eventIndex].type == 'DeltaTime': christopher@45: deltaTime += track.events[eventIndex].time christopher@45: eventIndex += 1 christopher@45: christopher@45: success = eventIndex < len(track.events) christopher@45: return (eventIndex, deltaTime, success) christopher@45: christopher@45: