TastyPiano / src /music /pipeline /midi2processed.py
ccolas's picture
Update src/music/pipeline/midi2processed.py
8060600
raw
history blame
6.01 kB
import time
import os
import sys
sys.path.append('../../')
import pretty_midi as pm
import numpy as np
from src.music.utils import get_out_path
from src.music.config import MIN_LEN, MIN_NB_NOTES, MAX_GAP_IN_SONG, REMOVE_FIRST_AND_LAST
def sort_notes(notes):
starts = np.array([n.start for n in notes])
index_sorted = np.argsort(starts)
return [notes[i] for i in index_sorted].copy()
def delete_notes_end_after_start(notes):
indexes_to_keep = [i for i, n in enumerate(notes) if n.start < n.end]
return [notes[i] for i in indexes_to_keep].copy()
def compute_largest_gap(notes):
gaps = []
latest_note_end_so_far = notes[0].end
for i in range(len(notes) - 1):
note_start = notes[i + 1].start
if latest_note_end_so_far < note_start:
gaps.append(note_start - latest_note_end_so_far)
latest_note_end_so_far = max(latest_note_end_so_far, notes[i+1].end)
if len(gaps) > 0:
largest_gap = np.max(gaps)
else:
largest_gap = 0
return largest_gap
def analyze_instrument(inst):
# test that piano plays throughout
init = time.time()
notes = inst.notes.copy()
nb_notes = len(notes)
start = notes[0].start
end = inst.get_end_time()
duration = end - start
largest_gap = compute_largest_gap(notes)
return nb_notes, start, end, duration, largest_gap
def remove_beginning_and_end(midi, end_time):
notes = midi.instruments[0].notes.copy()
new_notes = [n for n in notes if n.start > REMOVE_FIRST_AND_LAST and n.end < end_time - REMOVE_FIRST_AND_LAST]
midi.instruments[0].notes = new_notes
return midi
def remove_blanks_beginning_and_end(midi):
# remove blanks and the beginning and the end
shift = midi.instruments[0].notes[0].start
for n in midi.instruments[0].notes:
n.start = max(0, n.start - shift)
n.end = max(0, n.end - shift)
for ksc in midi.key_signature_changes:
ksc.time = max(0, ksc.time - shift)
for tsc in midi.time_signature_changes:
tsc.time = max(0, tsc.time - shift)
for pb in midi.instruments[0].pitch_bends:
pb.time = max(0, pb.time - shift)
for cc in midi.instruments[0].control_changes:
cc.time = max(0, cc.time - shift)
return midi
def is_valid_inst(largest_gap, duration, nb_notes, gap_counts=True):
error_msg = ''
valid = True
if largest_gap > MAX_GAP_IN_SONG and gap_counts:
valid = False
error_msg += f'wide gap ({largest_gap:.2f} secs), '
if duration < (MIN_LEN + 2 * REMOVE_FIRST_AND_LAST):
valid = False
error_msg += f'too short ({duration:.2f} secs), '
if nb_notes < MIN_NB_NOTES * duration / 60: # nb of notes needs to be superior to the minimum number / min * the duration in minute
valid = False
error_msg += f'too few notes ({nb_notes}), '
return valid, error_msg
def midi2processed(midi_path, processed_path=None, apply_filtering=True, verbose=False, level=0):
assert midi_path.split('.')[-1] in ['mid', 'midi']
if not processed_path:
processed_path, _, _ = get_out_path(in_path=midi_path, in_word='midi', out_word='processed', out_extension='.mid')
if verbose: print(' ' * level + f'Processing {midi_path}.')
if os.path.exists(processed_path):
if verbose: print(' ' * (level + 2) + 'Processed midi file already exists.')
return processed_path, ''
error_msg = 'Error in scrubbing. '
# try:
inst_error_msg = ''
# load mid file
error_msg += 'Error in midi loading?'
midi = pm.PrettyMIDI(midi_path)
error_msg += ' Nope. Removing invalid notes?'
midi.remove_invalid_notes() # filter invalid notes
error_msg += ' Nope. Filtering instruments?'
# filter instruments
instruments = midi.instruments.copy()
new_instru = []
instruments_data = []
for i_inst, inst in enumerate(instruments):
if inst.program <= 7 and not inst.is_drum and len(inst.notes) > 5:
# inst is a piano
# check data
inst.notes = sort_notes(inst.notes) # sort notes
inst.notes = delete_notes_end_after_start(inst.notes) # delete invalid notes
nb_notes, start, end, duration, largest_gap = analyze_instrument(inst)
is_valid, err_msg = is_valid_inst(largest_gap=largest_gap, duration=duration, nb_notes=nb_notes, gap_counts='maestro' not in midi_path)
if is_valid or not apply_filtering:
new_instru.append(inst)
instruments_data.append([nb_notes, start, end, duration, largest_gap])
else:
inst_error_msg += 'inst1: ' + err_msg + '\n'
instruments_data = np.array(instruments_data)
error_msg += ' Nope. Taking one instrument?'
if len(new_instru) == 0:
error_msg = f'No piano instrument. {inst_error_msg}'
assert False
elif len(new_instru) > 1:
# take instrument playing the most notes
instrument = new_instru[np.argmax(instruments_data[:, 0])]
else:
instrument = new_instru[0]
instrument.program = 0 # set the instrument to Grand Piano.
midi.instruments = [instrument] # put instrument in midi file
error_msg += ' Nope. Removing blanks?'
# remove first and last REMOVE_FIRST_AND_LAST seconds (avoid clapping and jingles)
end_time = midi.get_end_time()
if apply_filtering: midi = remove_beginning_and_end(midi, end_time)
# remove beginning and end
midi = remove_blanks_beginning_and_end(midi)
error_msg += ' Nope. Saving?'
# save midi file
midi.write(processed_path)
error_msg += ' Nope.'
if verbose:
extra = f' Saved to {processed_path}' if midi_path else ''
print(' ' * (level + 2) + f'Success! {extra}')
return processed_path, ''
#except:
# if verbose: print(' ' * (level + 2) + 'Scrubbing failed.')
# if os.path.exists(processed_path):
# os.remove(processed_path)
# return None, error_msg + ' Yes.'