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:

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