Created
April 5, 2020 12:31
-
-
Save selenologist/024b24147ecaa4c558a8da8a0899c70d to your computer and use it in GitHub Desktop.
Crappy python script for generating MIDI from chord progressions
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
import re | |
import midi # https://github.com/louisabraham/python3-midi | |
# Transcript for Pachelbel's Canon chord progression: | |
# | |
# Key? D | |
# PROG [] 1 | |
# PROG [1M] 5 | |
# PROG [1M,5M] 6m | |
# PROG [1M,5M,6m] 3m | |
# PROG [1M,5M,6m,3m] 4 | |
# PROG [1M,5M,6m,3m,4M] 1 | |
# PROG [1M,5M,6m,3m,4M,1M] 4 | |
# PROG [1M,5M,6m,3m,4M,1M,4M] 5 | |
# PROG [1M,5M,6m,3m,4M,1M,4M,5M] done | |
# chords by number with modification | |
INPUT_NUMBERED_REGEX = re.compile(r"([ud]?)([0-7])([m+o]?)(add9|M?[0-9]?[0-9]?m?)(sus2|sus4)?") | |
def parse_chord(last, s): | |
match = INPUT_NUMBERED_REGEX.match(s) | |
if match: | |
updown = match.group(1) | |
interval = int(match.group(2)) | |
kind = match.group(3) or 'M' # default to major | |
add = match.group(4) or None # default to no added | |
sus = match.group(5) or None # default to not suspended | |
if sus: | |
# but if sus was valid, replace with just its number | |
sus = int(sus[3]) | |
root = None | |
if updown == 'u' or interval == 0: # interval of zero is just reusing the last value | |
root = last + interval | |
elif updown == 'd': | |
root = last - interval | |
else: | |
# replacement chord number given; starts at 1 so subtract that to get the 0-based index. | |
root = interval - 1 | |
return [root, kind, add, sus], root # returns new chord and new 'last' value | |
else: | |
raise Exception("failed to match s") | |
def chord_string(chord): | |
s = str(chord[0] + 1) | |
for data in chord[1:]: | |
if data: | |
s += str(data) | |
return s | |
def is_done(s): | |
if len(s) == 0 or s == 'x' or s == 'done': | |
return True | |
def get_progression(): | |
last = 0 | |
progression = [] | |
while True: | |
s = input("PROG [" + ",".join(map(chord_string, progression)) + "] ") | |
if is_done(s): | |
return progression | |
ch, last = parse_chord(last, s) | |
progression.append(ch) | |
SEMIS_UNISON = 0 | |
SEMIS_MINSECOND = 1 | |
SEMIS_MAJSECOND = 2 | |
SEMIS_MINTHIRD = 3 | |
SEMIS_MAJTHIRD = 4 | |
SEMIS_FOURTH = 5 | |
SEMIS_DIMFIFTH = 6 | |
SEMIS_FIFTH = 7 | |
SEMIS_AUGFIFTH = 8 # = MINSIXTH | |
SEMIS_MINSIXTH = 8 | |
SEMIS_MAJSIXTH = 9 | |
SEMIS_DIMSEVENTH = 9 # = MAJSIXTH | |
SEMIS_MINSEVENTH = 10 | |
SEMIS_MAJSEVENTH = 11 | |
SEMIS_OCTAVE = 12 | |
SEMIS_MINNINTH = 13 | |
SEMIS_MAJNINTH = 14 | |
SEMIS_MINTENTH = 15 | |
SEMIS_MAJTENTH = 16 | |
SEMIS_ELEVENTH = 17 | |
SEMIS_TWELTH = 19 | |
SEMIS_MINTHIRTEEN= 20 | |
SEMIS_MAJTHIRTEEN= 21 | |
SEMIS_MINFOURTEEN= 22 | |
SEMIS_MAJFOURTEEN= 23 | |
SEMIS_FIFTEENTH = 24 | |
# semitones for major intervals | |
SEMIS_MAJ = [ | |
SEMIS_UNISON, # 0, so subtract 1 from the interval to lookup | |
SEMIS_MAJSECOND, # 1 | |
SEMIS_MAJTHIRD, # 2 | |
SEMIS_FOURTH, # 3 | |
SEMIS_FIFTH, # 4 | |
SEMIS_MAJSIXTH, # 5 | |
SEMIS_MAJSEVENTH, # 6 | |
SEMIS_OCTAVE, # 7 | |
SEMIS_MAJNINTH, # 8 | |
SEMIS_MAJTENTH, # 9 | |
SEMIS_ELEVENTH, # 10 | |
SEMIS_TWELTH, # 11 | |
SEMIS_MAJTHIRTEEN, # 12 | |
SEMIS_MAJFOURTEEN, # 13 | |
SEMIS_FIFTEENTH, # 14 | |
] | |
# semitones for minor intervals | |
SEMIS_MIN = [ | |
SEMIS_UNISON, # 0, so subtract 1 from the interval to lookup | |
SEMIS_MINSECOND, # 1 | |
SEMIS_MINTHIRD, # 2 | |
SEMIS_FOURTH, # 3 | |
SEMIS_FIFTH, # 4 | |
SEMIS_MINSIXTH, # 5 | |
SEMIS_MINSEVENTH, # 6 | |
SEMIS_OCTAVE, # 7 | |
SEMIS_MINNINTH, # 8 | |
SEMIS_MINTENTH, # 9 | |
SEMIS_ELEVENTH, # 10 | |
SEMIS_TWELTH, # 11 | |
SEMIS_MINTHIRTEEN, # 12 | |
SEMIS_MINFOURTEEN, # 13 | |
SEMIS_FIFTEENTH, # 14 | |
] | |
def chord_to_notes(root, kind, add, sus): | |
# default to major third, override for minor, sus2, sus4 | |
third = SEMIS_MAJTHIRD | |
# override the third if suspended chord | |
if sus == 2: | |
third = SEMIS_MAJSECOND # always major second | |
elif sus == 4: | |
third = SEMIS_FOURTH | |
elif kind == 'm' or kind == 'o': # minor and diminished chords have a minor third | |
third = SEMIS_MINTHIRD | |
fifth = SEMIS_FIFTH # default to a real fifth, override for diminished and augmented | |
if kind == 'o': # override for diminished | |
fifth = SEMIS_DIMFIFTH | |
elif kind == 'a': # override for augmented | |
fifth = SEMIS_AUGFIFTH | |
notes = [root, root + third, root + fifth] | |
if add: | |
# special-case add9 | |
if add == 'add9': | |
notes.append(root + SEMIS_MAJNINTH) | |
# special-case 9th chords | |
elif add == '9': # handle dominant ninth (no explicit M9 or 9m = minor 7th + major 9th) | |
notes.append(root + SEMIS_MINSEVENTH) | |
notes.append(root + SEMIS_MAJNINTH) | |
elif add == '9m': # dominant minor ninth (minor 7th + minor 9th) | |
notes.append(root + SEMIS_MINSEVENTH) | |
notes.append(root + SEMIS_MINNINTH) | |
elif add == 'M9': # major ninth = major 7th + major 9th | |
notes.append(root + SEMIS_MAJSEVENTH) | |
notes.append(root + SEMIS_MAJNINTH) | |
# special-case the 7th | |
elif add == 'M7': # handle major seventh | |
notes.append(root + SEMIS_MAJSEVENTH) | |
elif add == '7': | |
# special-case diminished seventh (when not explicitly major) | |
if kind == 'o': | |
notes.append(root + SEMIS_DIMSEVENTH) | |
# or default to minor seventh | |
else: | |
notes.append(root + SEMIS_MINSEVENTH) | |
# treat other added notes as major intervals (may not be correct but I'm over this...) | |
else: | |
notes.append(root + SEMIS_MAJ[int(add) - 1]) | |
return notes | |
MAJOR_SCALE = [0, 2, 4, 5, 7, 9, 11] | |
MINOR_SCALE = [0, 2, 3, 5, 7, 8, 10] | |
def progression_to_notes(progression, scale=MAJOR_SCALE): | |
for chord in progression: | |
degree, kind, add, sus = chord | |
octave = degree // len(scale) | |
index = degree % len(scale) | |
root = octave * SEMIS_OCTAVE + scale[index] | |
yield chord_to_notes(root, kind, add, sus) | |
NOTE_LETTERS = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] | |
def note_string(note): | |
octave = note // SEMIS_OCTAVE | |
index = note % SEMIS_OCTAVE | |
s = "" | |
if octave < 0: | |
s = "-" * -octave | |
elif octave > 0: | |
s = "+" * octave | |
return s + NOTE_LETTERS[index] | |
def main(): | |
#for chord_notes in progression_to_notes(get_progression()): | |
# print(", ".join(map(note_string, chord_notes))) | |
PPQ = 48 | |
note_divisor = 8 | |
note_length = (4 * PPQ) // note_divisor | |
middle_c = 60 # as per GM, do not trust the midi library | |
base_note = middle_c # default to C | |
key = input("Key? ").split() | |
scale = MAJOR_SCALE | |
if len(key) > 0: | |
key_s = key[0] | |
scale_s = "" | |
try: | |
base_note = middle_c + NOTE_LETTERS.index(key_s) | |
except ValueError: | |
print("Couldn't find note", key_s, ", defaulting to C") | |
if len(key) > 1: | |
scale_s = key[1] | |
if scale_s == 'minor': | |
scale = MINOR_SCALE | |
# otherwise default to major | |
pattern = midi.Pattern(resolution=PPQ) | |
track = midi.Track() | |
pattern.append(track) | |
chords = progression_to_notes(get_progression(), scale) | |
def do_chord(chord): | |
for note in chord: | |
track.append(midi.NoteOnEvent(tick=0, pitch=base_note + note, velocity=64)) | |
step = note_length # only the first off message should have a tick >0, then it's reset 0. | |
for note in chord: | |
track.append(midi.NoteOffEvent(tick=step, pitch=base_note + note)) | |
step = 0 | |
def do_arp(chord): | |
for note in chord: | |
track.append(midi.NoteOnEvent(tick=0, pitch=base_note + note, velocity=64)) | |
track.append(midi.NoteOffEvent(tick=note_length, pitch=base_note + note)) | |
def arp_pattern(chord, pattern): | |
for index in pattern: | |
yield chord[index % len(chord)] | |
for chord in chords: | |
do_arp(arp_pattern(chord, [2,1,2,0])) | |
eot = midi.EndOfTrackEvent(tick=1) | |
track.append(eot) | |
midi.write_midifile("/tmp/out.mid", pattern) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment