File size: 12,849 Bytes
a03c9b4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
"""preprocess_rwc_pop.py"""
import os
import glob
import re
import json
import csv
from typing import Dict, List, Any, Tuple
import numpy as np
from utils.audio import get_audio_file_info, load_audio_file
from utils.midi import midi2note, note_event2midi
from utils.note2event import note2note_event, sort_notes, validate_notes, trim_overlapping_notes, extract_program_from_notes
from utils.event2note import event2note_event
from utils.note_event_dataclasses import Note, NoteEvent
from utils.utils import note_event2token2note_event_sanity_check
from mido import MetaMessage, Message, MidiFile, MidiTrack

# UNUSED_IDS = ["010", "071", "099", "023", "034", "036", "038", "049", "060", "062"]
# UNUSED_IDS = ["071", "099", "049", "060", "062"]
UNUSED_IDS = []

DRUM_CHANNEL = 9  # all drums are in channel 9 in geerdes dataset
DRUM_PROGRAM = 128
SINGING_VOICE_PROGRAM = 100
SINGING_VOICE_CHORUS_PROGRAM = 101
TRACK_NAME_TO_PROGRAM_MAP = { # compared by exact match of lowercase
    "Singing Voice": SINGING_VOICE_PROGRAM,
    "Singing Voice (Chorus)": SINGING_VOICE_CHORUS_PROGRAM,
    "Drums": DRUM_PROGRAM,
}

# yapf: disable
TRACK_NAME_FILTERS = {
    SINGING_VOICE_PROGRAM: {"include": ["MELO", "VOCAL"], "exclude": ["SUB", "GT"]},
    SINGING_VOICE_CHORUS_PROGRAM: {"include": ["CHORUS", "SUB VOCAL", "SUB MELO"],
                                   "exclude": ["/", "GT"]},
    DRUM_PROGRAM: {"include": ["DRUMS", "DR", "HIHAT", "BD&SD", "TOM", "KICK"],
                   "exclude": ["ATOMOS"], "exact": ["DS"]},
    0: {"include": ["P.F.", "PF", "PIANO", "A.P", "CLAV", "CEMBAL", "HARPSI"], "exclude": ["E.PIANO", "MARIMBA"]},
    2: {"include": ["E.P"], "exclude": []},
    8: {"include": ["GLOCKEN", "VIBRA", "VIBE", "MARIMBA", "BELL", "CHIME", "CHAIM", "KALIMB", "CHIMRE", "MALLET"],
        "exclude": []},
    16: {"include": ["ORG", "HAMO", "HARMONICA", "ACCORD"], "exclude": []},
    24: {"include": ["MANDORIN", "AG", "NYLON", "AC.G", "GUITAR", "A.G", "E.G", "GT", "G. SOLO", "CLEAN LEAD", "SITAR", "ATOMOS", "ATMOS",
                      "CLEAN"],
         "exclude": ["DIST", "DIS.", "D.", "E.G SOLO", "E.G.SOLO"]},
    30: {"include": ["OD L", "OD R", "DIS.", "DIST GT", "D.G", "DIST", "DIS.SOLO", "E.GUITAR (SOLO)", "E.G SOLO", "LEAD", "E.G.SOLO", "EG", "GT MELO"],
        "exclude": ["PAD","SYN.LEAD"]},
    33: {"include": ["BASS"], "exclude": []},
    48: {"include": ["OR 2", "ST", "STR", "ORCH", "PIZZ", "HIT", "TIMPANI", "VIORA", "VIOLA", "VIOLIN", "VN", "VA", "VC", "HARP", "LO FI", "CHO", "VLN", "CELLO"],
          "exclude": ["CHORUS", "HARPSI", "STEEL", "GUITAR", "PAD", "BRASS", "GT", "HORN"],
          "exact": ["OR"]},
    56: {"include": ["BRAS", "TRUMP", "TP", "TB", "TROM", "HORN", "FLUGEL"], "exclude": []},
    64: {"include": ["SAX", "OBOE", "BASS"], "exclude": ["SYNSAX"]},
    72: {"include": ["FLUTE", "PICO", "BOTTLE", "GAYA"], "exclude": []},
    80: {"include": ["S SOLO", "SYN SOLO", "SOLO SYNTH", "SYNTH SOLO", "SYN.LEAD", "SYNTH(SEQ)", "PORTASYN", "SQ", "SEQ", "VOICE"], "exclude": []},
    88: {"include": ["SYNTH", "SYN", "PAD", "FANTASIA", "BRIGHTNESS", "FANTASY"], "exclude": ["SYNBELL", "PORTA", "SOLO", "SEQ", "LEAD", "ORGAN", "BRAS", "BASS", "TROM"]},
    None: {"include": ["INTRO SE", "WOW", "PERC", "EXC", "REVERSE", "GONG", "PER.", "RAP", "REV", "S.E", "LASER",
                        "LESER", "TAMBOURINE", "KANE", "PER", "SHAKER", "RWC-MDB"],
           "exclude": [],
           "exact": ["SE", "EX", "808", "ICERAIN"]},
    "USE RWC PROGRAM MAP": {"include": ["KIRA", "KILA", "ETHNIC&GK"], "exclude": [], "exact": ["FUE", "OU-01A"]},
}
# yapf: enable
RWC_PROGRAM_MAP = {
    9: 8,
    11: 8,
    74: 72,
    94: 80,
    98: 88,
    100: 88,
}

