Spaces:
Runtime error
Runtime error
""" Structured MIDI encoding method as using in the Piano Inpainting Application | |
https://arxiv.org/abs/2107.05944 | |
""" | |
from typing import List, Tuple, Dict, Optional | |
import numpy as np | |
from miditoolkit import Instrument, Note, TempoChange | |
from miditok import Structured | |
from miditok.midi_tokenizer_base import MIDITokenizer, Vocabulary, Event | |
from miditok.constants import * | |
from itertools import combinations | |
Cs = np.array([60 + oct for oct in range(-12*4, 12*5, 12)]) | |
def get_chord_map(): | |
my_chord_map = {#'octave': (0, 12), | |
#'power': (0, 7), | |
#'power_inv_1': (0, 5), | |
'min': (0, 3, 7), | |
'maj': (0, 4, 7), | |
'dim': (0, 3, 6), | |
'aug': (0, 4, 8), | |
'sus2': (0, 2, 7), | |
'sus4': (0, 5, 7), | |
'7dom': (0, 4, 7, 10), | |
'7min': (0, 3, 7, 10), | |
'7maj': (0, 4, 7, 11), | |
'7halfdim': (0, 3, 6, 10), | |
'7dim': (0, 3, 6, 9), | |
'7aug': (0, 4, 8, 11), | |
'9maj': (0, 4, 7, 10, 14), | |
'9min': (0, 4, 7, 10, 13)} | |
# | |
for k in list(my_chord_map.keys()).copy(): | |
n_notes = len(my_chord_map[k]) | |
if n_notes > 2: | |
if k not in ['7dim', 'aug', 'sus2', 'sus4']: | |
if '9' in k: | |
nb_invs = 3 | |
else: | |
nb_invs = n_notes | |
for i_inv in range(1, nb_invs): | |
shift = np.array([my_chord_map[k][(i + i_inv) % n_notes] for i in range(n_notes)]) | |
shift[-i_inv:] += 12 | |
pattern = [0] | |
for i in range(1, len(shift)): | |
pattern.append(shift[i] - shift[0]) | |
my_chord_map[k + f'_inv_{i_inv}'] = tuple(pattern) | |
known = set() | |
for k in my_chord_map.keys(): | |
assert my_chord_map[k] not in known | |
inverted_chord_map = dict() | |
for k, v in my_chord_map.items(): | |
inverted_chord_map[v] = k | |
return my_chord_map, inverted_chord_map | |
def find_sub_pattern(pattern, candidate_patterns): | |
for i in np.arange(len(pattern) - 1, 0, -1): | |
patt_indexes = [(0,) + c for c in combinations(range(1, len(pattern)), i)] | |
for p_ind in patt_indexes: | |
sorted_pattern = np.sort(np.array(pattern)[np.array(p_ind)]) | |
sorted_pattern = tuple(sorted_pattern - sorted_pattern[0]) | |
if sorted_pattern in candidate_patterns: | |
return True, sorted_pattern, np.array(p_ind) | |
return False, None, None | |
# def find_sub_pattern(pattern, candidate_patterns, indexes, n_asserted=1): | |
# if len(candidate_patterns) == 0 or len(pattern) < 3: | |
# return False, None, None | |
# else: | |
# sorted_pattern = np.sort(pattern) | |
# sorted_pattern = tuple(sorted_pattern - sorted_pattern[0]) | |
# if sorted_pattern in candidate_patterns: | |
# return True, sorted_pattern, indexes | |
# else: | |
# if n_asserted + 1 == len(pattern): | |
# return False, None, None | |
# else: | |
# # hypothesis that pattern is good up to n_asserted + 1 | |
# asserted_pattern = pattern[:n_asserted + 1] | |
# len_asserted = len(asserted_pattern) | |
# # find candidate patterns matching that beginning | |
# sorted_asserted_pattern = np.sort(asserted_pattern) | |
# sorted_asserted_pattern = tuple(sorted_asserted_pattern - sorted_asserted_pattern[0]) | |
# c_p = [cp for cp in candidate_patterns if cp[:len_asserted] == sorted_asserted_pattern] | |
# found, found_pattern, found_indexes = find_sub_pattern(pattern, c_p, indexes, n_asserted=n_asserted+1) | |
# if found: | |
# return True, found_pattern, found_indexes | |
# # if the pattern was not found, then we need to remove that note | |
# else: | |
# pattern2 = pattern[: n_asserted] + pattern[n_asserted + 1:] | |
# if pattern2 == pattern: | |
# stop = 1 | |
# new_indexes = indexes.copy() | |
# new_indexes.pop(n_asserted) | |
# return find_sub_pattern(pattern2, candidate_patterns, new_indexes, n_asserted=n_asserted) | |
def filter_notes_find_chord_and_root(chord, inverted_chord_map): | |
known_chords = list(inverted_chord_map.keys()) | |
found, chord_pattern, chord_indexes = find_sub_pattern(tuple(chord), known_chords) | |
if found: | |
chord_id = inverted_chord_map[chord_pattern].split('_')[0] | |
else: | |
return False, None, None, None | |
# find root now :) | |
if 'inv' not in inverted_chord_map[chord_pattern]: | |
root_id = 0 | |
else: | |
inv_id = int(inverted_chord_map[chord_pattern].split('_')[-1]) | |
n_notes = len(chord_pattern) | |
root_id = n_notes - inv_id | |
return True, chord_id, root_id, chord_indexes | |
class ChordStructured(MIDITokenizer): | |
""" Structured MIDI encoding method as using in the Piano Inpainting Application | |
https://arxiv.org/abs/2107.05944 | |
The token types follows the specific pattern: | |
Pitch -> Velocity -> Duration -> Time Shift -> back to Pitch ... | |
NOTE: this encoding uses only "Time Shifts" events to move in the time, and only | |
from one note to another. Hence it is suitable to encode continuous sequences of | |
notes without long periods of silence. If your dataset contains music with long | |
pauses, you might handle them with an appropriate "time shift" dictionary | |
(which values are made from the beat_res dict) or with a different encoding. | |
:param pitch_range: range of used MIDI pitches | |
:param beat_res: beat resolutions, with the form: | |
{(beat_x1, beat_x2): beat_res_1, (beat_x2, beat_x3): beat_res_2, ...} | |
The keys of the dict are tuples indicating a range of beats, ex 0 to 3 for the first bar | |
The values are the resolution, in samples per beat, of the given range, ex 8 | |
:param nb_velocities: number of velocity bins | |
:param program_tokens: will add entries for MIDI programs in the dictionary, to use | |
in the case of multitrack generation for instance | |
:param sos_eos_tokens: Adds Start Of Sequence (SOS) and End Of Sequence (EOS) tokens to the vocabulary | |
:param params: can be a path to the parameter (json encoded) file or a dictionary | |
""" | |
def __init__(self, pitch_range: range = PITCH_RANGE, beat_res: Dict[Tuple[int, int], int] = BEAT_RES, | |
nb_velocities: int = NB_VELOCITIES, program_tokens: bool = ADDITIONAL_TOKENS['Program'], | |
sos_eos_tokens: bool = False, params=None): | |
# No additional tokens | |
additional_tokens = {'Chord': False, 'Rest': False, 'Tempo': False, 'TimeSignature': False, 'Program': program_tokens} | |
self.pitch2octave_relative = dict() | |
self.octave_relative2pitch = dict() | |
for p in pitch_range: | |
self.pitch2octave_relative[p] = self.get_octave_and_relative(p) | |
self.octave_relative2pitch[self.pitch2octave_relative[p]] = p | |
self.chord_maps, self.inverted_chord_map = get_chord_map() | |
super().__init__(pitch_range, beat_res, nb_velocities, additional_tokens, sos_eos_tokens, params) | |
def get_octave_and_relative(self, pitch): | |
octave = np.argwhere(pitch - Cs >=0).flatten()[-1] | |
relative = pitch - Cs[octave] | |
return octave, relative | |
def get_note_events(self, note, dur_bins, next_note_start): | |
events = [] | |
if isinstance(note.pitch, str): # it's a chord | |
chord_id = '_'.join(note.pitch.split('_')[:-1]) | |
pitch = int(note.pitch.split('_')[-1]) | |
else: # it's a note | |
chord_id = 'note' | |
pitch = note.pitch | |
# get octave and relative position of the pitch (root pitch for a chord) | |
octave, relative = self.pitch2octave_relative[pitch] | |
# Add chord/note event. A note is defined as Chord_note | |
events.append(Event(type_='Chord', time=note.start, value=chord_id, desc=note.pitch)) | |
# Add octave of the root | |
events.append(Event(type_='OctavePitch', time=note.start, value=octave, desc=note.pitch)) | |
# Add octave relative pitch of the root | |
events.append(Event(type_='RelativePitch', time=note.start, value=relative, desc=note.pitch)) | |
# Velocity | |
events.append(Event(type_='Velocity', time=note.start, value=note.velocity, desc=f'{note.velocity}')) | |
# Duration | |
duration = note.end - note.start | |
index = np.argmin(np.abs(dur_bins - duration)) | |
events.append(Event(type_='Duration', time=note.start, value='.'.join(map(str, self.durations[index])), desc=f'{duration} ticks')) | |
# Time-Shift | |
time_shift = next_note_start - note.start | |
assert time_shift >= 0 # this asserts that events are sorted | |
index = np.argmin(np.abs(dur_bins - time_shift)) | |
events.append(Event(type_='Time-Shift', time=note.start, desc=f'{time_shift} ticks', | |
value='.'.join(map(str, self.durations[index])) if time_shift != 0 else '0.0.1')) | |
return events, time_shift | |
def track_to_tokens(self, track: Instrument) -> List[int]: | |
""" Converts a track (miditoolkit.Instrument object) into a sequence of tokens | |
:param track: MIDI track to convert | |
:return: sequence of corresponding tokens | |
""" | |
# Make sure the notes are sorted first by their onset (start) times, second by pitch | |
# notes.sort(key=lambda x: (x.start, x.pitch)) # done in midi_to_tokens | |
events = [] | |
dur_bins = self.durations_ticks[self.current_midi_metadata['time_division']] | |
# assume first note is the beginning of the song, no time shift at first. | |
# Track chords. For each chord, insert a fake note that contains its info so that it can be converted to the proper event | |
if self.additional_tokens['Chord'] and not track.is_drum: | |
notes_and_chords = self.detect_chords(track.notes, self.current_midi_metadata['time_division'], self._first_beat_res) | |
else: | |
notes_and_chords = track.notes | |
sum_shifts = 0 | |
# Creates the Pitch, Velocity, Duration and Time Shift events | |
for n, note in enumerate(notes_and_chords): | |
if n == len(notes_and_chords) - 1: | |
next_note_start = note.start # add zero time shift at the end | |
else: | |
next_note_start = notes_and_chords[n + 1].start | |
new_events, time_shift = self.get_note_events(note, dur_bins, next_note_start=next_note_start) | |
events += new_events | |
sum_shifts += time_shift | |
assert len(events) // 6 == len(notes_and_chords) | |
return self.events_to_tokens(events) | |
def tokens_to_track(self, tokens: List[int], time_division: Optional[int] = TIME_DIVISION, | |
program: Optional[Tuple[int, bool]] = (0, False)) -> Tuple[Instrument, List[TempoChange]]: | |
""" Converts a sequence of tokens into a track object | |
:param tokens: sequence of tokens to convert | |
:param time_division: MIDI time division / resolution, in ticks/beat (of the MIDI to create) | |
:param program: the MIDI program of the produced track and if it drum, (default (0, False), piano) | |
:return: the miditoolkit instrument object and a "Dummy" tempo change | |
""" | |
events = self.tokens_to_events(tokens) | |
instrument = Instrument(program[0], is_drum=False, name=MIDI_INSTRUMENTS[program[0]]['name']) | |
current_tick = 0 | |
count = 0 | |
# start at first chord event | |
while count < len(events) and events[count].type != 'Chord': | |
count += 1 | |
while count < len(events): | |
if events[count].type == 'Chord': | |
note_chord_events = [events[c] for c in range(count, count + 6)] | |
events_types = [c.type for c in note_chord_events] | |
if events_types[1:] == ['OctavePitch', 'RelativePitch', 'Velocity', 'Duration', 'Time-Shift']: | |
octave, relative = int(note_chord_events[1].value), int(note_chord_events[2].value) | |
duration = self._token_duration_to_ticks(note_chord_events[4].value, time_division) | |
vel = int(note_chord_events[3].value) | |
root_pitch = self.octave_relative2pitch[(octave, relative)] | |
if note_chord_events[0].value == "note": | |
# pass | |
instrument.notes.append(Note(vel, root_pitch, current_tick, current_tick + duration)) | |
else: | |
pitches = self.find_chord_pitches(root_pitch, note_chord_events[0].value) | |
for p in pitches: | |
instrument.notes.append(Note(vel, p, current_tick, current_tick + duration)) | |
beat, pos, res = map(int, note_chord_events[5].value.split('.')) | |
current_tick += (beat * res + pos) * time_division // res # time shift | |
count += 6 | |
else: | |
count += 1 | |
else: | |
count += 1 | |
return instrument, [TempoChange(TEMPO, 0)] | |
def find_chord_pitches(self, root_pitch, chord_name): | |
chord_map = self.chord_maps[chord_name] | |
if 'inv' not in chord_map: | |
root_position = 0 | |
else: | |
inv_id = int(chord_name.split('_')[-1]) | |
n_notes = len(chord_map) | |
root_position = n_notes - inv_id | |
deltas = np.array(chord_map) - chord_map[root_position] | |
pitches = [root_pitch + d for d in deltas] | |
return pitches | |
def _create_vocabulary(self, sos_eos_tokens: bool = False) -> Vocabulary: | |
""" Creates the Vocabulary object of the tokenizer. | |
See the docstring of the Vocabulary class for more details about how to use it. | |
NOTE: token index 0 is often used as a padding index during training | |
:param sos_eos_tokens: will include Start Of Sequence (SOS) and End Of Sequence (tokens) | |
:return: the vocabulary object | |
""" | |
vocab = Vocabulary({'PAD_None': 0}) | |
if self.additional_tokens['Chord']: | |
vocab.add_event(f'Chord_{chord_quality}' for chord_quality in CHORD_MAPS) | |
# PITCH | |
vocab.add_event('Chord_note') | |
vocab.add_event(f'OctavePitch_{i}' for i in range(8)) | |
vocab.add_event(f'RelativePitch_{i}' for i in range(12)) | |
# vocab.add_event(f'Pitch_{i}' for i in self.pitch_range) | |
# VELOCITY | |
vocab.add_event(f'Velocity_{i}' for i in self.velocities) | |
# DURATION | |
vocab.add_event(f'Duration_{".".join(map(str, duration))}' for duration in self.durations) | |
# TIME SHIFT (same as durations) | |
vocab.add_event('Time-Shift_0.0.1') # for a time shift of 0 | |
vocab.add_event(f'Time-Shift_{".".join(map(str, duration))}' for duration in self.durations) | |
# PROGRAM | |
if self.additional_tokens['Program']: | |
vocab.add_event(f'Program_{program}' for program in range(-1, 128)) | |
# SOS & EOS | |
if sos_eos_tokens: | |
vocab.add_sos_eos_to_vocab() | |
return vocab | |
def _create_token_types_graph(self) -> Dict[str, List[str]]: | |
""" Returns a graph (as a dictionary) of the possible token | |
types successions. | |
NOTE: Program type is not referenced here, you can add it manually by | |
modifying the tokens_types_graph class attribute following your strategy. | |
:return: the token types transitions dictionary | |
""" | |
dic = {'Pitch': ['Velocity'], 'Velocity': ['Duration'], 'Duration': ['Time-Shift'], 'Time-Shift': ['Pitch']} | |
self._add_pad_type_to_graph(dic) | |
return dic | |
def token_types_errors(self, tokens: List[int], consider_pad: bool = False) -> float: | |
""" Checks if a sequence of tokens is constituted of good token types | |
successions and returns the error ratio (lower is better). | |
The Pitch values are also analyzed: | |
- a pitch token should not be present if the same pitch is already played at the time | |
:param tokens: sequence of tokens to check | |
:param consider_pad: if True will continue the error detection after the first PAD token (default: False) | |
:return: the error ratio (lower is better) | |
""" | |
err = 0 | |
previous_type = self.vocab.token_type(tokens[0]) | |
current_pitches = [] | |
def check(tok: int): | |
nonlocal err | |
nonlocal previous_type | |
nonlocal current_pitches | |
token_type, token_value = self.vocab.token_to_event[tok].split('_') | |
# Good token type | |
if token_type in self.tokens_types_graph[previous_type]: | |
if token_type == 'Pitch': | |
if int(token_value) in current_pitches: | |
err += 1 # pitch already played at current position | |
else: | |
current_pitches.append(int(token_value)) | |
elif token_type == 'Time-Shift': | |
if self._token_duration_to_ticks(token_value, 48) > 0: | |
current_pitches = [] # moving in time, list reset | |
# Bad token type | |
else: | |
err += 1 | |
previous_type = token_type | |
if consider_pad: | |
for token in tokens[1:]: | |
check(token) | |
else: | |
for token in tokens[1:]: | |
if previous_type == 'PAD': | |
break | |
check(token) | |
return err / len(tokens) | |
def detect_chords(self, list_notes: List[Note], time_division: int, beat_res: int = 4, onset_offset: int = 1, | |
only_known_chord: bool = False, simul_notes_limit: int = 20, verbose=False) -> List[Event]: | |
""" Chord detection method. | |
NOTE: make sure to sort notes by start time then pitch before: notes.sort(key=lambda x: (x.start, x.pitch)) | |
NOTE2: on very large tracks with high note density this method can be very slow ! | |
If you plan to use it with the Maestro or GiantMIDI datasets, it can take up to | |
hundreds of seconds per MIDI depending on your cpu. | |
One time step at a time, it will analyse the notes played together | |
and detect possible chords. | |
:param notes: notes to analyse (sorted by starting time, them pitch) | |
:param time_division: MIDI time division / resolution, in ticks/beat (of the MIDI being parsed) | |
:param beat_res: beat resolution, i.e. nb of samples per beat (default 4) | |
:param onset_offset: maximum offset (in samples) ∈ N separating notes starts to consider them | |
starting at the same time / onset (default is 1) | |
:param only_known_chord: will select only known chords. If set to False, non recognized chords of | |
n notes will give a chord_n event (default False) | |
:param simul_notes_limit: nb of simultaneous notes being processed when looking for a chord | |
this parameter allows to speed up the chord detection (default 20) | |
:return: the detected chords as Event objects | |
""" | |
assert simul_notes_limit >= 5, 'simul_notes_limit must be higher than 5, chords can be made up to 5 notes' | |
tuples = [] | |
for note in list_notes: | |
tuples.append((note.pitch, int(note.start), int(note.end), int(note.velocity))) | |
notes = np.asarray(tuples) | |
time_div_half = time_division // 2 | |
onset_offset = time_division * onset_offset / beat_res | |
count = 0 | |
previous_tick = -1 | |
detected_chords = [] | |
note_belong_to_chord_id = dict() | |
while count < len(notes): | |
# Checks we moved in time after last step, otherwise discard this tick | |
if notes[count, 1] == previous_tick: | |
count += 1 | |
continue | |
# Gathers the notes around the same time step | |
# Reduce the scope of the search | |
notes_to_consider = notes[count:count + simul_notes_limit].copy() | |
old_true_notes_indexes = np.arange(count, count + simul_notes_limit) # keep track of true note indexes | |
# Take notes withing onset_offset samples of the first note | |
indexes_valid = np.where(notes_to_consider[:, 1] <= notes_to_consider[0, 1] + onset_offset) | |
true_notes_indexes = old_true_notes_indexes[indexes_valid] | |
onset_notes = notes_to_consider[indexes_valid] | |
# Take notes that end close to the first note's end | |
indexes_valid = np.where(np.abs(onset_notes[:, 2] - onset_notes[0, 2]) < time_div_half) | |
true_notes_indexes = true_notes_indexes[indexes_valid] | |
onset_notes = onset_notes[indexes_valid] | |
# if there are at least 3 notes, try to find the chord | |
if len(onset_notes) >= 3: | |
found, chord_name, root_id, chord_notes_indexes = filter_notes_find_chord_and_root(onset_notes[:, 0], self.inverted_chord_map) | |
# if found: | |
# found, chord_name, root_id, chord_notes_indexes = filter_notes_find_chord_and_root(notes_to_consider[:, 0], self.inverted_chord_map) | |
if found: | |
detected_chord_id = len(detected_chords) | |
# get the indexes of the notes in the chord wrt the onset_notes array | |
relative_indexes_chord_notes_in_onset_notes = np.array(chord_notes_indexes) | |
# get true indexes of the notes in the chord (indexes of the note stream) | |
true_indexes = true_notes_indexes[relative_indexes_chord_notes_in_onset_notes] | |
# for each note, track the chords it belongs to in note_belong_to_chord_id | |
for i in true_indexes: | |
if i not in note_belong_to_chord_id.keys(): | |
note_belong_to_chord_id[i] = [detected_chord_id] | |
else: | |
note_belong_to_chord_id[i].append(detected_chord_id) | |
# save the info of the detected chord | |
root_position_in_sorted_onset = chord_notes_indexes[root_id] | |
root_pitch = onset_notes[root_position_in_sorted_onset, 0] | |
onset = np.min([notes[i, 1] for i in true_indexes]) | |
offset = int(np.mean([notes[i, 2] for i in true_indexes])) | |
velocity = self.velocities[int(np.argmin(np.abs(self.velocities - int(np.mean([notes[i, 3] for i in true_indexes])))))] # quantize velocity | |
detected_chords.append((chord_name, true_indexes, root_pitch, onset, offset, velocity)) | |
if verbose: print(f'New chord detected: {chord_name}, root {root_pitch} with notes: {true_indexes}, onset: {onset}, offset: {offset}, velocity: {velocity}') | |
count += 1 | |
# now we need to delete some the redundant detected chords to have just one chord per note | |
indexes_chords_to_remove = [] | |
for note, chord_ids in note_belong_to_chord_id.copy().items(): | |
# remove chords that were already filtered | |
chord_ids = sorted(set(chord_ids) - set(indexes_chords_to_remove)) | |
if len(chord_ids) == 0: # if not remaining chords, then the note should be removed | |
del note_belong_to_chord_id[note] | |
else: | |
note_belong_to_chord_id[note] = chord_ids # update the chord_ids | |
if len(chord_ids) > 1: # if several, we need to filter by the number of notes in the chords | |
chords = [detected_chords[i] for i in chord_ids] | |
selected_chord = np.argmax([len(c[1]) for c in chords]) | |
note_belong_to_chord_id[note] = [chord_ids[selected_chord]] | |
for i_c, c in enumerate(chord_ids): | |
if i_c != selected_chord: | |
indexes_chords_to_remove.append(c) | |
for note, chord_ids in note_belong_to_chord_id.copy().items(): | |
chord_ids = sorted(set(chord_ids) - set(indexes_chords_to_remove)) | |
if len(chord_ids) == 0: # if not remaining chords, then the note should be removed | |
del note_belong_to_chord_id[note] | |
else: | |
note_belong_to_chord_id[note] = chord_ids # update the chord_ids | |
selected_chords = [detected_chords[i] for i in range(len(detected_chords)) if i not in indexes_chords_to_remove] | |
selected_chords_ids = [i for i in range(len(detected_chords)) if i not in indexes_chords_to_remove] | |
# check that all notes are used just once | |
all_chord_notes = [] | |
for c in selected_chords: | |
all_chord_notes += list(c[1]) | |
assert len(all_chord_notes) == len(set(all_chord_notes)) | |
# format new stream of notes, removing chord notes from them, and inserting "chord" to be able to track timeshifts | |
new_list_notes = [] | |
note_dict_keys = list(note_belong_to_chord_id.keys()) | |
inserted_chords = [] | |
count_added = 0 | |
for i in range(len(list_notes)): | |
if i not in note_dict_keys: | |
new_list_notes.append(list_notes[i]) | |
else: | |
assert len(note_belong_to_chord_id[i]) == 1 | |
chord_id = note_belong_to_chord_id[i][0] | |
if chord_id not in inserted_chords: | |
inserted_chords.append(chord_id) | |
count_added += 1 | |
chord_id, _, root_pitch, onset, offset, velocity = detected_chords[chord_id] | |
new_list_notes.append(Note(velocity=velocity, start=onset, end=offset, pitch=chord_id + '_' + str(root_pitch))) | |
# check the new count of notes (all previous notes - the number of notes in the chords + the number of chords) | |
assert len(new_list_notes) == (len(list_notes) - len(all_chord_notes) + len(selected_chords)) | |
return new_list_notes | |
if __name__ == '__main__': | |
from miditoolkit import MidiFile | |
pitch_range = range(21, 109) | |
beat_res = {(0, 4): 8, (4, 12): 4} | |
nb_velocities = 32 | |
tokenizer_structured = ChordStructured(pitch_range, beat_res, nb_velocities) | |
# tokenizer_structured = Structured(pitch_range, beat_res, nb_velocities) | |
path = '/home/cedric/Documents/pianocktail/data/music/processed/vkgoeswild_processed/ac_dc_hells_bells_vkgoeswild_piano_cover_processed.mid' | |
midi = MidiFile(path) | |
tokens = tokenizer_structured.midi_to_tokens(midi) | |
midi = tokenizer_structured.tokens_to_midi(tokens) | |
midi.dump("/home/cedric/Desktop/tes/transcribed.mid") |