def __init__(self, path): pm = PrettyMIDI(path) pm.remove_invalid_notes() self.midi = pm self.tempo = pm.estimate_tempo() self.beat_times = pm.get_beats() self.bar_times = pm.get_downbeats() self.end_time = pm.get_end_time() self.instruments = pm.instruments
def get_feat(midi: pmidi.PrettyMIDI, fs=_fs, dic=_dic): global feat_dim end = int(fs * midi.get_end_time()) rolls = np.zeros((len(midi.instruments), end)).astype(np.int64) for idx, ins in enumerate(midi.instruments): # Allocate a matrix of zeros - we will add in as we go end = int(fs * ins.get_end_time()) roll = np.zeros((feat_dim, end)).astype(np.int64) for note in ins.notes: method = 1 start, end = int(note.start * fs), int(note.end * fs) pitch = _range(note.pitch, PITCH_LOW, PITCH_HIGH) velocity = _range(note.velocity, VELOCITY_LOW, VELOCITY_HIGH, VELOCITY_STEP) if pitch == 0 or velocity == 0: raise Exception # if roll[2,end-1] != 0 or roll[2, start] != 0: if np.any(roll[2, start:end] != 0): # overwrite method print('Warn: Overwrite', start, end) # print(np.where(roll[2,start:end] == 2)[0]) method = 2 if method == 1: roll[0, start:end] = pitch roll[1, start:end] = velocity roll[2, start:end] = 1 roll[ 2, start] = 2 # means note on(2), time_shift(1) -> direct overwrite elif method == 2: modify_place = np.where(roll[2, start:end] == 0)[0] + start cuts = find_continue(modify_place) for start, end in cuts: # print(start, end, end='\t') start = modify_place[start] end = modify_place[end] + 1 # from end-index to end-slice # print(start, end, end='\n') roll[0, start:end] = pitch roll[1, start:end] = velocity roll[2, start:end] = 1 roll[ 2, start] = 2 # means note on(2), time_shift(1) -> direct overwrite # Encode to one-hot for i in range(roll.shape[1]): p, v, c = roll[:, i] rolls[idx, i] = dic[p][v][c] return rolls
def split_midi(midi: pretty_midi.PrettyMIDI, min_length: float = 15, max_length: float = 90) -> List[pretty_midi.PrettyMIDI]: total_length = midi.get_end_time() # part_length = total_length / optimal_part_count # part_length = max(min_length, part_length) # part_length = min(max_length, part_length) # part_length = random.randrange(min_length, max_length) result = [] time = 0.0 while time < total_length: tmp_length = random.randrange(min_length, max_length) part_end = min(total_length, time + tmp_length) part = create_sub_midi(midi, time, part_end) result.append(part) time += tmp_length return result
class MidiFile: id: int name: str midi: PrettyMIDI duration: float def __init__(self, id: int, file: str): self.id = id self.name = file.replace('\\', '/').rsplit('/', 1)[1].rsplit('.', 1)[0] self.midi = PrettyMIDI(file) self.duration = self.midi.get_end_time() def json(self): return { "id": self.id, "name": self.name, "length": self.duration }
def encoding_to_LSTM(midi_data: pretty_midi.PrettyMIDI, time_segments=False): """ The encoding for this data is specific to solo classical guitar pieces with no pinch harmonics nor percussive elements. The encoding for LSTM will be a np.ndarray of 50 rows and d columns, where there are d time steps. The first 44 rows will be for marking whether or not the corresponding pitch will be played. Row 0 will correspond with E2, the lowest note on classical guitar, row 1 will correspond with F2, row 2 will correspond with F#2, and so on, until row 43, which corresponds with B5, and is the highest non-harmonic note on classical guitar. The last 6 rows correspond with whether or not a specific note if plucked or held from the previous timestep. For example, if a 1 exists in row 44, then the lowest note found in above 44 rows is to be plucked in this timestep. If it is 0, then the lowest note found in the above 44 rows is held from the previous timestep. This is the same for rows 45-49, where each row corresponds with the 2nd-6th lowest note, respectively. The rationale for this part of the encoding is to differentiate between many of the same note being played at the same time and a not being held. Each timestep is 1/24 of a beat. This is to account for both notes that last 1/8 of a beat, and notes that last 1/3 of a beat. As most songs' shortest notes are roughly either 1/6 or 1/8 of a beat, this will account for both. Midi_data will be segmented by tempo and Time Signature. Sections less than 4 beats of constant tempo will be ignored. In addition to this, the number of timesteps into the song will be measured, so the beat matrix can be accurately aligned with it :param midi_data: A pretty_midi.PrettyMidi object to be encoded :param tempo: Tempo of pretty_midi object. Default is 100 :return: encoded_matrices: A list of encoded matrices """ # This will only work with midi data with a single instrument def find_attack_matrix(midi_data: pretty_midi.PrettyMIDI, vector: np.ndarray, piano_roll: np.ndarray, tempo: int): attack_matrix = np.zeros((6, vector.shape[0])) section_notes = lambda _note: _note.start >= vector[ 0] and _note.start <= vector[-1] notes = sorted(filter(section_notes, midi_data.instruments[0].notes), key=lambda x: x.pitch) section_start = vector[0] for note in notes: timestep = int( round((note.start - section_start) / 60 * tempo * beat_length, 0)) simultaneous_notes = np.sum(piano_roll[0:(note.pitch - E2), timestep]) attack_matrix[simultaneous_notes, timestep] = 1 return attack_matrix def adjust_end_times(instrument, inc): for note in instrument.notes: note.end -= inc note.start += inc return instrument def overlap_check(instrument, midi_matrix): # Sometimes, start and end times overlap, and cause more than 6 notes to be detected, this fixes that inc = 0 timestep = vector[1] - vector[0] max_simul = np.max(midi_matrix.sum(axis=0)) while max_simul > 6: if inc > 10: return None instrument = adjust_end_times(instrument, timestep / 10) midi_matrix = instrument.get_piano_roll(times=vector)[E2:B5 + 1, :] one_hot = np.vectorize(lambda x: np.int(x != 0)) midi_matrix = one_hot(midi_matrix) max_simul = np.max(midi_matrix.sum(axis=0)) inc += 1 return midi_matrix one_hot = np.vectorize(lambda x: np.int(x != 0)) def from_vector_to_matrix(vector: np.ndarray, tempo: int, instrument: pretty_midi.Instrument): # Right now, midi_matrix is a matrix of velocities. # Let's change this so midi matrix is a matrix of whether the note is played or not midi_matrix = one_hot( instrument.get_piano_roll(times=vector)[E2:B5 + 1, :]) midi_matrix = overlap_check(instrument, midi_matrix) if midi_matrix is None: return midi_matrix = one_hot(midi_matrix) attack_matrix = find_attack_matrix(midi_data, vector, midi_matrix, tempo) return np.append(midi_matrix, attack_matrix, axis=0) # First we want to find the locations in the midi track where the time signature changes beats_min = 4 tempo_change_times, tempi = midi_data.get_tempo_changes() time_signature_changes = sorted(midi_data.time_signature_changes, key=lambda sign: sign.time) # Changes marks every portion where either the tempo or time signature changes. # Each object in changes is a tuple with 3 values. # The first value is when the change occurs. The second value is the tempo at this change. # The Third Value is whether or not this was a tempo change changes = [] last_tempo = midi_data.estimate_tempo() i = 0 j = 0 while i + j < len(time_signature_changes) + tempo_change_times.shape[0]: if j == len(time_signature_changes) or \ (i != tempo_change_times.shape[0] and time_signature_changes[j].time >= tempo_change_times[i]): next_change = (tempo_change_times[i], tempi[i], True) last_tempo = tempi[i] i += 1 else: next_change = (time_signature_changes[j].time, last_tempo, False) j += 1 changes.append(next_change) curr_ts = 0 ts_passed = [] encoded_matrices = [] instrument = midi_data.instruments[0] # After we do so, we need to find the range vectors to pass into the function "PrettyMIDI.get_piano_roll" for the given # range and tempo if changes is None: encoded_matrices.append( from_vector_to_matrix( np.arange(0, midi_data.get_end_time(), 1 / (beat_length * last_tempo)), last_tempo, instrument)) ts_passed.append(0) else: for i in range(len(changes)): start_time = changes[i][0] end_time = changes[ i + 1][0] if i < len(changes) - 1 else midi_data.get_end_time() vector = np.arange(start_time, end_time, 1 / (changes[i][1] / 60 * beat_length))[:-1] if vector.shape[0] > beats_min * beat_length: next_matrix = from_vector_to_matrix(vector, changes[i][1], instrument) # In the case where a tempo change and a time signature change occur at the same time, the # Time signature change must appear after the tempo change, since we will always append # to matrices where only the tempo is different, but not so for the time signature if encoded_matrices and (not time_segments and not changes[i][2]) or changes[i][2]: encoded_matrices[-1] = np.append(encoded_matrices[-1], next_matrix, axis=1) else: encoded_matrices.append(next_matrix) ts_passed.append(curr_ts) curr_ts += vector.shape[0] return zip(encoded_matrices, ts_passed)
def extract_notes(midi_handler: pretty_midi.PrettyMIDI): print("Total ticks:", midi_handler.time_to_tick(midi_handler.get_end_time())) print("Time signatures:", midi_handler.time_signature_changes) print("Resolution:", midi_handler.resolution) new_mid_notes = [] avg_data = [] if len(midi_handler.time_signature_changes) == 0: num = 4 denom = 4 else: num = midi_handler.time_signature_changes[0].numerator denom = midi_handler.time_signature_changes[0].denominator resolution = midi_handler.resolution ticks_per_bar = num * (resolution / (denom / 4)) total_bars = int( midi_handler.time_to_tick(midi_handler.get_end_time()) // ticks_per_bar) for current_channel, instrument in enumerate(midi_handler.instruments): if instrument.is_drum: continue ch = [] avg_data_ch = {} bar = {} sum_pitch = 0 sum_dur = 0 current_bar = int( midi_handler.time_to_tick(instrument.notes[0].start) // ticks_per_bar) for index, note in enumerate(instrument.notes): starting_tick = midi_handler.time_to_tick(note.start) nro_bar = int(starting_tick // ticks_per_bar) if nro_bar != current_bar: notes_per_bar = len(bar.keys()) avg_data_ch[current_bar] = (sum_pitch / notes_per_bar, sum_dur / notes_per_bar) ch.append(bar) bar = {} current_bar = nro_bar sum_pitch = sum_dur = 0 if starting_tick not in bar.keys(): # We substract 12 pitch levels if # the note belongs to a different clef sum_pitch += note.pitch if note.pitch < 60 else (note.pitch - 13) sum_dur += note.get_duration() bar[starting_tick] = (note.pitch, current_channel, nro_bar, midi_handler.time_to_tick(note.end), midi_handler.time_to_tick(note.duration), note.velocity) else: # If the current note overlaps with a previous one # (they play at the same time/tick) # we will keep the one with the highest pitch new_pitch = note.pitch if note.pitch < 60 else (note.pitch - 13) old_pitch = bar[starting_tick][0] if bar[starting_tick][ 0] < 60 else (bar[starting_tick][0] - 13) if new_pitch > old_pitch: old_duration = midi_handler.tick_to_time( bar[starting_tick][4]) sum_pitch -= old_pitch sum_dur -= old_duration sum_pitch += new_pitch sum_dur += note.get_duration() bar[starting_tick] = (note.pitch, current_channel, nro_bar, midi_handler.time_to_tick(note.end), midi_handler.time_to_tick( note.duration), note.velocity) notes_per_bar = len(bar.keys()) avg_data_ch[current_bar] = (sum_pitch / notes_per_bar, sum_dur / notes_per_bar) ch.append(bar) new_mid_notes.append(ch) avg_data.append(avg_data_ch) return [avg_data, new_mid_notes, total_bars]