Spaces:
Runtime error
Runtime error
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.' | |