Last time I said I needed to apply some brain power to sort where and how to get to my next goal: generating a melody. I will start by trying to produce something that might resemble a traditional eight bar melody. Hopefully employing some of the standard construction principles for doing so. I expect how exactly to do so in code is going to prove, for me, challenging. But, I have been led to understand that “nothing ventured, nothing gained.”

Some Initial Thoughts

Going to start with 8 bar melodies. Using a few basic rules to set each one up:

  • use cadences
  • first note will always be the tonic (I or i) and at least a quarter note, perhaps a half
  • 4th measure will usually end on a imperfect cadence, though occasionally a perfect or deceptive cadence
  • last measure will end on a perfect or plagal cadence (did think about including a deceptive cadence, but…)

First note of each measure will be a down beat, either longer note or louder note. In 4/4 time, 3rd beat of each measure will be a softer down beat (not sure how to make that work). Will worry about other time signatures down the road.

I am thinking a maximum of 1 jump per 4 bars, but that may vary depending on the number of notes per measure (i.e. maybe more jumps when using a large number of shorter notes). Otherwise mostly runs of steps, up or down, perhaps when switching direction add a jump on some random frequency. I am also thinking that during runs the code will favour direction of previous step’s direction, but occasionally allow reversing course.

I refer you to the Resources section for a couple links discussing composing an 8 bar melody.

Some Design Thoughts

I have in many cases been passing a lot of parameters to functions. That got so crazy I started passing an argument dictionary to the most recent functions. But that doesn’t do a particularly good job of documenting what should be in that dictionary. So this time I am going to use a class for that purpose.

As I started coding my first test function, I realized I’d likely be tracking a great deal of information as I went a long. So, I have decided to forget my current function approach and use a Melody class during this test development process. That first configuration class will be amalgamated into the new Melody class. Going to be a bit of work, I have not really used a lot of classes in my coding to date.

A Melody Class

Okay, let’s get started on that Melody class. You know, I am thinking for now that I will not include that configuration class in the Melody class. We will see if that makes sense and works in the long run.

All this initial development code will likely be refactored as I move along trying to get a melody generated.

Configuration Class

Here’s the current state of my melody configuration class and my development module. Not sure configuration is the best label or description, but… There is in fact the start of a function in the module, create_melody(); but, with my change of plans I will not be showing that in the post.

# tst_melody.py: want to test approaches to generating melodies to go with a given chord progression
#  or musical key
# Expect it will take a bit of work and messing around, hopefully worthwhile.
# Going to include in git as want to save work as I go along.

from dataclasses import dataclass
from typing import Any, Union
import numpy as np
import numpy.typing as npt

from pkgs import em_bars as emb
from pkgs import em_chords as emc
from pkgs import em_notes as emn

""" Current Classes
  MelodyConfig
  Melody
"""

""" Current Functions
"""

""" Globals
  rng (np random gnerator)
"""

rng = np.random.default_rng()

""" Passing a large bunch of parameters is driving me nuts, so going to use a dataclass to group
the needed/desired parameters required by the create_melody function
"""
@dataclass
class MelodyConfig:
  pd_len:int = 8                  # number bars in melody period
  r_oct:int = 4                   # root octave (piano equivalent)
  t_sig:tuple[int, int] = (4, 4)  # time signature
  s_bt:float = 0.5                # seconds per beat
  dbg:bool = False                # print debug stmts to terminal

Back to Melody Class

Okay here’s my intitial class code. Not much and a bit of guess work. As mentioned, I don’t have much experience working with classes in Python. So not really sure about the documentation and such.

When I started working on this, I realized that it might be easier if I picked notes based on duration rather than on symbol. That would allow me to more easily make some likely needed calculations. E.G. if I want to play four notes over one beat, I know that each note will have a duration of ¼ the time of a single beat. I also then know, that the note will be ¼ the length of the standard note for the time signature. E.G. with a 3/4 signature those notes would need to be ¼ of a quarter note. That is, a sixteen note each. Not sure this will make a difference but I am going to give it a try and see how things go.