PRG2CH = {
    0: (0, "Acoustic Piano"),
    2: (1, "Electric Piano"),
    8: (2, "Chromatic Percussion"),
    16: (3, "Organ"),
    24: (4, "Guitar (clean)"),
    30: (5, "Guitar (distortion)"),
    33: (6, "Bass"),
    48: (7, "Strings"),
    56: (8, "Brass"),
    DRUM_PROGRAM: (9, "Drums"),
    64: (10, "Reed"),
    72: (11, "Pipe"),
    80: (12, "Synth Lead"),
    88: (13, "Synth Pad"),
    SINGING_VOICE_PROGRAM: (14, "Singing Voice"),
    SINGING_VOICE_CHORUS_PROGRAM: (15, "Singing Voice (Chorus)"),
}


def find_matching_filters(input_text, filters):
    input_text = input_text.upper()

    def text_matches_filter(text, filter_dict):
        matchness = False
        if "exact" in filter_dict:
            for keyword in filter_dict["exact"]:
                if keyword == text:
                    matchness = True
                    break
        for keyword in filter_dict["include"]:
            if keyword in text:
                matchness = True
                break
        for keyword in filter_dict["exclude"]:
            if keyword in text:
                matchness = False
                break
        return matchness

    matching_filters = []
    for filter_name, filter_dict in filters.items():
        if text_matches_filter(input_text, filter_dict):
            matching_filters.append(filter_name)
    return matching_filters


def generate_corrected_midi(org_mid_file: os.PathLike,
                            new_mid_file: os.PathLike,
                            filters: Dict[Any, Dict[str, List]],
                            prg2ch: Dict[int, Tuple[int, str]]):
    # Load original MIDI file
    org_mid = MidiFile(org_mid_file)

    # Create a new MIDI file
    new_mid = MidiFile(ticks_per_beat=org_mid.ticks_per_beat)

    # Extract global messages from the first track (usually the master track)
    global_messages = [msg for msg in org_mid.tracks[0] if msg.is_meta]
    global_track = MidiTrack(global_messages)
    new_mid.tracks.append(global_track)

    # Loop over all tracks
    for track in org_mid.tracks[1:]:
        # Get track name
        track_name = None
        for msg in track:
            if msg.type == 'track_name':
                track_name = msg.name
                break
        if track_name is None:
            raise ValueError('track name not found in midi file')

        # Get program number from track name
        matching_filters = find_matching_filters(track_name, filters)
        assert (len(matching_filters) != 0)
        if isinstance(matching_filters[0], int):
            program = matching_filters[0]
        elif matching_filters[0] == "USE RWC PROGRAM MAP":
            for msg in track:
                if msg.type == 'program_change':
                    program = RWC_PROGRAM_MAP.get(msg.program, msg.program)
                    break
        elif matching_filters[0] == None:
            continue

        # Get channel and new track name
        ch, new_track_name = prg2ch[program]

        # Copy messages to new track with new program, channel, and track_name
        new_track = MidiTrack()
        new_track.append(MetaMessage('track_name', name=new_track_name,
                                     time=0))
        if program == DRUM_PROGRAM:
            new_track.append(
                Message('program_change', program=0, time=0, channel=9))
        else:
            new_track.append(
                Message('program_change', program=program, time=0, channel=ch))
        new_mid.tracks.append(new_track)

        for msg in track:
            if msg.type in ['track_name', 'instrument_name', 'program_change']:
                continue
            else:
                new_msg = msg.copy()
                if hasattr(msg, 'channel'):
                    new_msg.channel = ch
                new_track.append(new_msg)

    # Save new MIDI file
    new_mid.save(new_mid_file)
    print(f'Created {new_mid_file}')


def check_file_existence(file: str) -> bool:
    """Checks if file exists."""
    res = True
    if not os.path.exists(file):
        res = False
    elif get_audio_file_info(file)[1] < 10 * 16000:
        print(f'File {file} is too short.')
        res = False
    return res


