Spaces:
Runtime error
Runtime error
| # ezchord - convert complex chord names to midi notes | |
| import sys | |
| import math | |
| import argparse | |
| from enum import Enum, auto | |
| from midiutil import MIDIFile | |
| class Mode(Enum): | |
| DIM = auto() | |
| MIN = auto() | |
| MAJ = auto() | |
| DOM = auto() | |
| AUG = auto() | |
| SUS2 = auto() | |
| SUS = auto() | |
| FIVE = auto() | |
| TEXT_TO_MODE = { | |
| "maj": Mode.MAJ, | |
| "dim": Mode.DIM, | |
| "o": Mode.DIM, | |
| "min": Mode.MIN, | |
| "m": Mode.MIN, | |
| "-": Mode.MIN, | |
| "aug": Mode.AUG, | |
| "+": Mode.AUG, | |
| "sus2": Mode.SUS2, | |
| "sus": Mode.SUS, | |
| "5": Mode.FIVE, | |
| "five": Mode.FIVE | |
| } | |
| MODE_TO_SHIFT = { | |
| Mode.MAJ: {3:0, 5:0}, | |
| Mode.DOM: {3:0, 5:0}, | |
| Mode.DIM: {3:-1, 5:-1}, | |
| Mode.MIN: {3:-1, 5:0}, | |
| Mode.AUG: {3:0, 5:1}, | |
| Mode.SUS2: {3:-2, 5:0}, | |
| Mode.SUS: {3:1, 5:0}, | |
| Mode.FIVE: {3:3, 5:0}, | |
| } | |
| NOTE_TO_PITCH = { | |
| "a": 9, | |
| "b": 11, | |
| "c": 12, | |
| "d": 14, | |
| "e": 16, | |
| "f": 17, | |
| "g": 19 | |
| } | |
| PITCH_TO_NOTE = {} | |
| for note, pitch in NOTE_TO_PITCH.items(): | |
| PITCH_TO_NOTE[pitch] = note | |
| RM_TO_PITCH = { | |
| "vii": 11, | |
| "iii": 4, | |
| "vi": 9, | |
| "iv": 5, | |
| "ii": 2, | |
| "i": 0, | |
| "v": 7 | |
| } | |
| ACC_TO_SHIFT = { | |
| "b": -1, | |
| "#": 1 | |
| } | |
| SCALE_DEGREE_SHIFT = { | |
| 1: 0, | |
| 2: 2, | |
| 3: 4, | |
| 4: 5, | |
| 5: 7, | |
| 6: 9, | |
| 7: 11 | |
| } | |
| def getNumber(string): | |
| numStr = "" | |
| for char in string: | |
| if char.isdigit(): | |
| numStr += char | |
| if len(numStr) > 0: | |
| return int(numStr) | |
| return | |
| def textToPitch(text, key = "c", voice = True): | |
| text = text.lower() | |
| isLetter = text[0] in NOTE_TO_PITCH.keys() | |
| if isLetter: | |
| pitch = NOTE_TO_PITCH[text[0]] | |
| else: | |
| for rm in RM_TO_PITCH.keys(): | |
| if rm in text: | |
| pitch = RM_TO_PITCH[rm] + textToPitch(key) | |
| isRomanNumeral = True | |
| break | |
| for i in range(1 if isLetter else 0, len(text)): | |
| if text[i] in ACC_TO_SHIFT.keys(): | |
| pitch += ACC_TO_SHIFT[text[i]] | |
| return pitch | |
| def pitchToText(pitch): | |
| octave = math.floor(pitch / 12) | |
| pitch = pitch % 12 | |
| pitch = pitch + (12 if pitch < 9 else 0) | |
| accidental = "" | |
| if not (pitch in PITCH_TO_NOTE.keys()): | |
| pitch = (pitch + 1) % 12 | |
| pitch = pitch + (12 if pitch < 9 else 0) | |
| accidental = "b" | |
| return PITCH_TO_NOTE[pitch].upper() + accidental + str(octave) | |
| def degreeToShift(deg): | |
| return SCALE_DEGREE_SHIFT[(deg - 1) % 7 + 1] + math.floor(deg / 8) * 12 | |
| def voice(chords): | |
| center = 0 | |
| voiced_chords = [] | |
| chord_ct = 0 | |
| pChord = None | |
| for i, currChord in enumerate(chords): | |
| if len(currChord) == 0: | |
| voiced_chords.append( [] ) | |
| continue | |
| else: | |
| if chord_ct == 0: | |
| voiced_chords.append( currChord ) | |
| chord_ct += 1 | |
| center = currChord[1] + 3 | |
| pChord = currChord | |
| continue | |
| prevChord = pChord | |
| voiced_chord = [] | |
| for i_, currNote in enumerate(currChord): | |
| # Skip bass note | |
| if i_ == 0: | |
| prevNote = prevChord[0] | |
| if abs(currNote - prevNote) > 7: | |
| if currNote < prevNote and abs(currNote + 12 - prevNote) < abs(currNote - prevNote): | |
| bestVoicing = currNote + 12 | |
| elif currNote > prevNote and abs(currNote - 12 - prevNote) < abs(currNote - prevNote): | |
| bestVoicing = currNote - 12 | |
| else: | |
| bestVoicing = currNote | |
| voiced_chord.append(bestVoicing) | |
| continue | |
| bestNeighbor = None | |
| allowance = -1 | |
| while bestNeighbor == None: | |
| allowance += 1 | |
| for i__, prevNote in enumerate(prevChord): | |
| if i__ == 0: | |
| continue | |
| if ( | |
| abs(currNote - prevNote) % 12 == allowance | |
| or abs(currNote - prevNote) % 12 == 12 - allowance | |
| ): | |
| bestNeighbor = prevNote | |
| break | |
| if currNote <= bestNeighbor: | |
| bestVoicing = currNote + math.floor((bestNeighbor - currNote + 6) / 12) * 12 | |
| else: | |
| bestVoicing = currNote + math.ceil((bestNeighbor - currNote - 6) / 12) * 12 | |
| bestVoicing = bestVoicing if (abs(bestVoicing - center) <= 8 or allowance > 2) else currNote | |
| voiced_chord.append(bestVoicing) | |
| voiced_chord.sort() | |
| voiced_chords.append(voiced_chord) | |
| pChord = voiced_chord | |
| return voiced_chords | |
| class Chord: | |
| def __init__(self, string): | |
| self.string = string | |
| self.degrees = {} | |
| string += " " | |
| self.split = [] | |
| sect = "" | |
| notes = list(NOTE_TO_PITCH.keys()) | |
| rms = list(RM_TO_PITCH.keys()) | |
| accs = list(ACC_TO_SHIFT.keys()) | |
| modes = list(TEXT_TO_MODE.keys()) | |
| rootAdded = False | |
| modeAdded = False | |
| isRomanNumeral = False | |
| isSlashChord = False | |
| isMaj7 = False | |
| for i in range(0, len(string) - 1): | |
| sect += string[i] | |
| currChar = string[i].lower() | |
| nextChar = string[i+1].lower() | |
| rootFound = not rootAdded and (currChar in notes+rms+accs and not nextChar in rms+accs) | |
| modeFound = False | |
| numFound = (currChar.isdigit() and not nextChar.isdigit()) | |
| if ( | |
| (i == len(string) - 2) | |
| or rootFound | |
| or numFound | |
| or nextChar == "/" | |
| or currChar == ")" | |
| ): | |
| if rootFound: | |
| self.root = sect | |
| rootAdded = True | |
| isRomanNumeral = self.root in rms | |
| elif sect[0] == "/": | |
| # case for 6/9 chords | |
| if sect[1] == "9": | |
| self.degrees[9] = 0 | |
| else: | |
| isSlashChord = True | |
| self.bassnote = sect[1:len(sect)] | |
| else: | |
| if not modeAdded: | |
| for mode in modes: | |
| modeFound = mode in sect[0:len(mode)] | |
| if modeFound: | |
| self.mode = TEXT_TO_MODE[mode] | |
| modeAdded = True | |
| break | |
| if not modeAdded: | |
| if not isRomanNumeral and str(getNumber(sect)) == sect: | |
| self.mode = Mode.DOM | |
| modeFound = True | |
| modeAdded = True | |
| deg = getNumber(sect) | |
| if deg != None: | |
| shift = 0 | |
| for char in sect: | |
| if char == "#": | |
| shift += 1 | |
| elif char == "b": | |
| shift -= 1 | |
| if (not modeFound) or deg % 2 == 0: | |
| self.degrees[deg] = shift | |
| elif deg >= 7: | |
| for i in range(7, deg+1): | |
| if i % 2 != 0: | |
| self.degrees[i] = shift | |
| self.split.append(sect) | |
| sect = "" | |
| if not modeAdded: | |
| # Case for minor roman numeral chords | |
| if self.root in rms and self.root == self.root.lower(): | |
| self.mode = Mode.MIN | |
| else: | |
| self.mode = Mode.DOM | |
| if not isSlashChord: | |
| self.bassnote = self.root | |
| for sect in self.split: | |
| isMaj7 = ("maj" in sect) or isMaj7 | |
| if (7 in self.degrees.keys()) and not isMaj7: | |
| self.degrees[7] = -1 | |
| def getMIDI(self, key="c", octave=4): | |
| notes = {} | |
| notes[0] = textToPitch(self.bassnote, key) - 12 | |
| root = textToPitch(self.root, key) | |
| notes[1] = root | |
| notes[3] = root + degreeToShift(3) + MODE_TO_SHIFT[self.mode][3] | |
| notes[5] = root + degreeToShift(5) + MODE_TO_SHIFT[self.mode][5] | |
| for deg in self.degrees.keys(): | |
| notes[deg] = root + degreeToShift(deg) + self.degrees[deg] | |
| for deg in notes.keys(): | |
| notes[deg] += 12 * octave | |
| return list(notes.values()) | |