File size: 6,200 Bytes
e775f6d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
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.'