def create_note_event_and_note_from_midi(
        mid_file: str,
        id: str,
        ch_9_as_drum: bool = False,
        track_name_to_program: Dict = None,
        ignore_pedal: bool = False) -> Tuple[Dict, Dict]:
    """Create note_events and notes from midi file."""

    # Load midi file
    notes, dur_sec, program = midi2note(
        mid_file,
        ch_9_as_drum=ch_9_as_drum,
        track_name_to_program=track_name_to_program,
        binary_velocity=True,
        ignore_pedal=ignore_pedal,
        return_programs=True)
    program = [x for x in set(program)
               if x is not None]  # remove None and duplicates
    return { # notes
        'rwc_pop_id': id,
        'program': program,
        'is_drum': [1 if p == DRUM_PROGRAM else 0 for p in program],
        'duration_sec': dur_sec,
        'notes': notes,
    }, { # note_events
        'rwc_pop_id': id,
        'program': program,
        'is_drum': [1 if p == DRUM_PROGRAM else 0 for p in program],
        'duration_sec': dur_sec,
        'note_events': note2note_event(notes),
    }


def preprocess_rwc_pop_full16k(data_home='../../data',
                               dataset_name='rwc_pop') -> None:
    # Directory and file paths
    base_dir = os.path.join(data_home, dataset_name + '_yourmt3_16k')
    output_index_dir = os.path.join(data_home, 'yourmt3_indexes')
    os.makedirs(output_index_dir, exist_ok=True)

    # Load CSV: construct id to midi/wav dictionary
    csv_file = os.path.join(base_dir, 'wav_to_midi_filename_mapping.csv')
    rwc_all = {}
    with open(csv_file, 'r') as f:
        reader = csv.reader(f)
        headers = next(reader)

        for row in reader:
            id = row[2]
            mix_audio_file = os.path.join(base_dir, headers[0] + row[0],
                                          row[1] + ' ' + headers[1] + '.wav')
            assert check_file_existence(mix_audio_file)
            mid_file = os.path.join(base_dir, 'MIDI', id + '.mid')
            assert os.path.exists(mid_file)
            notes_file = mid_file.replace('.mid', '_notes.npy')
            note_events_file = mid_file.replace('.mid', '_note_events.npy')

            rwc_all[id] = {
                'rwc_pop_id': id,
                'n_frames': get_audio_file_info(mix_audio_file)[1],
                'mix_audio_file': mix_audio_file,
                'notes_file': notes_file,
                'note_events_file': note_events_file,
                'midi_file': mid_file,
                'program': None,
                'is_drum': None,
            }
    assert len(rwc_all) == 100

    # Generate corrected MIDI files by reassigning program numbers
    os.makedirs(os.path.join(base_dir, 'MIDI_full_corrected'), exist_ok=True)
    for id, info in rwc_all.items():
        org_mid_file = info['midi_file']
        new_mid_file = org_mid_file.replace('/MIDI/', '/MIDI_full_corrected/')
        generate_corrected_midi(org_mid_file,
                                new_mid_file,
                                filters=TRACK_NAME_FILTERS,
                                prg2ch=PRG2CH)
        # Update file path with corrected MIDI file
        rwc_all[id]['midi_file'] = new_mid_file
        rwc_all[id]['notes_file'] = new_mid_file.replace('.mid', '_notes.npy')
        rwc_all[id]['note_events_file'] = new_mid_file.replace(
            '.mid', '_note_events.npy')

    # Unused ids
    for id in UNUSED_IDS:
        rwc_all.pop(str(id))
    print(f'Number of used IDs: {len(rwc_all)}, Unused ids: {UNUSED_IDS}')

    # Create note and note_event files
    for id in rwc_all.keys():
        midi_file = rwc_all[id]['midi_file']
        notes_file = rwc_all[id]['notes_file']
        note_events_file = rwc_all[id]['note_events_file']

        # Create note and note_event files
        notes, note_events = create_note_event_and_note_from_midi(
            midi_file,
            id,
            ch_9_as_drum=False,  # we will use track_name_to_program instead
            track_name_to_program=TRACK_NAME_TO_PROGRAM_MAP,
            ignore_pedal=False)

        # Update programs and is_drum
        rwc_all[id]['program'] = notes['program']
        rwc_all[id]['is_drum'] = notes['is_drum']

        # Save note and note_event files
        np.save(notes_file, notes, allow_pickle=True, fix_imports=False)
        print(f'Created {notes_file}')
        np.save(note_events_file,
                note_events,
                allow_pickle=True,
                fix_imports=False)
        print(f'Created {note_events_file}')

    # Save index file
    split = 'full'
    output_index_file = os.path.join(output_index_dir,
                                     f'rwc_pop_{split}_file_list.json')

    file_list = {}
    for i, id in enumerate(rwc_all.keys()):
        file_list[i] = rwc_all[id]

    with open(output_index_file, 'w') as f:
        json.dump(file_list, f, indent=4)
    print(f'Created {output_index_file}')