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