danurahul commited on
Commit
83c6932
1 Parent(s): 0b1ce65

Upload chord_recognition.py

Browse files
Files changed (1) hide show
  1. 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