The plan was to simply write some code to play a chord progression. But given what we managed in the last post that seems a little less than a meaningful endeavour. As a result, it seems I have reached an intersection with many roads running off in every direction.
I was thinking I should be able to determine the chords available for a given musical key. Then generate good sounding chord progressions from the available chords. So, started looking at some musical theory on-line. Wow, my brain was barely getting the basics, never mind the enhancements. Though I must admit I am not interested in becoming a musician or a composer as such. An example or two:
- Circle of Fifths: The Key to Unlocking Harmonic Understanding
- The Circle of Fifths Progression
- Chords By Key
And, I haven’t yet looked at a minor scale. Well, or any other. I was also thinking that I need to limit the length of notes to something meaningful in music. I.E. full, half, —, sixteenth notes. And, what about rest values—something that hasn’t occurred to me up until now. I don’t plan on writing any sheet music, but I may also need to think about slurs, dots and ties.
Keys, Chords and Progressions
Scales
It appears that I didn’t fully understand the concept of scales in music. There are considerably more of them than I realized. In the preceding post I was dealing with chormatic scales. But in this post we will primarily be dealing with diatonic or heptatonic scales.
Scales in traditional Western music generally consist of seven notes and repeat at the octave. Notes in the commonly used scales (see just below) are separated by whole and half step intervals of tones and semitones. The harmonic minor scale includes a three-semitone step (an augmented second); the anhemitonic pentatonic includes two of those and no semitones.
Western music in the Medieval and Renaissance periods (1100–1600) tends to use the white-note diatonic scale C–D–E–F–G–A–B. Accidentals are rare, and somewhat unsystematically used, often to avoid the tritone.
Scale (music)
Chords and Progression
There is a specific sequence of chords for each diatonic scale based on the note at the first degree (tonic). That is, that sequence will be different for the C-scale and the F-scale. But their patterns will be identical. There is one chord named for each note in the scale. The quality of the chord is as follows.
- Major keys: major, minor, minor, major, major, minor, diminished
- Minor keys: minor, diminished, major, minor, minor, major, major
And, if we go to 7th chords, the above will change slightly.
Now a progression could conceivably use any of the chords available in the specified key. But I expect that may or may not generate pleasant sounding music. Chords are placed in three fundamental groups or functions.
- tonic: these chords create a feeling of rest and stability; tonic chord (I) plus iii and vi
- dominant: creates tension or instability and seeks to go elsewhere, typically leads a tonic chord; V and VII chords
- predominant: generates moderate tension, usually leading to a dominant chord, though could lead to tonic chord; ii and IV
I expect these functions have a big effect on successful chord progressions.
That said, there are a number of common chord progressions.
So far the above and all my reading has left me more confused than anything. So, I plan to play and hopefully learn a little. Though as I write this, I don’t really have any plan of action.
Let’s look at a couple of the common chord progressions and see how the chord functions line-up.
- doo-wop
- “Earth Angel”, “Heart and Soul”, Elton John’s “Crocodile Rock”, Ed Sheeran’s “Perfect”
- I vi IV V -> tonic tonic predominant dominant
- Pachebel’s
- Pachelbel’s “Canon in D”, Green Day’s “Basket Case”, Aerosmith’s “Cryin’”
- I V vi iii IV I IV V -> tonic dominant tonic tonic predominant tonic predominant dominant
- the four magic chords
- The Beatles’ “Let It Be,” Bob Marley’s “No Woman No Cry,” U2’s “With or Without You,”
- I V vi IV -> tonic dominant tonic predominant
And that seems to indicate that those chord functions do in fact get recognized/utilized when building chord progressions.
Develop, Play, Test
Do believe it is time to do some coding and see what can be learned or created. So far all the code in this post is being developed in my test module.
Major + Minor Keys
The first thing I thought I would do is refactor or add a function to produce minor chords. Again over two octaves as the function get_scale_4_note currently does for the major scale. I am going to start by trying to get a single function to do both. So I think a new function, for now same name.
Well, turns out both is a bit of misnomer. There are, in fact, three variations of the minor scale: Natural, Harmonic, and Melodic. For now I plan to code for those three and the major in the new function. Wish me luck.
Diatonic Scales Function
I think the first new function should be one to produce the correct diatonic/heptatonic scales for the major and minor groups. Expect that will make life a touch easier down the road. And, once you have the appropriate formulae for the scales it is pretty straightforward. Haven’t yet looked at tidying up my code, so here’s the first effort.
Added an enum for the allowed scale modes. For coding and for typing.
... ...
class Scale_forms():
"""Currently acceptable scale modes and there formulae"""
def __init__(self):
self.s_forms = {
"major": ["W", "W", "H", "W", "W", "W", "H"],
"min_nat": ["W", "H", "W", "W", "H", "W", "W"],
"min_har": ["W", "H", "W", "W", "H", "W½", "H"],
"min_mel": ["W", "H", "W", "W", "W", "W", "H"],
}
... ...
def get_scale_4_note(r_nt:Notes_scale, s_mode:Scale_forms="major") -> list[Notes_scale]:
""" Get the sequence of scale notes for the specific root note
I assume that the root note is in the 4th octave of a piano.
Returning 2 octaves to simplify generating lengthier chords.
:param r_nt: root note symbol for the desired chromatic scalle, str
:param s_mode: mode in which to generate scale, str, one of
Scale_forms.s_forms
:return: list of the notes in the appropriate order for 2 octaves
"""
s_frms = {
"major": ["W", "W", "H", "W", "W", "W", "H"],
"min_nat": ["W", "H", "W", "W", "H", "W", "W"],
"min_har": ["W", "H", "W", "W", "H", "W½", "H"],
"min_mel": ["W", "H", "W", "W", "W", "W", "H"],
}
# bloody hell!, 3 minor scales, who knew?? ascending/descending?
# melodic minor also has descending version which is natural minor
s_frms = Scale_forms()
step_s_2_int = {"W": 2, "H": 1, "W½": 3}
h_scale, h_steps = [], []
if s_mode not in s_frms.s_forms:
return h_scale
# get notes for two piano octaves starting at tonic note
t_scale = get_2_oct_piano(r_nt)
# generate requested scale
s_steps = s_frms.s_forms[s_mode]
c_step = 0
h_steps.append(c_step)
h_scale.append(t_scale[c_step])
for i, stp in enumerate(s_steps[:-1]):
c_step += step_s_2_int[stp]
h_steps.append(c_step)
h_scale.append(t_scale[c_step])
return h_scale
And a wee test.
if do_cpg:
# generate chord progression with overtones
print(f"chord progression with overtones on each chord")
# test get_scale_4_note(r_nt:Notes_scale, s_mode:str="major")
# C, D, E, F, G, A, B
print(f"C major: {get_scale_4_note('C')}")
# C, D, D#, F, G, G#, A#
print(f"C minor: {get_scale_4_note('C', 'min_nat')}")
# B C# D E F# G A
print(f"B natural minor: {get_scale_4_note('B', 'min_nat')}")
# A B C D E F# G#
print(f"A melodic minor: {get_scale_4_note('A', 'min_mel')}")
# A B C D E F G#
print(f"A harmonic minor: {get_scale_4_note('A', 'min_har')}")
And, in the terminal I got the following.
PS R:\learn\e_music> uv run tst_chords_rek.py
chord progression with overtones on each chord
C major: ['C4', 'D4', 'E4', 'F4', 'G4', 'A4', 'B4']
C minor: ['C4', 'D4', 'D#4', 'F4', 'G4', 'G#4', 'A#4']
B natural minor: ['B4', 'C#5', 'D5', 'E5', 'F#5', 'G5', 'A5']
A melodic minor: ['A4', 'B4', 'C5', 'D5', 'E5', 'F#5', 'G#5']
A harmonic minor: ['A4', 'B4', 'C5', 'D5', 'E5', 'F5', 'G#5']
And, for that minimal bit of testing, seems to indicate the function works as expected.
Chords by Scale
Now, we saw above that there a patterns for the chords allowed for a given scale—one for each note in the scale. And, each chord can only use notes in the relevant scale. For now I am going to ignore 7th chords.
This really is getting to be a lot of work.
Chords for Scale Function
I am going to return a list of the permissible chords for the given scale (tonic note and scale type). I had thought I would generate the scale in this function, but have for now decided to return the list of chords (root note and chord quality). It is likely I will need that scale and its notes in other places; so, just generate it once. I was going to return a list of chords as strings, e.g. “D major”, “A# minor”. Have also decided, for now, to return a tuple as the chord generation function takes the two items as separate parameters. So, (“D”, “major”), (“A#”, “minor”), etc.
Pretty simple, but expect it will come in handy.
... ...
class Chords_ok():
""" Order of chords for a given scale type (see Scale_forms)
"""
def __init__(self):
self.c_series = {
"major": {
"I": "major",
"ii": "minor",
"iii": "minor",
"IV": "major",
"V": "major",
"vi": "minor",
"vii": "dim",
},
"min_nat": {
"i": "minor",
"ii": "dim",
"III": "major",
"iv": "minor",
"v": "minor",
"VI": "major",
"VII": "major",
},
"min_har": {
"i": "minor",
"ii": "dim",
"iii": "aug",
"iv": "minor",
"V": "major",
"VI": "major",
"vii": "dim",
},
"min_mel": {
"i": "minor",
"ii": "minor",
"iii": "aug",
"IV": "major",
"V": "major",
"vi": "dim",
"vii": "dim",
},
}
... ...
def scale_chords(scale:list[Notes_scale], s_mode:Scale_forms="major") -> list[tuple[str, str]]:
""" Return the acceptable chords for the given scale and scale type.
:param scale: a list of the notes in the scale
:param s_mode: the scale type/mode, one of keys in Scale_forms
:return: acceptable chords for key, list of tuples, (note, chord type)
"""
c_list = list(Chords_ok().c_series[s_mode].values())
# print(f"c_list({scale[0]}, {s_mode}): {c_list}")
ok_chrds = []
for i, nt in enumerate(scale):
ok_chrds.append((nt[:-1], c_list[i]))
return ok_chrds
And an updated/refactored test block.
if do_cpg:
# generate chord progression with overtones
print(f"chord progression with overtones on each chord")
# test get_scale_4_note(r_nt:Notes_scale, s_mode:str="major")
# C, D, E, F, G, A, B
print(f"C major:")
cj_scl = get_scale_4_note('C')
print(f"\t{cj_scl}")
# C major, D minor, E minor, F major, G major, A minor, and B diminished.
print(f"\t{scale_chords(cj_scl, "major")}")
# C, D, D#, F, G, G#, A#
print(f"C minor:")
ci_scl = get_scale_4_note('C', 'min_nat')
print(f"\t{ci_scl}")
# C minor, D diminished, E flat major, F minor, G minor, A flat major, B flat major
print(f"\t{scale_chords(ci_scl, "min_nat")}")
# B C# D E F# G A
print(f"B natural minor:")
bi_scl = get_scale_4_note('B', 'min_nat')
print(f"\t{bi_scl}")
# B minor, C# diminished, D major, E minor, F# minor, G major, A major
print(f"\t{scale_chords(bi_scl, "min_nat")}")
# A B C D E F# G#
print(f"A melodic minor:")
ai_scl = get_scale_4_note('A', "min_mel")
print(f"\t{ai_scl}")
# Am, Bm, C+, D, E, F#dim, G#dim
print(f"\t{scale_chords(ai_scl, "min_mel")}")
# A B C D E F G#
print(f"A harmonic minor:")
ai_scl = get_scale_4_note('A', "min_har")
print(f"\t{ai_scl}")
# Am, Bdim, C+, Dm, E, F, G#dim
print(f"\t{scale_chords(ai_scl, "min_har")}")
And in the terminal the following was displayed.
PS R:\learn\e_music> uv run tst_chords_rek.py
chord progression with overtones on each chord
C major:
['C4', 'D4', 'E4', 'F4', 'G4', 'A4', 'B4']
[('C', 'major'), ('D', 'minor'), ('E', 'minor'), ('F', 'major'), ('G', 'major'), ('A', 'minor'),
('B', 'dim')]
C minor:
['C4', 'D4', 'D#4', 'F4', 'G4', 'G#4', 'A#4']
[('C', 'minor'), ('D', 'dim'), ('D#', 'major'), ('F', 'minor'), ('G', 'minor'), ('G#', 'major'),
('A#', 'major')]
B natural minor:
['B4', 'C#5', 'D5', 'E5', 'F#5', 'G5', 'A5']
[('B', 'minor'), ('C#', 'dim'), ('D', 'major'), ('E', 'minor'), ('F#', 'minor'), ('G', 'major'),
('A', 'major')]
A melodic minor:
['A4', 'B4', 'C5', 'D5', 'E5', 'F#5', 'G#5']
[('A', 'minor'), ('B', 'minor'), ('C', 'aug'), ('D', 'major'), ('E', 'major'), ('F#', 'dim'),
('G#', 'dim')]
A harmonic minor:
['A4', 'B4', 'C5', 'D5', 'E5', 'F5', 'G#5']
[('A', 'minor'), ('B', 'dim'), ('C', 'aug'), ('D', 'minor'), ('E', 'major'), ('F', 'major'),
('G#', 'dim')]
Chord Progressions by Scale
Well, don’t think I am going to get around to generating/playing any chord progressions this post. But before calling it quits I want to look at producing chord progressions for a given scale. I will try to allow for selecting a known progression, or generating one at random (well not quite random, there will be some rules for each chord position). Though not sure how to handle the known progressions as I am sure I won’t find a name for each one.
For now a randomly generated progression for a given scale. Nowever I won’t be passing a scale, but rather the chords for some scale. The function won’t know what scale. The length of the progression will be another parameter. A bit lengthy and pretty convoluted, but… And, may yet get more convoluted once I start testing and playing generated progressions. Have removed the print statements I used during testing though they will be included in the terminal output for my test code.
... ...
def make_progression(chords:list[tuple[str, str]], n_chd:int) -> list[tuple[str, str]]:
# chord location in sumbitted chords
f2loc = {"I": 0, "ii": 1, "iii": 2, "IV": 3, "V": 4, "vi": 5, "vii": 6}
# chords for each chord function type
c_fns = {
"tonic": ["I", "iii", "vi"],
"predom": ["ii", "IV"],
"domin": ["V", "vii"],
}
p_fns = ["tonic"] # chord function progression, used to determine next chord
f_cnt = {"tonic": 1, "domin": 0, "predom": 0} # count for each function to-date
c_prg = [chords[0]] # chord progression
# I think this is going to get horribly messy/complex
while len(c_prg) < n_chd:
p_init = len(c_prg) == 1
p_pnlt = len(c_prg) == (n_chd - 2)
p_last = len(c_prg) == (n_chd - 1)
if n_chd > 3 and (p_pnlt or p_last) and f_cnt["predom"] == 0:
# if nearing end of progression of length 4 or better,
# and no prdeominant in progression, add one
nxt_fn = "predom"
nxt_p = rng.choice(c_fns[nxt_fn])
elif p_fns[-1] == "tonic":
nxt_fn = rng.choice(["tonic", "domin", "predom"])
if (p_init or p_last) and nxt_fn == "tonic":
# don't double up the base chord
nxt_p = rng.choice(c_fns[nxt_fn][1:])
else:
nxt_p = rng.choice(c_fns[nxt_fn])
elif p_fns[-1] == "predom":
# generally leads to dominant chord
nxt_fn = rng.choice(["tonic", "domin"], p=[.3, .7])
nxt_p = rng.choice(c_fns[nxt_fn])
elif p_fns[-1] == "domin":
# generally leads to a tonic chord
nxt_fn = rng.choice(["tonic", "predom"], p=[.8, .2])
if p_last and nxt_fn == "tonic":
# don't double up the base chord
nxt_p = rng.choice(c_fns[nxt_fn][1:])
else:
nxt_p = rng.choice(c_fns[nxt_fn])
c_prg.append(chords[f2loc[nxt_p]])
f_cnt[nxt_fn] += 1
p_fns.append(str(nxt_fn))
return c_prg
... ...
bm_scl = get_scale_4_note('B', 'major')
bm_chds = scale_chords(bm_scl, "major")
for i in range(3, 7):
c_prg = make_progression(bm_chds, i)
And in the terminal the following output was printed.
PS R:\learn\e_music> uv run tst_chords_rek.py
B major chords: [('B', 'major'), ('C#', 'minor'), ('D#', 'minor'), ('E', 'major'), ('F#', 'major'), ('G#', 'minor'), ('A#', 'dim')]
make_progression(bm_chds, 3)
domin -> vii -> ('A#', 'dim')
-> ['tonic', 'domin'] -> {'tonic': 1, 'domin': 1, 'predom': 0}
tonic -> iii -> ('D#', 'minor')
-> ['tonic', 'domin', 'tonic'] -> {'tonic': 2, 'domin': 1, 'predom': 0}
[('B', 'major'), ('A#', 'dim'), ('D#', 'minor')]
make_progression(bm_chds, 4)
tonic -> iii -> ('D#', 'minor')
-> ['tonic', 'tonic'] -> {'tonic': 2, 'domin': 0, 'predom': 0}
predom -> IV -> ('E', 'major')
-> ['tonic', 'tonic', 'predom'] -> {'tonic': 2, 'domin': 0, 'predom': 1}
domin -> V -> ('F#', 'major')
-> ['tonic', 'tonic', 'predom', 'domin'] -> {'tonic': 2, 'domin': 1, 'predom': 1}
[('B', 'major'), ('D#', 'minor'), ('E', 'major'), ('F#', 'major')]
make_progression(bm_chds, 5)
predom -> ii -> ('C#', 'minor')
-> ['tonic', 'predom'] -> {'tonic': 1, 'domin': 0, 'predom': 1}
domin -> V -> ('F#', 'major')
-> ['tonic', 'predom', 'domin'] -> {'tonic': 1, 'domin': 1, 'predom': 1}
tonic -> I -> ('B', 'major')
-> ['tonic', 'predom', 'domin', 'tonic'] -> {'tonic': 2, 'domin': 1, 'predom': 1}
tonic -> vi -> ('G#', 'minor')
-> ['tonic', 'predom', 'domin', 'tonic', 'tonic'] -> {'tonic': 3, 'domin': 1, 'predom': 1}
[('B', 'major'), ('C#', 'minor'), ('F#', 'major'), ('B', 'major'), ('G#', 'minor')]
make_progression(bm_chds, 6)
tonic -> iii -> ('D#', 'minor')
-> ['tonic', 'tonic'] -> {'tonic': 2, 'domin': 0, 'predom': 0}
predom -> ii -> ('C#', 'minor')
-> ['tonic', 'tonic', 'predom'] -> {'tonic': 2, 'domin': 0, 'predom': 1}
domin -> vii -> ('A#', 'dim')
-> ['tonic', 'tonic', 'predom', 'domin'] -> {'tonic': 2, 'domin': 1, 'predom': 1}
predom -> ii -> ('C#', 'minor')
-> ['tonic', 'tonic', 'predom', 'domin', 'predom'] -> {'tonic': 2, 'domin': 1, 'predom': 2}
domin -> vii -> ('A#', 'dim')
-> ['tonic', 'tonic', 'predom', 'domin', 'predom', 'domin'] -> {'tonic': 2, 'domin': 2, 'predom': 2}
[('B', 'major'), ('D#', 'minor'), ('C#', 'minor'), ('A#', 'dim'), ('C#', 'minor'), ('A#', 'dim')]
Fini
You know I believe this one is done. Getting to be pretty lengthy for my liking. And, I think, still a ways from playing any chord progressions.
Never realized that music and music theory would prove this confusing and difficult. Makes one truly wonder how composers like Bach, Beethoven, Mozart and Vivaldi managed to produce so many different and enjoyable compositions. Especially in relatively short life spans.
Resources
- Scale (music)
- Music Scales: A Beginner’s Guide = Major and Minor Scales Made Simple
- Minor Scales
- Minor Scales
- Rhythmic and Rest Values
- Dots and Ties
- Chords By Key
- Circle of Fifths: The Key to Unlocking Harmonic Understanding
- The Circle of Fifths Progression
- Common Chord Progressions and How to Make Them Your Own