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