|
import miditoolkit |
|
import numpy as np |
|
|
|
class MIDIChord(object): |
|
def __init__(self): |
|
|
|
self.PITCH_CLASSES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] |
|
|
|
self.CHORD_MAPS = {'maj': [0, 4], |
|
'min': [0, 3], |
|
'dim': [0, 3, 6], |
|
'aug': [0, 4, 8], |
|
'dom': [0, 4, 7, 10]} |
|
|
|
self.CHORD_INSIDERS = {'maj': [7], |
|
'min': [7], |
|
'dim': [9], |
|
'aug': [], |
|
'dom': []} |
|
|
|
self.CHORD_OUTSIDERS_1 = {'maj': [2, 5, 9], |
|
'min': [2, 5, 8], |
|
'dim': [2, 5, 10], |
|
'aug': [2, 5, 9], |
|
'dom': [2, 5, 9]} |
|
|
|
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: |
|
|
|
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' |
|
|
|
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) |
|
|
|
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 = [] |
|
_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: |
|
|
|
for n in sorted_notes: |
|
if n in __root_note: |
|
root_note = n |
|
break |
|
|
|
quality = qualities.get(root_note) |
|
sequence = candidates.get(root_note) |
|
|
|
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_tick = 0 |
|
while start_tick < max_tick: |
|
_candidates = candidates.get(start_tick) |
|
_candidates = sorted(_candidates.items(), key=lambda x: (x[1][-1], x[0])) |
|
|
|
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 |
|
|
|
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): |
|
|
|
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) |
|
|
|
candidates = {} |
|
|
|
for interval in [4, 2]: |
|
for start_tick in range(0, max_tick, ticks_per_beat): |
|
|
|
end_tick = int(ticks_per_beat * interval + start_tick) |
|
if end_tick > max_tick: |
|
end_tick = max_tick |
|
_pianoroll = pianoroll[start_tick:end_tick, :] |
|
|
|
root_note, quality, bass_note, score = self.find_chord(pianoroll=_pianoroll) |
|
|
|
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) |
|
|
|
chords = self.greedy(candidates=candidates, |
|
max_tick=max_tick, |
|
min_length=ticks_per_beat) |
|
return chords |
|
|