class Melody():
  """ Generates a musical melody. The class arguments define some basic
      melody configuration attributes. A class method is then used to
      create the melody using those initial attributes and a number of
      subsequently generated attributes.

    Arguments:
      :param r_nt: the root node for the melody
      :parm s_qual: the chord quality for the scale the melody should be based on
      :param m_cfg: a number of additional configuration parameters for the melody

    Methods:
      rn2nts(self) -> tuple[dict, dict]:
        Generate 2 dictionaries relating roman numerals to scale notes and vice versa.
  """
  def __init__(self, r_nt:emn.Notes_scale, s_qual:emc.ChordFormula, m_cfg:MelodyConfig):
    self.m_rnt = r_nt           # tonic note
    self.m_squ = s_qual         # chord quality
    self.m_nbr = m_cfg.pd_len   # number bars in melody period
    self.m_oct = m_cfg.r_oct    # starting melody octave (piano equiv)
    self.m_sig = m_cfg.t_sig    # melody time signature
    self.m_spb = m_cfg.s_bt     # seconds per beat
    self.p_dbg = m_cfg.dbg      # print debug statements?
    # some of the following may end up being moved to class methods
    # stuff I believe will be needed when generating the melody
    # total seconds for the period
    self.sec_p = self.m_sig[0] * self.m_spb * self.m_nbr
    # second per bar/measure
    self.sec_b = self.m_sig[0] * self.m_spb
    # scale notes
    self.s_nts = emn.get_scale_4_note(self.m_rnt, s_mode=self.m_squ, r_oct=self.m_oct)
    # duration in seconds for all notes
    self.n_durs = emb.Note_durations(self.m_sig, self.m_spb)
    # control and data vars
    self.mldy = [[]] * self.m_nbr # to store the melody data
    self.m_dur = [0] * self.m_nbr # current total note duration for each measure
    self.c_bar = 0                # current bar/measure
    self.last_what = "nada"       # last melodic action (steps, jump, repeat, etc)


  def __repr__(self):
    cls_nm = type(self).__name__
    return f"{cls_nm}(m_rnt={self.m_rnt!r}, m_squ={self.m_squ!r}, m_nbr={self.m_nbr!r}, " \
      f"m_oct={self.m_oct!r}, m_sig={self.m_sig!r}, m_spb={self.m_spb!r}, p_dbg={self.p_dbg!r})"


  # Expect this will be needed when generating the melody.
  def rn2nts(self) -> tuple[dict, dict]:
    """ Generate 2 dictionaries relating roman numerals to scale notes and vice versa.

      :param snts: list of the notes in the scale
      
      :return: two dictionaries: nt2rn: scale notes are keys, roman numerals are values
                  rn2nt: reverse of nt2rn
    """
    nt2rn, rn2nt = {}, {}
    for i, rn in enumerate(["i", "ii", "iii", "iv", "v", "vi", "vii"]):
      nt2rn[self.s_nts[i]] = rn
      rn2nt[rn] = self.s_nts[i]

    return nt2rn, rn2nt

And a quick test of the above.

... ...
if __name__ == '__main__':
  mldy_cfg = MelodyConfig(dbg=True)
  print(f"\n{mldy_cfg}")
  r_nt, s_qual = "A", "min_nat"
  mldy = Melody(r_nt, s_qual, mldy_cfg)
  t_n2r, t_r2n = mldy.rn2nts()
  print(f"\n\t{t_n2r}\n\t{t_r2n}")
  print(f"\n{mldy}")

And in the terminal, I got the following.

PS R:\learn\e_m_311> uv run tst_melody.py

MelodyConfig(pd_len=8, r_oct=4, t_sig=(4, 4), s_bt=0.5, dbg=True)

        {'A4': 'i', 'B4': 'ii', 'C5': 'iii', 'D5': 'iv', 'E5': 'v', 'F5': 'vi', 'G5': 'vii'}
        {'i': 'A4', 'ii': 'B4', 'iii': 'C5', 'iv': 'D5', 'v': 'E5', 'vi': 'F5', 'vii': 'G5'}

Melody(m_rnt='A', m_squ='min_nat', m_nbr=8, m_oct=4, m_sig=(4, 4), m_spb=0.5, p_dbg=True)

(For some reason, I feel strangely good for having written that bit of code which, in fact, does nothing.)

A Create Melody Method

Let’s look at starting work on a method that will generate a melody based on the configuration members of the Melody class. There will be, I am sure, additional helper methods; but, we will get to those as necessary. I expect coding the makeMelody method will slowly determine what those additional methods will be and/or need to do. Did think about calling it createMelody but, to me, the former name just seems a better choice.

The first thing the method will do is generate those random number to note dictionaries. Then it will add the tonic to the first measure. After which, it will establish some ground rules for the melody (I refer you to the discussion above or the resource links below).

For now I am not going to set a volume for each note. Will deal with that down the road.

... ...
  def getNote(self, nbr_bts:int) -> str:
    # likely incomplete but for now
    # base_notes = {2: "hlf", 4: "qtr", 8: "8th"}
    # c_base = base_notes[self.m_sig[1]]
    return self.n_durs.dur2nt[nbr_bts * self.m_spb]
