Spaces:
Build error
Build error
import miditoolkit | |
import numpy as np | |
class MIDIChord(object): | |
def __init__(self): | |
# define pitch classes | |
self.PITCH_CLASSES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] | |
# define chord maps (required) | |
self.CHORD_MAPS = {'maj': [0, 4], | |
'min': [0, 3], | |
'dim': [0, 3, 6], | |
'aug': [0, 4, 8], | |
'dom': [0, 4, 7, 10]} | |
# define chord insiders (+1) | |
self.CHORD_INSIDERS = {'maj': [7], | |
'min': [7], | |
'dim': [9], | |
'aug': [], | |
'dom': []} | |
# define chord outsiders (-1) | |
self.CHORD_OUTSIDERS_1 = {'maj': [2, 5, 9], | |
'min': [2, 5, 8], | |
'dim': [2, 5, 10], | |
'aug': [2, 5, 9], | |
'dom': [2, 5, 9]} | |
# define chord outsiders (-2) | |
self.CHORD_OUTSIDERS_2 = {'maj': [1, 3, 6, 8, 10], | |
'min': [1, 4, 6, 9, 11], | |
'dim': [1, 4, 7, 8, 11], | |
'aug': [1, 3, 6, 7, 10], | |
'dom': [1, 3, 6, 8, 11]} | |
def note2pianoroll(self, notes, max_tick, ticks_per_beat): | |
return miditoolkit.pianoroll.parser.notes2pianoroll( | |
note_stream_ori=notes, | |
max_tick=max_tick, | |
ticks_per_beat=ticks_per_beat) | |
def sequencing(self, chroma): | |
candidates = {} | |
for index in range(len(chroma)): | |
if chroma[index]: | |
root_note = index | |
_chroma = np.roll(chroma, -root_note) | |
sequence = np.where(_chroma == 1)[0] | |
candidates[root_note] = list(sequence) | |
return candidates | |
def scoring(self, candidates): | |
scores = {} | |
qualities = {} | |
for root_note, sequence in candidates.items(): | |
if 3 not in sequence and 4 not in sequence: | |
scores[root_note] = -100 | |
qualities[root_note] = 'None' | |
elif 3 in sequence and 4 in sequence: | |
scores[root_note] = -100 | |
qualities[root_note] = 'None' | |
else: | |
# decide quality | |
if 3 in sequence: | |
if 6 in sequence: | |
quality = 'dim' | |
else: | |
quality = 'min' | |
elif 4 in sequence: | |
if 8 in sequence: | |
quality = 'aug' | |
else: | |
if 7 in sequence and 10 in sequence: | |
quality = 'dom' | |
else: | |
quality = 'maj' | |
# decide score | |
maps = self.CHORD_MAPS.get(quality) | |
_notes = [n for n in sequence if n not in maps] | |
score = 0 | |
for n in _notes: | |
if n in self.CHORD_OUTSIDERS_1.get(quality): | |
score -= 1 | |
elif n in self.CHORD_OUTSIDERS_2.get(quality): | |
score -= 2 | |
elif n in self.CHORD_INSIDERS.get(quality): | |
score += 1 | |
scores[root_note] = score | |
qualities[root_note] = quality | |
return scores, qualities | |
def find_chord(self, pianoroll): | |
chroma = miditoolkit.pianoroll.utils.tochroma(pianoroll=pianoroll) | |
chroma = np.sum(chroma, axis=0) | |
chroma = np.array([1 if c else 0 for c in chroma]) | |
if np.sum(chroma) == 0: | |
return 'N', 'N', 'N', 0 | |
else: | |
candidates = self.sequencing(chroma=chroma) | |
scores, qualities = self.scoring(candidates=candidates) | |
# bass note | |
sorted_notes = [] | |
for i, v in enumerate(np.sum(pianoroll, axis=0)): | |
if v > 0: | |
sorted_notes.append(int(i%12)) | |
bass_note = sorted_notes[0] | |
# root note | |
__root_note = [] | |
_max = max(scores.values()) | |
for _root_note, score in scores.items(): | |
if score == _max: | |
__root_note.append(_root_note) | |
if len(__root_note) == 1: | |
root_note = __root_note[0] | |
else: | |
#TODO: what should i do | |
for n in sorted_notes: | |
if n in __root_note: | |
root_note = n | |
break | |
# quality | |
quality = qualities.get(root_note) | |
sequence = candidates.get(root_note) | |
# score | |
score = scores.get(root_note) | |
return self.PITCH_CLASSES[root_note], quality, self.PITCH_CLASSES[bass_note], score | |
def greedy(self, candidates, max_tick, min_length): | |
chords = [] | |
# start from 0 | |
start_tick = 0 | |
while start_tick < max_tick: | |
_candidates = candidates.get(start_tick) | |
_candidates = sorted(_candidates.items(), key=lambda x: (x[1][-1], x[0])) | |
# choose | |
end_tick, (root_note, quality, bass_note, _) = _candidates[-1] | |
if root_note == bass_note: | |
chord = '{}:{}'.format(root_note, quality) | |
else: | |
chord = '{}:{}/{}'.format(root_note, quality, bass_note) | |
chords.append([start_tick, end_tick, chord]) | |
start_tick = end_tick | |
# remove :None | |
temp = chords | |
while ':None' in temp[0][-1]: | |
try: | |
temp[1][0] = temp[0][0] | |
del temp[0] | |
except: | |
print('NO CHORD') | |
return [] | |
temp2 = [] | |
for chord in temp: | |
if ':None' not in chord[-1]: | |
temp2.append(chord) | |
else: | |
temp2[-1][1] = chord[1] | |
return temp2 | |
def extract(self, notes): | |
# read | |
max_tick = max([n.end for n in notes]) | |
ticks_per_beat = 480 | |
pianoroll = self.note2pianoroll( | |
notes=notes, | |
max_tick=max_tick, | |
ticks_per_beat=ticks_per_beat) | |
# get lots of candidates | |
candidates = {} | |
# the shortest: 2 beat, longest: 4 beat | |
for interval in [4, 2]: | |
for start_tick in range(0, max_tick, ticks_per_beat): | |
# set target pianoroll | |
end_tick = int(ticks_per_beat * interval + start_tick) | |
if end_tick > max_tick: | |
end_tick = max_tick | |
_pianoroll = pianoroll[start_tick:end_tick, :] | |
# find chord | |
root_note, quality, bass_note, score = self.find_chord(pianoroll=_pianoroll) | |
# save | |
if start_tick not in candidates: | |
candidates[start_tick] = {} | |
candidates[start_tick][end_tick] = (root_note, quality, bass_note, score) | |
else: | |
if end_tick not in candidates[start_tick]: | |
candidates[start_tick][end_tick] = (root_note, quality, bass_note, score) | |
# greedy | |
chords = self.greedy(candidates=candidates, | |
max_tick=max_tick, | |
min_length=ticks_per_beat) | |
return chords | |