I rather abruptly ended the last post after giving a glimpse at what I wanted the Melody class method sortInitFW to do (accomplish?). But, looking at that bit of code, I felt that it would not be appropriate to sort all the initial framework bits and pieces entirely in that method. So, I decided I should write helper methods to deal with each of the decisions needed to initialize the basic melody framework. Here we go!
I am currently looking at three helper methods: ending the question phrase, starting the answer phrase, and finishing the melody. For now I expect those will be called from the sortInitFW method rather than individually in my code. Though I am wondering if they should be part of the Melody class intialization? No idea what best practice would be. So let’s stick with the original plan.
Though can’t help thinking that, when I am setting up the melody data structure, adding the known framework elements at that time would just make sense.
Refactor makeMelody(), Add First Note to Melody
Given we are going to write a method to build the intial melody framework, it makes sense to me that we should move the setting of the initial note from makeMelody to sortInitFW. After all that is basically the first step in sorting the basic melody framework. I was just going to stick the code in the framework method, sortInitFW(), but think I will add a helper function—just in case things get more complicated down the road.
def fwStart(self) -> None:
""" Sort first note for melody framework.
:returns: nothing
:side-effect: the method modifies the mldy member
"""
# 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
# select starting octave, value specified in config or one lower or higher
# not giving the all a equal likelihood of selection
p_oct = [.05, .75, .2]
s_oct = rng.choice([self.m_oct-1, self.m_oct, self.m_oct+1], p=p_oct)
o_lvl = {self.m_oct-1: "low", self.m_oct: "mid", self.m_oct+1: "high"}
# set current bar, don't use or modify class c_bar member
c_bar = 0
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)
self.mldy[c_bar].append((f"{r_nt}{s_oct}", t_nt, self.n_durs.n_dur[t_nt]))
self.m_dur[c_bar] = self.m_dur[c_bar] + self.n_durs.n_dur[t_nt]
self.last_what = f'first tonic {o_lvl[s_oct]}'
if self.p_dbg:
print(f"\nfwStart() bar {c_bar + 1} -> {self.last_what}: {self.mldy[c_bar]}\n\ttotal: {self.m_dur[c_bar]} of {self.sec_b}")
I will test it once I get the next helper method written.
Finish the Melody
This one strikes me as being one of the most obvious decisions. Pretty much everything I have read suggests it should be finished on the tonic note. And more often than not, led there by the dominant. But, I also found another option that seems to work almost as well. Let’s see if we can sort this out. In small steps.
Make A Choice
I will start by deciding what the finish should look like. E.G. what notes, in which order, should be used. I currently intend to give all options an equal probability of being chosen. That may in time change. So going to include some variables to allow for that possibility. Pretty straightforward, so here it is.
def fwFinish(self, t_r2n:dict) -> None:
""" Select and encode the finish for the melody. Only a few
possible cadences are considered viable. They are all
assigned a likelihood for selection.
:param t_r2n: dictionary of roman numerals to notes for melody scale
:returns: nothing
:side-effect: the method modifies the mldy member
"""
# set bar to be modified
c_bar = 7
# set cadence for end of melody
cdncs = ["v-i", "iv-1"]
c_prb = [.5, .5]
f_cad = rng.choice(cdncs, p=c_prb)
f_nts = [t_r2n[rn] for rn in f_cad.split("-")]
if self.p_dbg:
print(f"fwFinish bar {c_bar + 1} -> {f_cad}, {f_nts}")
Wasn’t going to, but a wee test.
... ...
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()
self.sortInitFW(t_r2n)
... ...
def sortInitFW(self, t_r2n:dict) -> None:
""" Sort out what will happen at the beginning and end of each phrase.
:param t_r2n: dictionary of roman numerals to notes for melody scale
:returns: nothing
:side-effect: modifies class mldy member
"""
# set first note for melody (framework)
self.fwStart()
# set cadence for end of melody
self.fwFinish(t_r2n)
... ...
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()
The following was displayed in the terminal.
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)
fwStart() bar 1 -> first tonic mid: [('A4', 'hlf', 1.0)]
total: 1.0 of 2.0
fwFinish bar 8 -> v-i, ['E5', 'A4']
Sort Note Durations
A wee bit of progress. Now perhaps the hard part—sorting note durations. Again, my reading, seems to indicate that the finishing notes should have relatively long durations. What exactly that might be I don’t know. But I am going to go with a recently formulated concept. That is, the finishing notes should be longer than the base beat for the time signature. E.G. in \({}^{3}_{4}\) or \({}^{4}_{4}\) they should be longer than a quarter note. Which means they may extend over two measures in some cases. Good or bad idea? Me no know.
Having some issues sorting the octaves for the two final notes. Will work on that down the road. Also, decided to add an emptyish tuple to last measure if the final two notes did not fill the measure.
def fwFinish(self, t_r2n:dict) -> None:
""" Select and encode the finish for the melody. Only a few
possible cadences are considered viable. They are all
assigned a likelihood for selection.
:param t_r2n: dictionary of roman numerals to notes for melody scale
:returns: nothing
:side-effect: the method modifies the mldy member
"""
# set bar to be modified
c_bar = 7
# set cadence for end of melody
cdncs = ["v-i", "iv-i"]
c_prb = [.5, .5]
f_cad = rng.choice(cdncs, p=c_prb)
f_nts = [t_r2n[rn] for rn in f_cad.split("-")]
# sort octave for finish
#! not working at present, need to sort out, perhaps helper method
if f_nts[0][-1] >= f_nts[1][-1]:
f_nts[1] = f"{f_nts[1][:-1]}{f_nts[0][-1]}"
# sort note durations
if self.m_sig[0] == 2:
b1, b2 = 2, 2
elif self.m_sig[0] == 3:
b1, b2 = 1.5, 1.5
else:
b1 = rng.choice([1.5, 2])
b2 = b1
n1, n2 = self.getNote(b1), self.getNote(b2)
# print(f"\t{f_cad} -> {f_nts}")
if self.m_sig[0] == 2:
self.mldy[c_bar - 1].append((f"{f_nts[0]}", n1, self.n_durs.n_dur[n1]))
self.m_dur[c_bar - 1] = self.m_dur[c_bar - 1] + self.n_durs.n_dur[n1]
self.mldy[c_bar].append((f"{f_nts[1]}", n2, self.n_durs.n_dur[n2]))
self.m_dur[c_bar] = self.m_dur[c_bar] + self.n_durs.n_dur[n2]
if self.p_dbg:
print(f"fwFinish ({f_cad}) -> bars {c_bar}, {c_bar + 1}: {self.mldy[c_bar - 1:]}")
print(f"\ttotal: {self.m_dur[c_bar - 1]} of {self.sec_b} and {self.m_dur[c_bar]} of {self.sec_b}")
else:
# if necessary, indicate incomplete bar
if (b1 + b2) < self.m_sig[0]:
self.mldy[c_bar].append((f"?", "?", "?"))
self.mldy[c_bar].append((f"{f_nts[0]}", n1, self.n_durs.n_dur[n1]))
self.m_dur[c_bar] = self.m_dur[c_bar - 1] + self.n_durs.n_dur[n1]
self.mldy[c_bar].append((f"{f_nts[1]}", n2, self.n_durs.n_dur[n2]))
self.m_dur[c_bar] = self.m_dur[c_bar] + self.n_durs.n_dur[n2]
if self.p_dbg:
print(f"fwFinish ({f_cad}) -> bar {c_bar + 1}: {self.mldy[c_bar]}\n\ttotal: {self.m_dur[c_bar]} of {self.sec_b}")
Much longer than I anticipated. May need another helper function or two. And another quick test.
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)
fwStart() bar 1 -> first tonic high: [('A5', 'qtr●', 0.75)]
total: 0.75 of 2.0
fwFinish (iv-i) -> bar 8: [('?', '?', '?'), ('D5', 'qtr●', 0.75), ('A5', 'qtr●', 0.75)]
total: 1.5 of 2.0
Finishing Notes for Question Phrase
This one is I think a bit more convoluted than the finishing notes for the melody. But, not sure it will take significantly more code. Once again I have some cadences that are routinely used to finish this phrase. Though one of them is a little less developed than the others. As for note durations, I expect the last one should be of a good duration. Those preceding it are perhaps of less concern.
That said, let’s get started.
Get Note Durations
This code was getting repeated in at least a couple methods so far. So, going to create a helper method, getCdncDurs(). Basically just a copy of the code from the fwFinish() method.
def getCdncDurs(self) -> list[str, str]:
""" Determine the note durations for a 2 note cadence based on the
melody time signature.
:param: none
:returns: list of note durations for each of the two notes
"""
if self.m_sig[0] == 2:
b1, b2 = 2, 2
elif self.m_sig[0] == 3:
b1, b2 = 1.5, 1.5
else:
b1 = rng.choice([1.5, 2])
b2 = b1
n1, n2 = self.getNote(b1), self.getNote(b2)
return [n1, n2]
I refactored fwFinish() to use the new helper method. But, I also had to refactor the code that checks for an incomplete measure. I will leave that to you to sort out.
Back to the fwQFinish Method
And, the start of the new method.
def fwQFinish(self, t_r2n:dict) -> None:
""" Select and encode the finish for the antecedent/question phrase.
Only a few possible cadences are considered viable. Most are
considered an 'imperfect cadence'. They are all assigned am
equal likelihood for selection.
:param t_r2n: dictionary of roman numerals to notes for melody scale
:returns: nothing
:side-effect: the method modifies the mldy member
"""
# in case anything needs changing
c_bar = 3
q_cads = ["i-v", "ii-v", "iv-v", "ii-vii"]
p_cads = [.25, .25, .25,.25]
q_cad = rng.choice(q_cads, p=p_cads)
q_nts = [t_r2n[rn] for rn in q_cad.split("-")]
# sort note durations
q_durs = self.getCdncDurs()
q_n_d = [[a, b] for a, b in zip(q_nts, q_durs)]
print(f"fwQFinish() -> {q_cad}: {q_n_d}")
And a quick test.
.... ...
def sortInitFW(self, t_r2n:dict) -> None:
""" Sort out what will happen at the beginning andn end of each phrase.
:param t_r2n: dictionary of roman numerals to notes for melody scale
:returns: nothing
:side-effect: modifies class mldy member
"""
# set first note for melody (framework)
self.fwStart()
# set cadence for end of melody
self.fwFinish(t_r2n)
# set cadence for end of antecedent/question phrase
self.fwQFinish(t_r2n)
# set first note of consequent/answer phrase
nts_ans = ["i", "ii", "iii", "vi"]
ans_1st = rng.choice(nts_ans, p=[.4, .2, .2, .2])
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)
fwStart() bar 1 -> first tonic high: [('A5', 'hlf', 1.0)]
total: 1.0 of 2.0
fwFinish (iv-i) -> bar 8: [('?', '?', '?'), ('D5', 'qtr●', 0.75), ('A5', 'qtr●', 0.75)]
total: 1.5 of 2.0
fwQFinish() -> i-v: [['A4', 'qtr●'], ['E5', 'qtr●']]
For now, I am not going to worry about the octave in which each note is currently slotted.
So, let’s just write this to the melody data structure.
And Another Helper Method
Looks like I am going to once again repeat a bunch of code. So, let’s put that in its own method. I am not sure how shareable it actually is, but we will find out. The bit I am looking at is the code that writes the sorted notes to the appropriate measures in the melody data structure.
Debug print statements over half the code?!
def wrtCdncNts(self, c_nts:dict[str: list[tuple[str, str, float]]]) -> None:
""" Write the passed note data to the melody data structure.
:param c_nts: dictionary of note data, key is index of measure in
melody list, value is a list of notes for that measure
:returns: nothing
:side-effect: modifies class mldy member
"""
for c_br, notes in c_nts.items():
# if necessary, indicate incomplete bar
if (notes[0][2] + notes[1][2]) < self.sec_b:
self.mldy[c_br].append((f"?", "?", "?"))
for c_nt in notes:
self.mldy[c_br].append((f"{c_nt[0]}", c_nt[1], c_nt[2]))
self.m_dur[c_br] = self.m_dur[c_br] + c_nt[2]
if self.p_dbg:
if len(c_nts.keys()) == 2:
c_brs = list(c_nts.keys())
print(f"\t-> bars {c_brs[0]}, {c_brs[1]}: {self.mldy[c_brs[0]:c_brs[1] + 1]}")
print(f"\ttotal: {self.m_dur[c_brs[0]]} of {self.sec_b} and {self.m_dur[c_brs[1]]} of {self.sec_b}")
else:
c_br = list(c_nts.keys())[0]
print(f"\t-> bar {c_br}: {self.mldy[c_br]}")
print(f"\ttotal: {self.m_dur[c_br]} of {self.sec_b}")
I refactored fwFinish to use this new helper method. Will let you sort that out. But the following should help.
Back to fwQFinish()
I think we can now finish the current method of interest. Though there was some of it shown above, here it is in full.
def fwQFinish(self, t_r2n:dict) -> None:
""" Select and encode the finish for the antecedent/question phrase.
Only a few possible cadences are considered viable. Most are
considered an 'imperfect cadence'. They are all assigned am
equal likelihood for selection.
:param t_r2n: dictionary of roman numerals to notes for melody scale
:returns: nothing
:side-effect: the method modifies the mldy member
"""
# in case anything needs changing
c_bar = 3
q_cads = ["i-v", "ii-v", "iv-v", "ii-vii"]
p_cads = [.25, .25, .25,.25]
q_cad = rng.choice(q_cads, p=p_cads)
q_nts = [t_r2n[rn] for rn in q_cad.split("-")]
# sort note durations
q_durs = self.getCdncDurs()
q_n_d = [[nt, dr, self.n_durs.n_dur[dr]] for nt, dr in zip(q_nts, q_durs)]
if self.p_dbg:
print(f"fwQFinish() -> {q_cad}: {q_n_d}")
nts_bar = {}
if self.m_sig[0] == 2:
nts_bar[c_bar - 1] = [q_n_d[0]]
nts_bar[c_bar] = [q_n_d[1]]
else:
nts_bar[c_bar] = q_n_d
# write this cadence to the melody data structure
self.wrtCdncNts(nts_bar)
A wee test, of new helper function, completed fwQFinish() and refactored fwFinish(). I added code to print the full melody structure to the module test code block.
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)
fwStart() bar 1 -> first tonic high: [('A5', 'qtr●', 0.75)]
total: 0.75 of 2.0
fwFinish() -> v-i: [['E5', 'qtr●', 0.75], ['A5', 'qtr●', 0.75]]
-> bar 7: [('?', '?', '?'), ('E5', 'qtr●', 0.75), ('A5', 'qtr●', 0.75)]
total: 1.5 of 2.0
fwQFinish() -> ii-v: [['B4', 'qtr●', 0.75], ['E5', 'qtr●', 0.75]]
-> bar 3: [('?', '?', '?'), ('B4', 'qtr●', 0.75), ('E5', 'qtr●', 0.75)]
total: 1.5 of 2.0
mldy: [[('A5', 'qtr●', 0.75)], [], [], [('?', '?', '?'), ('B4', 'qtr●', 0.75), ('E5', 'qtr●', 0.75)], [], [], [], [('?', '?', '?'), ('E5', 'qtr●', 0.75), ('A5', 'qtr●', 0.75)]]
And things appear to be moving along in the right direction; if not exactly as nicely as I would like.
Sort the First Note of the Consequent/Answer Phrase
I didn’t know if I should be looking at a specific cadence. But we have, in our initial framework, only added one note at the start of the Antecedent/Question phrase. So I am only going to do so here as well. Though we do have a few more choices (I think).
Believe this one should be pretty straightforward given the work and methods so far.
def fwAStart(self, t_r2n:dict) -> None:
""" Sort first note for the consequent/answer phrase of the melody
framework.
:param t_r2n: dictionary of roman numerals to notes for melody scale
:returns: nothing
:side-effect: the method modifies the mldy member
"""
c_bar = 4
# octave should be similar to that of last note in previous bar
p_oct = int(self.mldy[3][-1][0][-1])
# set first note of consequent/answer phrase
nts_ans = ["i", "ii", "iii", "vi"]
ans_1st = rng.choice(nts_ans, p=[.3, .2, .2, .3])
a_nt = t_r2n[ans_1st]
# not sure about this, but its a start and a reminder
if ans_1st in ["i", "ii", "iii"]:
a_nt = f"{a_nt[:-1]}{p_oct}"
q_durs = self.getCdncDurs()
nts_bar = (a_nt, q_durs[0], self.n_durs.n_dur[q_durs[0]])
self.mldy[c_bar].append(nts_bar)
self.m_dur[c_bar] = self.m_dur[c_bar] + self.n_durs.n_dur[q_durs[0]]
if self.p_dbg:
print(f"fwAStart() -> {ans_1st}: {a_nt}")
print(f"\t-> bar {c_bar}: {self.mldy[c_bar]}")
print(f"\ttotal: {self.m_dur[c_bar]} of {self.sec_b}")
A quick test.
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)
fwStart() bar 1 -> first tonic high: [('A5', 'qtr●', 0.75)]
total: 0.75 of 2.0
fwFinish() -> iv-i: [['D5', 'hlf', 1.0], ['A5', 'hlf', 1.0]]
-> bar 7: [('D5', 'hlf', 1.0), ('A5', 'hlf', 1.0)]
total: 2.0 of 2.0
fwQFinish() -> ii-vii: [['B4', 'hlf', 1.0], ['G5', 'hlf', 1.0]]
-> bar 3: [('B4', 'hlf', 1.0), ('G5', 'hlf', 1.0)]
total: 2.0 of 2.0
fwAStart() -> i: A5
-> bar 4: [('A5', 'qtr●', 0.75)]
total: 0.75 of 2.0
mldy: [[('A5', 'qtr●', 0.75)], [], [], [('B4', 'hlf', 1.0), ('G5', 'hlf', 1.0)], [('A5', 'qtr●', 0.75)], [], [], [('D5', 'hlf', 1.0), ('A5', 'hlf', 1.0)]]
Appears we are still, more or less, heading in the right direction.
That’s It For This One
You know, got a fair bit done. Don’t know about best practices, but it currently works for me. And we covered setting up the basic melody framework in pretty good detail.
Still have some issues with the octaves assigned to various notes, but I am sure that will get sorted as we continue trying to build a melody. Expect I may need a new method or two to sort that particular issue.
Next time I will start with completing the first measure, if necessary. And continue on to sorting the remaining empty measures or partial measures.
Until then, do enjoy your time with an instrument or a code editor.