video2music / utilities /chord_to_midi.py
kjysmu's picture
add files
4e46a55
# 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())