Skip to content

Instantly share code, notes, and snippets.

@selenologist
Created April 5, 2020 12:31
Show Gist options
  • Save selenologist/024b24147ecaa4c558a8da8a0899c70d to your computer and use it in GitHub Desktop.
Save selenologist/024b24147ecaa4c558a8da8a0899c70d to your computer and use it in GitHub Desktop.
Crappy python script for generating MIDI from chord progressions
#!/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