Spaces:
Build error
Build error
import chord_recognition | |
import numpy as np | |
import miditoolkit | |
import copy | |
# parameters for input | |
DEFAULT_VELOCITY_BINS = np.linspace(0, 128, 32+1, dtype=np.int) | |
DEFAULT_FRACTION = 16 | |
DEFAULT_DURATION_BINS = np.arange(60, 3841, 60, dtype=int) | |
DEFAULT_TEMPO_INTERVALS = [range(30, 90), range(90, 150), range(150, 210)] | |
# parameters for output | |
DEFAULT_RESOLUTION = 480 | |
# define "Item" for general storage | |
class Item(object): | |
def __init__(self, name, start, end, velocity, pitch): | |
self.name = name | |
self.start = start | |
self.end = end | |
self.velocity = velocity | |
self.pitch = pitch | |
def __repr__(self): | |
return 'Item(name={}, start={}, end={}, velocity={}, pitch={})'.format( | |
self.name, self.start, self.end, self.velocity, self.pitch) | |
# read notes and tempo changes from midi (assume there is only one track) | |
def read_items(file_path): | |
midi_obj = miditoolkit.midi.parser.MidiFile(file_path) | |
# note | |
note_items = [] | |
notes = midi_obj.instruments[0].notes | |
notes.sort(key=lambda x: (x.start, x.pitch)) | |
for note in notes: | |
note_items.append(Item( | |
name='Note', | |
start=note.start, | |
end=note.end, | |
velocity=note.velocity, | |
pitch=note.pitch)) | |
note_items.sort(key=lambda x: x.start) | |
# tempo | |
tempo_items = [] | |
for tempo in midi_obj.tempo_changes: | |
tempo_items.append(Item( | |
name='Tempo', | |
start=tempo.time, | |
end=None, | |
velocity=None, | |
pitch=int(tempo.tempo))) | |
tempo_items.sort(key=lambda x: x.start) | |
# expand to all beat | |
max_tick = tempo_items[-1].start | |
existing_ticks = {item.start: item.pitch for item in tempo_items} | |
wanted_ticks = np.arange(0, max_tick+1, DEFAULT_RESOLUTION) | |
output = [] | |
for tick in wanted_ticks: | |
if tick in existing_ticks: | |
output.append(Item( | |
name='Tempo', | |
start=tick, | |
end=None, | |
velocity=None, | |
pitch=existing_ticks[tick])) | |
else: | |
output.append(Item( | |
name='Tempo', | |
start=tick, | |
end=None, | |
velocity=None, | |
pitch=output[-1].pitch)) | |
tempo_items = output | |
return note_items, tempo_items | |
# quantize items | |
def quantize_items(items, ticks=120): | |
# grid | |
grids = np.arange(0, items[-1].start, ticks, dtype=int) | |
# process | |
for item in items: | |
index = np.argmin(abs(grids - item.start)) | |
shift = grids[index] - item.start | |
item.start += shift | |
item.end += shift | |
return items | |
# extract chord | |
def extract_chords(items): | |
method = chord_recognition.MIDIChord() | |
chords = method.extract(notes=items) | |
output = [] | |
for chord in chords: | |
output.append(Item( | |
name='Chord', | |
start=chord[0], | |
end=chord[1], | |
velocity=None, | |
pitch=chord[2].split('/')[0])) | |
return output | |
# group items | |
def group_items(items, max_time, ticks_per_bar=DEFAULT_RESOLUTION*4): | |
items.sort(key=lambda x: x.start) | |
downbeats = np.arange(0, max_time+ticks_per_bar, ticks_per_bar) | |
groups = [] | |
for db1, db2 in zip(downbeats[:-1], downbeats[1:]): | |
insiders = [] | |
for item in items: | |
if (item.start >= db1) and (item.start < db2): | |
insiders.append(item) | |
overall = [db1] + insiders + [db2] | |
groups.append(overall) | |
return groups | |
# define "Event" for event storage | |
class Event(object): | |
def __init__(self, name, time, value, text): | |
self.name = name | |
self.time = time | |
self.value = value | |
self.text = text | |
def __repr__(self): | |
return 'Event(name={}, time={}, value={}, text={})'.format( | |
self.name, self.time, self.value, self.text) | |
# item to event | |
def item2event(groups): | |
events = [] | |
n_downbeat = 0 | |
for i in range(len(groups)): | |
if 'Note' not in [item.name for item in groups[i][1:-1]]: | |
continue | |
bar_st, bar_et = groups[i][0], groups[i][-1] | |
n_downbeat += 1 | |
events.append(Event( | |
name='Bar', | |
time=None, | |
value=None, | |
text='{}'.format(n_downbeat))) | |
for item in groups[i][1:-1]: | |
# position | |
flags = np.linspace(bar_st, bar_et, DEFAULT_FRACTION, endpoint=False) | |
index = np.argmin(abs(flags-item.start)) | |
events.append(Event( | |
name='Position', | |
time=item.start, | |
value='{}/{}'.format(index+1, DEFAULT_FRACTION), | |
text='{}'.format(item.start))) | |
if item.name == 'Note': | |
# velocity | |
velocity_index = np.searchsorted( | |
DEFAULT_VELOCITY_BINS, | |
item.velocity, | |
side='right') - 1 | |
events.append(Event( | |
name='Note Velocity', | |
time=item.start, | |
value=velocity_index, | |
text='{}/{}'.format(item.velocity, DEFAULT_VELOCITY_BINS[velocity_index]))) | |
# pitch | |
events.append(Event( | |
name='Note On', | |
time=item.start, | |
value=item.pitch, | |
text='{}'.format(item.pitch))) | |
# duration | |
duration = item.end - item.start | |
index = np.argmin(abs(DEFAULT_DURATION_BINS-duration)) | |
events.append(Event( | |
name='Note Duration', | |
time=item.start, | |
value=index, | |
text='{}/{}'.format(duration, DEFAULT_DURATION_BINS[index]))) | |
elif item.name == 'Chord': | |
events.append(Event( | |
name='Chord', | |
time=item.start, | |
value=item.pitch, | |
text='{}'.format(item.pitch))) | |
elif item.name == 'Tempo': | |
tempo = item.pitch | |
if tempo in DEFAULT_TEMPO_INTERVALS[0]: | |
tempo_style = Event('Tempo Class', item.start, 'slow', None) | |
tempo_value = Event('Tempo Value', item.start, | |
tempo-DEFAULT_TEMPO_INTERVALS[0].start, None) | |
elif tempo in DEFAULT_TEMPO_INTERVALS[1]: | |
tempo_style = Event('Tempo Class', item.start, 'mid', None) | |
tempo_value = Event('Tempo Value', item.start, | |
tempo-DEFAULT_TEMPO_INTERVALS[1].start, None) | |
elif tempo in DEFAULT_TEMPO_INTERVALS[2]: | |
tempo_style = Event('Tempo Class', item.start, 'fast', None) | |
tempo_value = Event('Tempo Value', item.start, | |
tempo-DEFAULT_TEMPO_INTERVALS[2].start, None) | |
elif tempo < DEFAULT_TEMPO_INTERVALS[0].start: | |
tempo_style = Event('Tempo Class', item.start, 'slow', None) | |
tempo_value = Event('Tempo Value', item.start, 0, None) | |
elif tempo > DEFAULT_TEMPO_INTERVALS[2].stop: | |
tempo_style = Event('Tempo Class', item.start, 'fast', None) | |
tempo_value = Event('Tempo Value', item.start, 59, None) | |
events.append(tempo_style) | |
events.append(tempo_value) | |
return events | |
############################################################################################# | |
# WRITE MIDI | |
############################################################################################# | |
def word_to_event(words, word2event): | |
events = [] | |
for word in words: | |
event_name, event_value = word2event.get(word).split('_') | |
events.append(Event(event_name, None, event_value, None)) | |
return events | |
def write_midi(words, word2event, output_path, prompt_path=None): | |
events = word_to_event(words, word2event) | |
# get downbeat and note (no time) | |
temp_notes = [] | |
temp_chords = [] | |
temp_tempos = [] | |
for i in range(len(events)-3): | |
if events[i].name == 'Bar' and i > 0: | |
temp_notes.append('Bar') | |
temp_chords.append('Bar') | |
temp_tempos.append('Bar') | |
elif events[i].name == 'Position' and \ | |
events[i+1].name == 'Note Velocity' and \ | |
events[i+2].name == 'Note On' and \ | |
events[i+3].name == 'Note Duration': | |
# start time and end time from position | |
position = int(events[i].value.split('/')[0]) - 1 | |
# velocity | |
index = int(events[i+1].value) | |
velocity = int(DEFAULT_VELOCITY_BINS[index]) | |
# pitch | |
pitch = int(events[i+2].value) | |
# duration | |
index = int(events[i+3].value) | |
duration = DEFAULT_DURATION_BINS[index] | |
# adding | |
temp_notes.append([position, velocity, pitch, duration]) | |
elif events[i].name == 'Position' and events[i+1].name == 'Chord': | |
position = int(events[i].value.split('/')[0]) - 1 | |
temp_chords.append([position, events[i+1].value]) | |
elif events[i].name == 'Position' and \ | |
events[i+1].name == 'Tempo Class' and \ | |
events[i+2].name == 'Tempo Value': | |
position = int(events[i].value.split('/')[0]) - 1 | |
if events[i+1].value == 'slow': | |
tempo = DEFAULT_TEMPO_INTERVALS[0].start + int(events[i+2].value) | |
elif events[i+1].value == 'mid': | |
tempo = DEFAULT_TEMPO_INTERVALS[1].start + int(events[i+2].value) | |
elif events[i+1].value == 'fast': | |
tempo = DEFAULT_TEMPO_INTERVALS[2].start + int(events[i+2].value) | |
temp_tempos.append([position, tempo]) | |
# get specific time for notes | |
ticks_per_beat = DEFAULT_RESOLUTION | |
ticks_per_bar = DEFAULT_RESOLUTION * 4 # assume 4/4 | |
notes = [] | |
current_bar = 0 | |
for note in temp_notes: | |
if note == 'Bar': | |
current_bar += 1 | |
else: | |
position, velocity, pitch, duration = note | |
# position (start time) | |
current_bar_st = current_bar * ticks_per_bar | |
current_bar_et = (current_bar + 1) * ticks_per_bar | |
flags = np.linspace(current_bar_st, current_bar_et, DEFAULT_FRACTION, endpoint=False, dtype=int) | |
st = flags[position] | |
# duration (end time) | |
et = st + duration | |
notes.append(miditoolkit.Note(velocity, pitch, st, et)) | |
# get specific time for chords | |
if len(temp_chords) > 0: | |
chords = [] | |
current_bar = 0 | |
for chord in temp_chords: | |
if chord == 'Bar': | |
current_bar += 1 | |
else: | |
position, value = chord | |
# position (start time) | |
current_bar_st = current_bar * ticks_per_bar | |
current_bar_et = (current_bar + 1) * ticks_per_bar | |
flags = np.linspace(current_bar_st, current_bar_et, DEFAULT_FRACTION, endpoint=False, dtype=int) | |
st = flags[position] | |
chords.append([st, value]) | |
# get specific time for tempos | |
tempos = [] | |
current_bar = 0 | |
for tempo in temp_tempos: | |
if tempo == 'Bar': | |
current_bar += 1 | |
else: | |
position, value = tempo | |
# position (start time) | |
current_bar_st = current_bar * ticks_per_bar | |
current_bar_et = (current_bar + 1) * ticks_per_bar | |
flags = np.linspace(current_bar_st, current_bar_et, DEFAULT_FRACTION, endpoint=False, dtype=int) | |
st = flags[position] | |
tempos.append([int(st), value]) | |
# write | |
if prompt_path: | |
midi = miditoolkit.midi.parser.MidiFile(prompt_path) | |
# | |
last_time = DEFAULT_RESOLUTION * 4 * 4 | |
# note shift | |
for note in notes: | |
note.start += last_time | |
note.end += last_time | |
midi.instruments[0].notes.extend(notes) | |
# tempo changes | |
temp_tempos = [] | |
for tempo in midi.tempo_changes: | |
if tempo.time < DEFAULT_RESOLUTION*4*4: | |
temp_tempos.append(tempo) | |
else: | |
break | |
for st, bpm in tempos: | |
st += last_time | |
temp_tempos.append(miditoolkit.midi.containers.TempoChange(bpm, st)) | |
midi.tempo_changes = temp_tempos | |
# write chord into marker | |
if len(temp_chords) > 0: | |
for c in chords: | |
midi.markers.append( | |
miditoolkit.midi.containers.Marker(text=c[1], time=c[0]+last_time)) | |
else: | |
midi = miditoolkit.midi.parser.MidiFile() | |
midi.ticks_per_beat = DEFAULT_RESOLUTION | |
# write instrument | |
inst = miditoolkit.midi.containers.Instrument(0, is_drum=False) | |
inst.notes = notes | |
midi.instruments.append(inst) | |
# write tempo | |
tempo_changes = [] | |
for st, bpm in tempos: | |
tempo_changes.append(miditoolkit.midi.containers.TempoChange(bpm, st)) | |
midi.tempo_changes = tempo_changes | |
# write chord into marker | |
if len(temp_chords) > 0: | |
for c in chords: | |
midi.markers.append( | |
miditoolkit.midi.containers.Marker(text=c[1], time=c[0])) | |
# write | |
midi.dump(output_path) | |