... ...
  def makeMelody(self):
    """ Using class configuration members create a random melody to go
      with the selected musical scale (tonic + chord quality). Class
      members specify the melody length (in bars), time signature,
      available notes, etc.

      As the notes and durations are determined, they will be added to
      the class mldy and m_dur members.
    """
    # get dicts to allow switching between scale notes and their
    # roman numeral equivalents
    t_n2r, t_r2n = self.rn2nts()

    # select starting octave, value specified in config or one higher
    s_oct = rng.choice([self.m_oct, self.m_oct+1])

    # the first note of the melody will always be the tonic,
    # and for now the note will be either a quarter or dotted
    # quarter. Once I look at adding time signatures other
    # than 3/4 or 4/4 the following will likely go in a method
    if self.m_sig[0] > 3:
      s_val = rng.choice([1, 1.5, 2.0])
    else:
      s_val = rng.choice([1, 1.5])
    t_nt = self.getNote(s_val)
    # if self.p_dbg:
    #   print(f"\nnbr beats: {s_val} -> note: {t_nt}")
    self.mldy[self.c_bar].append((f"{r_nt}{s_oct}", t_nt, self.n_durs.n_dur[t_nt]))
    self.m_dur[self.c_bar] = self.m_dur[self.c_bar] + self.n_durs.n_dur[t_nt]
    self.last_what = f'first tonic {"low" if s_oct == self.m_oct else "high"}'
    if self.p_dbg:
      print(f"\nbar {self.c_bar} -> {self.last_what}: {self.mldy[self.c_bar]}\n\ttotal: {self.m_dur[self.c_bar]} of {self.sec_b}")

A wee test. I know totally unnecessary, but I think this process is going to be full of little steps.

if __name__ == '__main__':
  mldy_cfg = MelodyConfig(dbg=True)
  print(f"\n{mldy_cfg}")
  r_nt, s_qual = "A", "min_nat"
  mldy = Melody(r_nt, s_qual, mldy_cfg)
  print(f"\n{mldy}")
  mldy.makeMelody()

And in the terminal the following was displayed.

PS R:\learn\e_m_311> uv run tst_melody.py

MelodyConfig(pd_len=8, r_oct=4, t_sig=(4, 4), s_bt=0.5, dbg=True)

Melody(m_rnt='A', m_squ='min_nat', m_nbr=8, m_oct=4, m_sig=(4, 4), m_spb=0.5, p_dbg=True)

bar 0 -> first tonic high: [('A5', 'qtr●', 0.75)]
        total: 0.75 of 2.0

Add More Framework to the Melody

Based on our design concept, we know that we usually want the question phrase of the melody to end on the dominant or similar. The first note of the answer phrase also has some typical values. Some of which might related to that last note of the question. And finally we know we wish to end with the tonic. But there are some variations to consider. So I think I am going to write a method, sortInitFW, that will sort and return those decisions.

I also think I am going to alter the data structure for the melody. It will be a list of measures instead of a list of notes. That way I can record these decisions as soon as they are made. Rather than holding on to them until I get to working on the appropriate measure. Though I will likely have to provide for changing them if they don’t fit, as written, into the melody at that point. I.E. depending on where the melody goes I may need to end on a tonic in a different octave. But that will hopefully be easy to correct.

Note: have made the changes regarding the new list of lists data structure for the melody to the code blocks above.

And, with this change in data structure, I think I will just have this new method write the decisions to the appropriate measure. Will likely make the method much more complex. So, perhaps a few more helper methods.

When I started to work on the sortInitFW method I felt so confused and lost that I walked away and took a lengthy break. (But a pot of chili stew is now simmering on the stove. And, not sure I am quite ready to get to the code just yet.)

Helper Method: getNote()

A method to return the approriate note duration symbol given the number of beats the note should last (can be a fraction).

  def getNote(self, nbr_bts:int) -> emb.Note_durations:
    """ Return the correct note value given the number of beats it is
      meant to last. The returned value accounts for the specified
      time signature for the current instantiation.

      :param nbr_bts: the number of beats the note is to last

      :returns: note duration symbol (e.g. hlf, qtr, qtr●, 8th, etc.)
    """
    # likely incomplete but for now
    return self.n_durs.dur2nt[nbr_bts * self.m_spb]

Initial Version of sortInitFW()

Okay, I started by copying some earlier test/development code into the new method.

  def sortInitFW(self):
    """ Sort out what will happen at end of each phrase and at the start of the second.
    """
    # set cadence for end of melody
    f_cad =  rng.choice(["perfect", "plagal"], p=[.7, .3,])
    # set cadence for end of antecedent/question phrase
    q_cad = rng.choice(["imperfect", "?-vii", "iv-v"])
    # set first not of consequent/answer phrase
    nts_ans = ["i", "ii", "iii", "vi"]
    ans_1st = rng.choice(nts_ans, p=[.4, .2, .2, .2])

But, because I want to write the selected cadences/notes to the melody data structure, I also need to sort out the durations for all the needed notes. That, I believe, will make the method a fair bit messy. So, I am now thinking a helper method for each of those three framework cases above.

I also decided I didn’t like the mixing of cadence names and roman numeral versions. Will likely go the romman numeral route for all of them.

Short and Sweet?

So, I think this post is done. Will start next post trying to sort out those helper methods and any others that might be required to get it all to work in some reasonable fashion. I am referring to generating a random melody; not necessarily a good sounding one.

Until next time, may your projects progress much more smoothly than this one is for me.

Resources