Been a bit of a break from this project. Was working on some draft posts that needed reviewing prior to being deployed. Which led me to spend some time fixing another issue (my temporary loss of interest in that past project at fault). But the database is now once again up-to-date. Will that last,…
Okay, back to trying to generate music programmatically. I thought I might get on to playing some chord progressions, but another function first.
Common and/or Popular Chord Progressions
We wrote a function to generate a random chord progression for a given key. But there are a number of lists on-line of progressions that are considered to be popular. I was thinking of somehow adding those to the random progression generation function. But do believe it would be best to put the option to use a common progression in a separate function. There will be the option to select by name (for a few at least) and randomly for those without specific labels. A few of the named ones were mentioned in the last post.
For now I am going to ignore progressions using chords borrowed from the parallel key. Music theory sure is a lot more complicated than I thought.
And, as I started working on this function I decided that it would only return the roman numeral list for the progression. I will refactor the function make_progression, from the previous post, to do the same. Then write a new function that will take that list and the chords for the specific scale and return the progression as a list of chords. Should be a short and sweet function. Well, unless I do decide to use borrowed chords. Then…
But for now, let’s get back to that common chord progressions’ function.
The Progressions
Decided to put these in a dictionary outside the function. Never know where else I might use it. Or when I might want to add new progressions. Here’s it’s current state. And, yes there is at least one progression with a borrowed chord. And, I am not really certain how to handle that rhythm changes progression. Will need to find out what those slashes mean or cause to happen. Always seems more research is needed with this project, regardless of my decisions and/or planning.
Where I couldn’t find a known name for a progression, I just used the string of the progression’s roman numerals for both the key and value. I also put all name keys in lowercase. The roman numberal keys retain their case (obviously?). (This list has been enhanced once or twice.)
# https://online.berklee.edu/takenote/common-chord-progressions-and-how-to-make-them-your-own/
# https://en.wikipedia.org/wiki/List_of_chord_progressions
# https://www.piano-keyboard-guide.com/chords-by-key.html
# https://www.musictheoryacademy.com/understanding-music/chord-progressions/
# https://www.hooktheory.com/blog/minor-chord-progressions/
# Major Key: I - ii - iii - IV - V - vi - vii°
# Minor Key: i - ii° - III - iv - v - VI - VII
# Capital numerals refer to major chords and lowercase numerals to minor chords. The little circle symbol refers to a “diminished” triad (minor chord with a flat 5th).
# https://www.musicalchord.com/en/music-theory/chord-progression
# dictionary of common chords
common_prog = {
"ballad": "I-ii-I",
"I-ii-IV-V": "I-ii-IV-V",
"I-iii-ii-V": "I-iii-ii-V",
"I-iii-IV-V": "I-iii-IV-V",
"I-iii-vi-V": "I-iii-vi-V",
"I IV V": "I-IV-V",
"montgomery-ward bridge": "I-IV-ii-V",
"blues": "I-IV-V7",
"blues2": "I7-IV7-V7",
"passamezzo moderno": "I-IV-I-V-I-IV-I-V-I",
"rhythm changes": "I-iv-ii-V / I-I7-iv-I-V-I / III7-VI7-II7-V7",
"sixteen-bar blues": "I-I-I-I-I-I-I-I-IV-IV-I-I-V-IV-I-I",
"twelve-bar blues": "I-I-I-I-IV-IV-I-I-V-IV-I-V",
"four magic chords": "I-V-vi-IV",
"chromatic descending 5-6": "I-V-♭VII-IV",
"eight-bar blues": "I-V-IV-IV-I-V-I-V",
"pachelbel's": "I-V-vi-iii-IV-I-IV-V",
"I-vi-ii-V": "I-vi-ii-V",
"doo-wop": "I-vi-IV-V",
"jazz standard": "ii-V-I",
"andalusian cadence": "iv-III-♭II-I",
"j-pop": "IV-III-vi-I7",
"IV-V-I-vi": "IV-V-I-vi",
"V-IV-I turnaround": "V-IV-I",
"circle of fifths": "vi-ii-V-I",
"pop": "vi-IV-I-V",
"♭VI ♭VII I cadence": "vi-♭VII-I",
"i-bIII-ii°-V7": "",
"passamezzo antico": "i-VII-i-V-III-VII-i-V-i",
"romanesca": "III-VII-i-V-III-VII-i-V-i",
"i-VI-VII": "i-VI-VII",
"i-iv-VII": "i-iv-VII",
"doo-wop_m": "i-iv-v",
"i-bVI-ii°-v7": "i-bVI-ii°-v7",
"i-VI-III-VII": "i-VI-III-VII",
"jazz standard_m": "ii-v-i"
}
The Function
def get_progression(n_prg:str) -> list[str]:
""" Select a named or random common chord progression from available progressions
:param n_prg: name of desired progression or string beginning with "rand"
:return: list of roman numerals specifying chords in the progression
or empty string if name not in dict or name doesn't start with "rand"
"""
if n_prg.startswith("rand"):
return rng.choice(list(common_prog.values()))
elif n_prg in common_prog.keys():
return common_prog[n_prg]
else:
return ""
More lack of real knowledge. Going around in circles me.
In the common_prog dictionary, you will see some progressions containing an uppercase I and some a lowercase i. The former are apparently for major scales and the latter for minor scales. I have decided to, for now, ignore that whole concept. I will treat lower and upper case roman numerals identically.
I’ll do some testing shortly. But first I want to refactor the make_progression function to behave the same as get_progession. I.E. simply return a list of roman numerals, not a list of specific chords. Then I would like to write a function that takes those lists of roman numerals, some other information and returns a list of chords I can use to play a chord progression in some fashion or other. Which may involve even more new/refactored functions.
Refactor Previous Progression Generation Function
I expect this to be pretty simple. Modify the parameter list, remove some code and return a different list. Well, did actually require a little more refactoring than that. But only going to show the changes and maybe deletions (as comments).
def make_progression(n_chd:int) -> list[str]:
... ...
# c_prg = [chords[0]] # chord progression
c_prg = ["I"]
... ...
# c_prg.append(chords[f2loc[nxt_p]])
c_prg.append(nxt_p)
f_cnt[nxt_fn] += 1
p_fns.append(str(nxt_fn))
s_cp = '-'.join(c_prg)
return s_cp
Quick Test of New Functions
... ...
if do_cpg:
... ...
for i in range(3, 7):
print(f"\nmake_progression({i})")
c_prg = make_progression(i)
print(f"\t'{c_prg}'")
for cp in ["doo-wop", "random", "circle of fifths", "rand", "not in list"]:
print(f"\nget_progression({cp})")
c_prg = get_progression(cp)
print(f"'\t{c_prg}'")
And, in the terminal I go the following for one given run.
(base) PS R:\learn\e_music> uv run tst_chords_rek.py
make_progression(3)
'I-IV-I'
make_progression(4)
'I-vii-ii-I'
make_progression(5)
'I-iii-IV-vii-vi'
make_progression(6)
'I-IV-iii-ii-V-iii'
get_progression(doo-wop)
'I-vi-IV-V'
get_progression(random)
'♭VI-♭VII-I'
get_progression(circle of fifths)
'vi-ii-V-I'
get_progression(rand)
'i-VI-III-VII'
get_progression(not in list)
''
And they both appear to work as intended. Though hard to say at this point exactly what I intended!?
And, I expect there will be any number of refactorings. You know: diminished vii, ♭VII, V7, etc.
Generate List of Chords From List of Roman Numerals
Okay, lets see what we can do with those lists of roman numerals those two functions give us.
We will, as previously, need to know the appropriate chords for the scale in which we will be working. And, this time I will also tell the function the form/mode of the scale (e.g. major, minor). Not sure why, but it may be used for some decision making down the road. I couldn’t decide whether to pass the scale and let the function sort the appropriate chords or to pass it the appropriate chords. For now going with the latter. I can determine the root note for the scale from that if needed. Though not sure it will be needed.
Can’t believe how I keep feeling like I am digging the hole I am in deeper and deeper. Haven’t looked at the current code for about 2 weeks. Just don’t seem to see my way forward. Perhaps took on a little too much? Though some of my state of mind has been caused by some complications in life at home.
Slow and Easy
Okay, let’s start simple and go slowly. Unlike previously, I will not be passing the scale or the suitable chords to the function. It will determine those itself. (Lot’s of print statements to follow what’s happening.) I chose to return the roman numerals for the chord progressions as a string of roman numerals separated by dashes as the numerals needed to be separated in some fashion. So, need to deal with that in the code. May need to rethink that return value. But then, simple fix for now.
def convert_cprog(cp_rn:str, r_nt:Notes_scale, p_qual:Chords_ok="major") -> list[tuple[Notes_scale, ChordFormula]]:
""" Convert a list of roman numerals into the actual chord progression.
But only chord root and quality, not actual chord notes.
param cp_rn: string of roman numerals + modifier symbols for chord progression separated by a dash
param: r_nt: root note including any incidental for the music scale
param: s_qual: type/quality of scale, e.g. major, min_nat, etc.
return: list of tuples identifying the specific chords in the progression
"""
# expect this to get rather messy
# chord location in sumbitted chords
f2loc = {"i": 0, "ii": 1, "iii": 2, "iv": 3, "v": 4, "vi": 5, "vii": 6}
chd_prg = []
# let's start by getting the notes for the appropriate scale
s_nts = get_scale_4_note(r_nt)
# then the available chords for that scale
s_chds = scale_chords(s_nts, p_qual)
print(f"{s_nts}\n{s_chds}")
# only want romman numerals, need to remove dashes
for rn in cp_rn.split("-"):
chd_prg.append(s_chds[f2loc[rn.lower()]])
return chd_prg
And some more tests.
print(f"\nC min_nat scale, made up progression")
p_rn = make_progression(4)
print(p_rn)
rn2ch = convert_cprog(p_rn, "C", "min_nat")
print(rn2ch)
# In G major, it would be: G (I) – Em (vi) – C (IV) – D (V)
print(f"\nG major scale, doo-wop progression")
p_rn = get_progression("doo-wop")
print(p_rn)
rn2ch = convert_cprog(p_rn, "G", "major")
print(rn2ch)
# In the key of D, it would be: D (I) - A (V) - Bm (vi) - F#m (iii) - G (IV) - D (I) - G (IV) - A (V)
print(f"\nD major scale, pachelbel's progression")
p_rn = get_progression("pachelbel's")
print(p_rn)
rn2ch = convert_cprog(p_rn, "D", "major")
print(rn2ch)
And in the terminal the following output was produced.
(base) PS R:\learn\e_music> uv run tst_chords_rek.py
chord progression with overtones on each chord
C min_nat scale, made up progression
I-V-IV-V
['C4', 'D4', 'E4', 'F4', 'G4', 'A4', 'B4']
[('C', 'minor'), ('D', 'dim'), ('E', 'major'), ('F', 'minor'), ('G', 'minor'), ('A', 'major'), ('B', 'major')]
[('C', 'minor'), ('G', 'minor'), ('F', 'minor'), ('G', 'minor')]
G major scale, doo-wop progression
I-vi-IV-V
['G4', 'A4', 'B4', 'C5', 'D5', 'E5', 'F#5']
[('G', 'major'), ('A', 'minor'), ('B', 'minor'), ('C', 'major'), ('D', 'major'), ('E', 'minor'), ('F#', 'dim')]
[('G', 'major'), ('E', 'minor'), ('C', 'major'), ('D', 'major')]
D major scale, pachelbel's progression
I-V-vi-iii-IV-I-IV-V
['D4', 'E4', 'F#4', 'G4', 'A4', 'B4', 'C#5']
[('D', 'major'), ('E', 'minor'), ('F#', 'minor'), ('G', 'major'), ('A', 'major'), ('B', 'minor'), ('C#', 'dim')]
[('D', 'major'), ('A', 'major'), ('B', 'minor'), ('F#', 'minor'), ('G', 'major'), ('D', 'major'), ('G', 'major'), ('A', 'major')]
Which for the last two definitely appears to be correct.
What About Incidentals and Such in the Progression?
Let’s see if we can easily refactor the function to deal with incidentals and/or chord specifications in the progression. E.G. IV-III-vi-I7, iv-III-♭II-I or i-bVI-ii°-v7. I am for now only going to deal with the few such modifiers that I currently have in the list of common progressions. I do not, at this time, plan to attempt to add any such modifiers to the progressions I generate randomly in make_progression.
Okay, a new function to sort the notes I am using, only sharps, versus equivalent flats. May need to rethink that down the road, but for now will stick with the current approach.
def convert_2_flat(nt:Notes_scale) -> Notes_scale:
nt2b = {
"C" : "B", "C#": "C", "D": "C#", "D#": "D", "E": "D#", "F": "E",
"F#": "F", "G": "F#", "G#": "G", "A": "G#", "A#": "A", "B": "A#"
}
return nt2b[nt]
And, the refactored function, convert_cprog. But only the changes. For now a fair bit of duplication in each if/elif block. Will try to refactor before I publish this post.
def convert_cprog(cp_rn:str, r_nt:Notes_scale, p_qual:Chords_ok="major") -> list[tuple[Notes_scale, ChordFormula]]:
... ...
# for now only deal with following modifiers: leading ♭, trailing 7 or °
if "°" in rn:
t_rn = rn[:-1]
c_rt = s_chds[f2loc[t_rn.lower()]][0]
chd_prg.append((c_rt, "dim"))
elif "7" in rn:
t_rn = rn[:-1]
c_rt = s_chds[f2loc[t_rn.lower()]][0]
c_qual = "major" if t_rn.isupper() else "minor"
chd_prg.append((c_rt, f"{c_qual}7"))
elif "♭" in rn:
t_rn = rn[1:]
# convert roman numeral to chord's root note
c_rt = s_chds[f2loc[t_rn.lower()]][0]
c_qual = "major" if t_rn.isupper() else "minor"
# for now I don't deal with notes labelled flat, bad choice perhaps
chd_prg.append((convert_2_flat(c_rt), f"{c_qual}"))
else:
# print(f"s_chds[f2loc[{rn}.lower()]] -> s_chds[f2loc[{rn.lower()}]")
chd_prg.append(s_chds[f2loc[rn.lower()]])
Some additional tests. Again, only the new ones. Didn’t bother testing the diminished modification (similar enough to 7th that didn’t think it necessary).
... ...
# In the key of C major the progression goes: A♭-B♭-C -> G#-A#-C.
print(f"\nC major scale, ♭VI-♭VII-I cadence")
p_rn = get_progression("♭VI-♭VII-I cadence")
print(p_rn)
rn2ch = convert_cprog(p_rn, "C", "major")
print(rn2ch)
# C - F - G7: Popular in blues and rock, creating strong tension and resolution.
print(f"\nC major scale, blues progression")
p_rn = get_progression("blues")
print(p_rn)
rn2ch = convert_cprog(p_rn, "C", "major")
print(rn2ch)
And, in the terminal, the tests—all of them—output the following.
(base) PS R:\learn\e_music> uv run tst_chords_rek.py
chord progression with overtones on each chord
C min_nat scale, made up progression
I-IV-vii-ii
['C4', 'D4', 'E4', 'F4', 'G4', 'A4', 'B4']
[('C', 'minor'), ('D', 'dim'), ('E', 'major'), ('F', 'minor'), ('G', 'minor'), ('A', 'major'), ('B', 'major')]
[('C', 'minor'), ('F', 'minor'), ('B', 'major'), ('D', 'dim')]
G major scale, doo-wop progression
I-vi-IV-V
['G4', 'A4', 'B4', 'C5', 'D5', 'E5', 'F#5']
[('G', 'major'), ('A', 'minor'), ('B', 'minor'), ('C', 'major'), ('D', 'major'), ('E', 'minor'), ('F#', 'dim')]
[('G', 'major'), ('E', 'minor'), ('C', 'major'), ('D', 'major')]
D major scale, pachelbel's progression
I-V-vi-iii-IV-I-IV-V
['D4', 'E4', 'F#4', 'G4', 'A4', 'B4', 'C#5']
[('D', 'major'), ('E', 'minor'), ('F#', 'minor'), ('G', 'major'), ('A', 'major'), ('B', 'minor'), ('C#', 'dim')]
[('D', 'major'), ('A', 'major'), ('B', 'minor'), ('F#', 'minor'), ('G', 'major'), ('D', 'major'), ('G', 'major'), ('A', 'major')]
C major scale, ♭VI-♭VII-I cadence
♭VI-♭VII-I
['C4', 'D4', 'E4', 'F4', 'G4', 'A4', 'B4']
[('C', 'major'), ('D', 'minor'), ('E', 'minor'), ('F', 'major'), ('G', 'major'), ('A', 'minor'), ('B', 'dim')]
[('G#', 'major'), ('A#', 'major'), ('C', 'major')]
C major scale, blues progression
I-IV-V7
['C4', 'D4', 'E4', 'F4', 'G4', 'A4', 'B4']
[('C', 'major'), ('D', 'minor'), ('E', 'minor'), ('F', 'major'), ('G', 'major'), ('A', 'minor'), ('B', 'dim')]
[('C', 'major'), ('F', 'major'), ('G', 'major7')]
I Believe That’s It for This One
I had hoped to actually try to generate and play some of those chord progressions. But, this post is reasonably long and focused. Has cost me considerable time and mental anxiety. So, I think I am closing it out. Will see what I can manage next time.
Resources
- What Is a Diminished Chord and How to Use Them
- Introduction to Seventh Chords
- Chord Symbols
- Borrowed Chords