Example #1
0
 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
Example #2
0
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
Example #4
0
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
    }
Example #5
0
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)
Example #6
0
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]