Upload chord_recognition.py
Browse files- chord_recognition.py +188 -0
chord_recognition.py
ADDED
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import miditoolkit
|
2 |
+
import numpy as np
|
3 |
+
|
4 |
+
class MIDIChord(object):
|
5 |
+
def __init__(self):
|
6 |
+
# define pitch classes
|
7 |
+
self.PITCH_CLASSES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
|
8 |
+
# define chord maps (required)
|
9 |
+
self.CHORD_MAPS = {'maj': [0, 4],
|
10 |
+
'min': [0, 3],
|
11 |
+
'dim': [0, 3, 6],
|
12 |
+
'aug': [0, 4, 8],
|
13 |
+
'dom': [0, 4, 7, 10]}
|
14 |
+
# define chord insiders (+1)
|
15 |
+
self.CHORD_INSIDERS = {'maj': [7],
|
16 |
+
'min': [7],
|
17 |
+
'dim': [9],
|
18 |
+
'aug': [],
|
19 |
+
'dom': []}
|
20 |
+
# define chord outsiders (-1)
|
21 |
+
self.CHORD_OUTSIDERS_1 = {'maj': [2, 5, 9],
|
22 |
+
'min': [2, 5, 8],
|
23 |
+
'dim': [2, 5, 10],
|
24 |
+
'aug': [2, 5, 9],
|
25 |
+
'dom': [2, 5, 9]}
|
26 |
+
# define chord outsiders (-2)
|
27 |
+
self.CHORD_OUTSIDERS_2 = {'maj': [1, 3, 6, 8, 10],
|
28 |
+
'min': [1, 4, 6, 9, 11],
|
29 |
+
'dim': [1, 4, 7, 8, 11],
|
30 |
+
'aug': [1, 3, 6, 7, 10],
|
31 |
+
'dom': [1, 3, 6, 8, 11]}
|
32 |
+
|
33 |
+
def note2pianoroll(self, notes, max_tick, ticks_per_beat):
|
34 |
+
return miditoolkit.pianoroll.parser.notes2pianoroll(
|
35 |
+
note_stream_ori=notes,
|
36 |
+
max_tick=max_tick,
|
37 |
+
ticks_per_beat=ticks_per_beat)
|
38 |
+
|
39 |
+
def sequencing(self, chroma):
|
40 |
+
candidates = {}
|
41 |
+
for index in range(len(chroma)):
|
42 |
+
if chroma[index]:
|
43 |
+
root_note = index
|
44 |
+
_chroma = np.roll(chroma, -root_note)
|
45 |
+
sequence = np.where(_chroma == 1)[0]
|
46 |
+
candidates[root_note] = list(sequence)
|
47 |
+
return candidates
|
48 |
+
|
49 |
+
def scoring(self, candidates):
|
50 |
+
scores = {}
|
51 |
+
qualities = {}
|
52 |
+
for root_note, sequence in candidates.items():
|
53 |
+
if 3 not in sequence and 4 not in sequence:
|
54 |
+
scores[root_note] = -100
|
55 |
+
qualities[root_note] = 'None'
|
56 |
+
elif 3 in sequence and 4 in sequence:
|
57 |
+
scores[root_note] = -100
|
58 |
+
qualities[root_note] = 'None'
|
59 |
+
else:
|
60 |
+
# decide quality
|
61 |
+
if 3 in sequence:
|
62 |
+
if 6 in sequence:
|
63 |
+
quality = 'dim'
|
64 |
+
else:
|
65 |
+
quality = 'min'
|
66 |
+
elif 4 in sequence:
|
67 |
+
if 8 in sequence:
|
68 |
+
quality = 'aug'
|
69 |
+
else:
|
70 |
+
if 7 in sequence and 10 in sequence:
|
71 |
+
quality = 'dom'
|
72 |
+
else:
|
73 |
+
quality = 'maj'
|
74 |
+
# decide score
|
75 |
+
maps = self.CHORD_MAPS.get(quality)
|
76 |
+
_notes = [n for n in sequence if n not in maps]
|
77 |
+
score = 0
|
78 |
+
for n in _notes:
|
79 |
+
if n in self.CHORD_OUTSIDERS_1.get(quality):
|
80 |
+
score -= 1
|
81 |
+
elif n in self.CHORD_OUTSIDERS_2.get(quality):
|
82 |
+
score -= 2
|
83 |
+
elif n in self.CHORD_INSIDERS.get(quality):
|
84 |
+
score += 1
|
85 |
+
scores[root_note] = score
|
86 |
+
qualities[root_note] = quality
|
87 |
+
return scores, qualities
|
88 |
+
|
89 |
+
def find_chord(self, pianoroll):
|
90 |
+
chroma = miditoolkit.pianoroll.utils.tochroma(pianoroll=pianoroll)
|
91 |
+
chroma = np.sum(chroma, axis=0)
|
92 |
+
chroma = np.array([1 if c else 0 for c in chroma])
|
93 |
+
if np.sum(chroma) == 0:
|
94 |
+
return 'N', 'N', 'N', 0
|
95 |
+
else:
|
96 |
+
candidates = self.sequencing(chroma=chroma)
|
97 |
+
scores, qualities = self.scoring(candidates=candidates)
|
98 |
+
# bass note
|
99 |
+
sorted_notes = []
|
100 |
+
for i, v in enumerate(np.sum(pianoroll, axis=0)):
|
101 |
+
if v > 0:
|
102 |
+
sorted_notes.append(int(i%12))
|
103 |
+
bass_note = sorted_notes[0]
|
104 |
+
# root note
|
105 |
+
__root_note = []
|
106 |
+
_max = max(scores.values())
|
107 |
+
for _root_note, score in scores.items():
|
108 |
+
if score == _max:
|
109 |
+
__root_note.append(_root_note)
|
110 |
+
if len(__root_note) == 1:
|
111 |
+
root_note = __root_note[0]
|
112 |
+
else:
|
113 |
+
#TODO: what should i do
|
114 |
+
for n in sorted_notes:
|
115 |
+
if n in __root_note:
|
116 |
+
root_note = n
|
117 |
+
break
|
118 |
+
# quality
|
119 |
+
quality = qualities.get(root_note)
|
120 |
+
sequence = candidates.get(root_note)
|
121 |
+
# score
|
122 |
+
score = scores.get(root_note)
|
123 |
+
return self.PITCH_CLASSES[root_note], quality, self.PITCH_CLASSES[bass_note], score
|
124 |
+
|
125 |
+
def greedy(self, candidates, max_tick, min_length):
|
126 |
+
chords = []
|
127 |
+
# start from 0
|
128 |
+
start_tick = 0
|
129 |
+
while start_tick < max_tick:
|
130 |
+
_candidates = candidates.get(start_tick)
|
131 |
+
_candidates = sorted(_candidates.items(), key=lambda x: (x[1][-1], x[0]))
|
132 |
+
# choose
|
133 |
+
end_tick, (root_note, quality, bass_note, _) = _candidates[-1]
|
134 |
+
if root_note == bass_note:
|
135 |
+
chord = '{}:{}'.format(root_note, quality)
|
136 |
+
else:
|
137 |
+
chord = '{}:{}/{}'.format(root_note, quality, bass_note)
|
138 |
+
chords.append([start_tick, end_tick, chord])
|
139 |
+
start_tick = end_tick
|
140 |
+
# remove :None
|
141 |
+
temp = chords
|
142 |
+
while ':None' in temp[0][-1]:
|
143 |
+
try:
|
144 |
+
temp[1][0] = temp[0][0]
|
145 |
+
del temp[0]
|
146 |
+
except:
|
147 |
+
print('NO CHORD')
|
148 |
+
return []
|
149 |
+
temp2 = []
|
150 |
+
for chord in temp:
|
151 |
+
if ':None' not in chord[-1]:
|
152 |
+
temp2.append(chord)
|
153 |
+
else:
|
154 |
+
temp2[-1][1] = chord[1]
|
155 |
+
return temp2
|
156 |
+
|
157 |
+
def extract(self, notes):
|
158 |
+
# read
|
159 |
+
max_tick = max([n.end for n in notes])
|
160 |
+
ticks_per_beat = 480
|
161 |
+
pianoroll = self.note2pianoroll(
|
162 |
+
notes=notes,
|
163 |
+
max_tick=max_tick,
|
164 |
+
ticks_per_beat=ticks_per_beat)
|
165 |
+
# get lots of candidates
|
166 |
+
candidates = {}
|
167 |
+
# the shortest: 2 beat, longest: 4 beat
|
168 |
+
for interval in [4, 2]:
|
169 |
+
for start_tick in range(0, max_tick, ticks_per_beat):
|
170 |
+
# set target pianoroll
|
171 |
+
end_tick = int(ticks_per_beat * interval + start_tick)
|
172 |
+
if end_tick > max_tick:
|
173 |
+
end_tick = max_tick
|
174 |
+
_pianoroll = pianoroll[start_tick:end_tick, :]
|
175 |
+
# find chord
|
176 |
+
root_note, quality, bass_note, score = self.find_chord(pianoroll=_pianoroll)
|
177 |
+
# save
|
178 |
+
if start_tick not in candidates:
|
179 |
+
candidates[start_tick] = {}
|
180 |
+
candidates[start_tick][end_tick] = (root_note, quality, bass_note, score)
|
181 |
+
else:
|
182 |
+
if end_tick not in candidates[start_tick]:
|
183 |
+
candidates[start_tick][end_tick] = (root_note, quality, bass_note, score)
|
184 |
+
# greedy
|
185 |
+
chords = self.greedy(candidates=candidates,
|
186 |
+
max_tick=max_tick,
|
187 |
+
min_length=ticks_per_beat)
|
188 |
+
return chords
|