| 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 |
|
|