Time to flesh out that melody framework we built in the last post. I will start by carrying on from that starting note we added to the first measure. When I run into measures that already contain notes added while building the melody framework I will endeavour to get things to flow together in a reasonably nice (melodic?) fashion. That may involve modifying the octave assigned to those existing notes.
I plan on writing a method that chooses some next movement: generally a jump (infrequently), a series of steps (more frequently) or a repetition of some previous measure (infrequently) or of a previous measures note pattern or rhythm (less frequently or infrequently?). For the first two options, I will need to determine the direction of movement. Also, for the jump the number of intervals and for the series of steps its length. Additionally for the step situation, I will need to decide on some potential patterns. E.G. single interval steps, the odd two interval step, all two interval steps, zigzag series, back and forth repetition of steps. The note durations will need to be sorted out as well. Those may be dependent on the other parameters for the particular movement. And, if repeating a pattern or rhythm the new notes and/or durations will need to be determined.
Just writing the above has given me a headache.
After a bit of a break, I think I am going to write a function to sort the big item to produce next. Then have one or more methods for each of those to sort out sizes, directions, frequencies, variations, etc. So, let’s draft that first one.
What Next?
After starting on this method, I decided I would not have it return that next activity or add/alter a class member. It would instead be a dispatcher. Once it decided on what to do next, it would call an appropriate method. For now this method will not take any arguments.
I am sure there will be plenty of refactoring as we progress towards generating a melody. But for now…
def getNextPattern(self) -> None:
""" Determine the next big step/task/musical pattern to take in
generating the melody.
! this method may be significantly refactored as development
! continues, for now keeping it horribly simple
:side-effect: calls another function, never does anything else
"""
# repeat not a particularly viable option if only have the
# first note in measure 1 (aka 0), and don't really likely
# want a jump right off the bat, but...
if self.last_what == 'first tonic':
m_options = ["jump", "step"]
p_options = [0.05, 0.95]
else:
m_options = ["jump", "repeat", "step"]
p_options = [0.2, 0.1, 0.7]
# return rng.choice(m_options, p=p_options)
wht_do = rng.choice(m_options, p=p_options)
if self.p_dbg:
print(f"\ngetNextPattern(): wht_do = {wht_do}")
p_rts = {
"jump": self.wrtMldyJump,
"repeat": self.wrtMldyRpt,
"step": self.wrtMldyStep
}
p_rts[wht_do]()
... ...
def makeMelody(self) -> None:
... ...
# get dicts to allow switching between scale notes and their
# roman numeral equivalents
t_n2r, t_r2n = self.rn2nts()
# sort basic melody framework
self.sortInitFW(t_r2n)
# now framework in place, finish the melody
# start by finishing the first measure
self.last_what = f'first tonic'
self.getNextPattern()
... ...
def wrtMldyJump(self):
...
def wrtMldyRpt(self):
...
def wrtMldyStep(self):
...
A quick test yielded the following. Pretty much as expected.
I know too many simple tests. But I need to go slow and easy with this crazy project. So, any readers will just have to suffer along with me.
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 low: [('A3', 'hlf', 1.0)]
total: 1.0 of 2.0
fwFinish() -> v-i: [['E5', 'hlf', 1.0], ['A5', 'hlf', 1.0]]
-> bar 7: [('E5', 'hlf', 1.0), ('A5', 'hlf', 1.0)]
total: 2.0 of 2.0
fwQFinish() -> ii-vii: [['B4', 'qtr●', 0.75], ['G5', 'qtr●', 0.75]]
-> bar 3: [('?', '?', '?'), ('B4', 'qtr●', 0.75), ('G5', 'qtr●', 0.75)]
total: 1.5 of 2.0
fwAStart() -> vi: F5
-> bar 4: [('F5', 'qtr●', 0.75)]
total: 0.75 of 2.0
getNextPattern(): wht_do = step
mldy: [[('A3', 'hlf', 1.0)], [], [], [('?', '?', '?'), ('B4', 'qtr●', 0.75), ('G5', 'qtr●', 0.75)], [('F5', 'qtr●', 0.75)], [], [], [('E5', 'hlf', 1.0), ('A5', 'hlf', 1.0)]]
Building A Step Sequence of Notes
Listening to music it really amazes me the number of ways this step wise thing can be and is done. I would like to implement a goodly variety of options. But I will, as usual for this project, start slow and easy. So, let’s say a series of steps either up or down, of some length, with some step size and a suitable set of note durations. And as I write this, my brain has no idea where or how this will go/work.
I would really like to see different patterns for the steps. But ones that more or less follow the traditional rules.
I will start by getting it to work for the purpose of finishing the first measure, though it may continue into the next measure.
New Module Function
Looks like I will need to be able to determine if one note is lower than another. So a new function: nt_lessthan(). Expect I should likely have some sort of Note class, but perhaps another day.
... ...
def nt_lessthan(nt1:str, nt2:str) -> bool:
""" determine if note nt1 less than note nt2
:param nt1: note + octave (e.g. C4, E#5)
:param nt2: note + octave (e.g. C4, E#5)
:returns: True if nt1 < nt2, False otherwise
"""
t1 = (nt1[0] < nt2[0]) and (int(nt1[-1]) <= int(nt2[-1]))
t2 = (nt1[0] in ["A", "B", "C"]) and (int(nt1[-1]) <= int(nt2[-1]))
t3 = (int(nt1[-1]) < int(nt2[-1]))
return t1 or t2 or t3
Some testing seems to indicate it works as intended.
New Helper Function
Once I got to working on the step method, the first thing I tried to sort out was the general direction the sequence would move: up or down. I quickly realized it likely needed its own method. So, going to add the method getStepDir. Here’s some of the comments:
for the antecedent phrase favour up, except maybe when approaching notes in the 4th, down for consequent phrase, except perhaps when approaching melody’s finishing notes. But also favour the opposite direction of any preceding sequence of steps notes getting too high in pitch or too low. How to code those ideas?
See why a separate method might be needed.
def getStepDir(self) -> str:
""" Determine, based on some criteria the general direction in which
the sequence of steps should move.
:returns: string, up or down
"""
dir_ch = ""
# let's get the direction
# for the antecedent phrase favour up, except maybe when approaching
# notes in the 4th, down for consequent phrase, except perhaps when
# approaching melody's finishing notes. But also favour the opposite
# direction of any preceding sequence of steps notes getting too high
# in pitch or too low. How to code those ideas?
p_nt = self.mldy[self.c_bar][-1][0]
# previous note too high or too low
p_hgh, p_lw = nt_lessthan("F5", p_nt), nt_lessthan(p_nt, "F4")
if p_hgh or p_lw:
dir_ch = "down" if p_hgh else "up"
return dir_ch
# previous action was a lengthy step series or a jump
# if so, go the other way
# not sure about this
# previous note a little high or a little low
bit_hg, bit_lw = nt_lessthan("D5", p_nt), nt_lessthan(p_nt, "G4")
bs_prb, adj_p = .7, .1
ch_dir = ["up" if self.c_bar <= 3 else "down", "down" if self.c_bar > 3 else "up"]
if (bit_hg and ch_dir == "up") or (bit_lw and ch_dir == "down"):
bs_prb -= adj_p
sd_prb = [bs_prb, 1 - bs_prb]
s_dir = rng.choice(ch_dir, p=sd_prb)
return s_dir
Back to wrtMldyStep()
We have a general direction. Next I’d like to sort how many steps we will be taking. I currently expect a minimum of three will be required. At that size we could repeat the sequence if needed to fill out a measure or start the next one. I expect that this number is related to note durations and the time signature. I am going to have a maximum number of notes in the sequence equal to twice the number of beats in the signature. E.G. in 3/4 time I will allow a maximum of 6 notes; in 4/4 a maximum of 8. In both those cases using eighth notes would fill a measure. But, I don;t necessarily need to fill a measure every time. That said, I think for the first measure that will be the goal.
I am for now just going to include a block of code in the wrtMldyStep method.
def wrtMldyStep(self) -> None:
""" Generate stepwise sequence of notes and durations, add to the
mldy member in the appropriate measure.
:returns: nothing
:side-effect: the method modifies the mldy member
"""
# Going to need a few parameters: direction, length, starting step
# size, movement pattern (not sure about this one just yet)
# let's get the direction
p_nt = self.mldy[self.c_bar][-1][0] # debug purposes only so far
s_dir = self.getStepDir()
# sort number of steps in sequence, ignoring durations
avl_stp = list(range(3, (self.m_sig[0] * 2 + 1)))
s_nbr = rng.choice(avl_stp)
if self.p_dbg:
print(f"\n\twrtMldyStep(c_bar={self.c_bar}):\n\ts_dir: {s_dir}")
print(f"\t\tprev note: {p_nt}, step dir: {s_dir}, nbr steps: {s_nbr}")
Now the decision I’d like to sort is the first note of the step sequence. From what I have read, this can be one or two notes from the most recent note. In the appropriate direction of course.
Beginning to believe I need to rethink some of my non-design. If moving down I need to consider that I might be moving to the beginning or end of a new octave. My notes member, self.s_nts, doesn’t make that particularly easy given it is a list of strings. The roman numeral to note dictionaries don’t help much either—really don’t want to get into roman numeral arithmetic, especially the modulus.
So for now another helper function or two.
Get First Note of Step Sequence
Here’s the first helper method. It’s job is to determine the next note based on the number of steps, the direction and the previous note (an optional parameter, thinking ahead maybe). And I apologize for the style of arithmetic.
def getNtSteps(self, nbr_steps:int, s_dir:str, prv_nt:str=None) -> str:
""" Return the note that is nbr_steps away from previous note in the
mldy member for current measure, in the specified direction.
:param nbr_steps: how many note tones to move
:param s_dir: direction of the step, up or down
:param prv_nt: base note for move, if not specified use previous
note in melody
:returns: the note determined by the parameters
"""
nxt_nt = ""
# if necessary, get the pervious note
if prv_nt is None:
if self.m_dur[self.c_bar] < self.sec_b:
p_nt = self.mldy[self.c_bar][-1][0]
elif self.m_dur[self.c_bar] == 0:
p_nt = self.mldy[self.c_bar - 1][-1][0]
else:
print(f"\nTROUBLE getNtSteps({nbr_steps}, {s_dir}): can not determine previous note")
else:
p_nt = prv_nt
op_nxt = 1 if s_dir == "up" else -1
pv_ord = ord(p_nt[0])
nx_ord = pv_ord + (nbr_steps * op_nxt)
ord_A, ord_G = ord("A"), ord("G")
nx_oct = int(p_nt[-1])
if nx_ord < ord_A:
nx_ord = ord_G - (ord_A - nx_ord - 1)
nx_oct -= 1
elif nx_ord > ord_G:
nx_ord = ord_A + (nx_ord - ord_G - 1)
nx_oct += 1
nx_nt = chr(nx_ord)
if self.p_dbg:
print(f"\n\tgetNtSteps({nbr_steps}, {s_dir}) -> prev note: {p_nt} -> nxt note: {nx_nt}{nx_oct}")
return f"{nx_nt}{nx_oct}"
... ...
def wrtMldyStep(self) -> None:
... ...
# let's get the direction
p_nt = self.mldy[self.c_bar][-1][0] # debug purposes only so far
s_dir = self.getStepDir()
# sort number of steps in sequence, ignoring durations
avl_stp = list(range(3, (self.m_sig[0] * 2 + 1)))
s_nbr = rng.choice(avl_stp)
# determine first note in the series
# how many steps from previous note, I am assuming up to 2 tones, not semitones
s2nt = rng.choice([1, 2])
# s_dir = "down"
s1st = self.getNtSteps(s2nt, s_dir, prv_nt=None)
if self.p_dbg:
print(f"\n\twrtMldyStep(c_bar={self.c_bar}):\n\ts_dir: {s_dir}")
print(f"\t\tprev note: {p_nt}, step dir: {s_dir}, nbr steps: {s_nbr}, step to 1st note: {s2nt}, 1st note: {s1st}")
print(f"\t\t{self.s_nts}")
Bug
While working on this I saw an obvious error. Do you see it?
[‘F4’, ‘G4’, ‘G#4’, ‘A#4’, ‘C5’, ‘C#5’, ‘D#5’]
Quick fix. Error in commented out line.
def get_2_oct_piano(r_nt:Notes_scale, r_oct:int=4) -> list[Notes_scale]:
... ...
for i, nt in enumerate(h_scale):
# if nt == "C" and i > 0:
if nt == "A" and i > 0:
s_oct += 1
Long after I wrote the above in the draft, it became very clear that was not a mistake. Well, not in the code, just in my head. But for now it will stay in the post as previously drafted/written.
Back to the New Method
And, a quick test output the following to the terminal. Note, I changed the tonic note for this 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='F', 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: [('F4', 'hlf', 1.0)]
total: 1.0 of 2.0
fwFinish() -> iv-i: [['A#5', 'qtr●', 0.75], ['F5', 'qtr●', 0.75]]
-> bar 7: [('?', '?', '?'), ('A#5', 'qtr●', 0.75), ('F5', 'qtr●', 0.75)]
total: 1.5 of 2.0
fwQFinish() -> ii-v: [['G4', 'qtr●', 0.75], ['C5', 'qtr●', 0.75]]
-> bar 3: [('?', '?', '?'), ('G4', 'qtr●', 0.75), ('C5', 'qtr●', 0.75)]
total: 1.5 of 2.0
fwAStart() -> vi: C#5
-> bar 4: [('C#5', 'hlf', 1.0)]
total: 1.0 of 2.0
getNextPattern(): wht_do = step
getNtSteps(1, up) -> prev note: F4 -> nxt note: G4
wrtMldyStep(c_bar=0):
s_dir: up
prev note: F4, step dir: up, nbr steps: 4, step to 1st note: 1, 1st note: G4
['F4', 'G4', 'G#4', 'A#5', 'C5', 'C#5', 'D#5']
mldy: [[('F4', 'hlf', 1.0)], [], [], [('?', '?', '?'), ('G4', 'qtr●', 0.75), ('C5', 'qtr●', 0.75)], [('C#5', 'hlf', 1.0)], [], [], [('?', '?', '?'), ('A#5', 'qtr●', 0.75), ('F5', 'qtr●', 0.75)]]
One test, but seems to work. (I did try a few other tonics while developing the code above.) However, I am generating starting notes outside the given scale. Don’t know if that is a concern or not.
Let’s Get the Rest of the Notes for the Sequence
I think I should be able to use the same method used above to get the first note of the sequence. Let’s see.
And wouldn’t you know, already refactoring wrtMldyStep() while working on the new code.
def wrtMldyStep(self) -> None:
""" Generate stepwise sequence of notes and durations, add to the
mldy member in the appropriate measure.
:returns: nothing
:side-effect: the method modifies the mldy member
"""
# Going to need a few parameters: direction, length, starting step
# size, movement pattern (not sure about this one just yet)
ss_nts = [] # to store the notes in the sequence
# let's get the direction
p_nt = self.mldy[self.c_bar][-1][0] # debug purposes only so far
s_dir = self.getStepDir()
o_dir = "down" if s_dir == "up" else "up"
# sort number of steps in sequence, ignoring durations
avl_stp = list(range(3, (self.m_sig[0] * 2 + 1)))
s_nbr = rng.choice(avl_stp)
# determine first note in the series
# how many steps from previous note, I am assuming up to 2 tones, not semitones
s2nt = rng.choice([1, 2])
# s_dir = "down"
s1st = self.getNtSteps(s2nt, s_dir, prv_nt=None)
ss_nts.append(s1st)
# let's get the rest of the notes
# 1st determine the step size for each note in sequence, 1 or 2
s_szs = rng.choice([1, 2], s_nbr - 1, [.8, .2])
t_dir = rng.choice([s_dir, o_dir], s_nbr - 1, p=[.9, .1])
# don't want first one not going in the chosen direction
t_dir[0] = s_dir
for i in range(s_nbr - 1):
nx_nt = self.getNtSteps(s_szs[i], t_dir[i], prv_nt=ss_nts[-1])
ss_nts.append(nx_nt)
if self.p_dbg:
print(f"\n\twrtMldyStep(c_bar={self.c_bar}):\n\ts_dir: {s_dir}")
print(f"\t\tprev note: {p_nt}, step dir: {s_dir}, nbr steps: {s_nbr}, step to 1st note: {s2nt}, 1st note: {s1st}")
print(f"\t\tscale notes: {self.s_nts}")
print(f"\t\tstep sizes: {s_szs}, step dirs: {t_dir}")
print(f"\t\tstep notes: {ss_nts}")
And in the terminal for a couple test runs (only the relevant bits).
... ...
wrtMldyStep(c_bar=0):
s_dir: up
prev note: F4, step dir: up, nbr steps: 5, step to 1st note: 1, 1st note: G4
scale notes: ['F4', 'G4', 'G#4', 'A#5', 'C5', 'C#5', 'D#5']
step sizes: [1 1 1 2], step dirs: ['up' 'up' 'up' 'up']
step notes: ['G4', 'A5', 'B5', 'C5', 'E5']
... ...
wrtMldyStep(c_bar=0):
s_dir: up
prev note: F4, step dir: up, nbr steps: 5, step to 1st note: 1, 1st note: G4
scale notes: ['F4', 'G4', 'G#4', 'A#5', 'C5', 'C#5', 'D#5']
step sizes: [1 1 1 2], step dirs: ['up' 'up' 'down' 'up']
step notes: ['G4', 'A5', 'B5', 'A5', 'C5']
... ...
Let’s Figure Out the Durations for the Notes in the Sequence of Steps
I am just going to list the set up code for the method I hope will take care of this stage in the process of producing a melodic step of notes. It contains comments with my current thoughts on this process.
def getStepDurs(self, nbr_stp:int) -> list[float]:
""" Sort out a set of durations for the number of notes in a step
sequence.
:param nbr_stp: the number of steps in the sequence for which
durations are required
:returns: list of durations, in seconds, for each note
"""
# expect this is going to get crazy and may eventually require
# a number of helper functions, but for now 'start slow'
# if the number of steps is small, whatever that is, look at
# generated a more or less repeating sequence of durations
# if the number of steps is large, whatever that is, generate
# a set of durations that will fill out the current measure
# Otherwise??
...
Don’t really know how to tackle this. I will need to know the remaining beats or time in the bar we are currently working on. Obviously will need to know the number of notes. But from there, where?
Anyway for now I am going to begin coding two helper functions. In each one, I will start developing some variables and see if any of those can help me get to where I want to go. I, currently, am going to look at the number of 8th or 16th notes available to me. If the number of notes is less than or eqaul to half of one of those values I will look at generating a repeating series of notes. E.G. if the series has 4 notes and I have eight 8ths or 16ths or some combination of 8ths and 16ths, repetition it is. Otherwise we will look at filling the available time with the sequence of notes.
Though not sure how to sort the case of a combination of 8ths and 16ths.
After a day or two of coding, refactoring and refactoring and… I have something that sort of works. Hopefully enough for now and a step forward.
There are going to be a number of helper methods. Though down the road I will likely combine two step generation methods into one. Way too much repeated code between them. You know, I think I will do that before posting the code below. But first a method to sort out whether or not to attempt to repeat the step sequence in the current measure.
doStpRpt()
Not going to say much. Code is pretty clear I think.
def doStpRpt(self, nbr_nts:int) -> bool:
""" Do we do a repeated step sequence or a single step sequence.
:param nbr_nts: number of notes in the sequence
"""
avl_tm = self.sec_b - self.m_dur[self.c_bar]
n_8, n_16 = avl_tm / self.n_durs.n_dur["8th"], avl_tm / self.n_durs.n_dur["16th"]
if self.p_dbg:
print(f"\n\tdoStpRpt({nbr_nts})\n\t\tavl tm = {avl_tm}, n_8 = {n_8}, n_16 = {n_16}")
print(f"\t\tnbr 8ths per note = {n_8 / nbr_nts}, nbr 16ths per note = {n_16 / nbr_nts}")
return (n_8 / nbr_nts >= 2) or (n_16 / nbr_nts >= 2)
Back to Step Note Durations Method
Again not going to say much. Except that a lot of refactoring took place before the following code was completed.
def getStepDurs(self, nbr_stp:int) -> list[str]:
""" Sort out a set of durations for the number of notes in a step
sequence.
:param nbr_stp: the number of steps in the sequence for which
durations are required
:returns: list of durations, as notes, for each step
In the case of a repeated series twice the number of notes is
returned.
"""
# expect this is going to get crazy and may eventually require
# a number of helper functions, but for now 'start slow'
# if the number of steps is small, whatever that is, look at
# generated a more or less repeating sequence of durations
# if the number of steps is large, whatever that is, generate
# a set of durations that will fill out the current measure
# Otherwise??
s_durs = []
doSRpt = self.doStpRpt(nbr_stp)
if doSRpt:
# s_durs = self.getStpDursRpt(nbr_stp)
avl_tm = (self.sec_b - self.m_dur[self.c_bar]) / 2
else:
# s_durs = self.getStpDursSgl(nbr_stp)
avl_tm = (self.sec_b - self.m_dur[self.c_bar])
if self.p_dbg:
print(f"\n\tgetStpDurs({nbr_stp}): doSRpt = {doSRpt}")
n_try = list(self.n_durs.n_dur.keys())[2:]
# how many of each available note fit in the available time
n_avl = {}
for nt in n_try:
if "●" in nt:
continue
n_avl[nt] = avl_tm / self.n_durs.n_dur[nt]
n_use = {nt: 0 for nt in n_try}
n_use["t_need"] = 0
# find enough notes to fill the available time and match the
# number of required steps
n_chr, n_prv = "", ""
for nt, nbr in n_avl.items():
n_prv, n_chr = n_chr, nt
if int(nbr) >= nbr_stp:
break
if n_avl[n_chr] == nbr_stp:
n_use[n_chr] = nbr_stp
else:
n_use[n_chr] = (nbr_stp - n_avl[n_prv])
n_use[n_prv] = n_avl[n_prv] - n_use[n_chr]
n_use[n_chr] *= 2
# get total of all note durations for notes in n_use
ss_tot, t_durs = 0, []
for nt, nbr in n_use.items():
if nbr != 0:
# n_dbl = 2 if doSRpt else 1
# ss_tot += (self.n_durs.n_dur[nt] * (nbr * n_dbl))
# t_durs.extend([nt] * int(nbr * n_dbl))
ss_tot += (self.n_durs.n_dur[nt] * nbr)
t_durs.extend([nt] * int(nbr))
# t_durs.extend(t_durs)
s_durs = rng.permutation(t_durs).tolist()
if doSRpt:
s_durs.extend(s_durs)
if self.p_dbg:
print(f"\n\t\tavl nts: {n_avl}")
print(f'\t\tnts use: {n_use}')
print(f"\t\ttmp durs: {t_durs}")
print(f"\t\trtn durs: {s_durs}")
print(f"\t\tdur proc: {avl_tm}, tot seq dur: {ss_tot}")
return s_durs
Update wrtMldyStep()
Okay let’s put those durations to use. Will add code to wrtMldyStep() to get step durations, update the melody and action history. Modified things slightly, added a list to track every activity. Still have member to store most recently completed activity. (Won’t bother showing change in Melody.__init__.)
I have decided to save the size of any step series, jump, etc. in the list of sequences.
def wrtMldyStep(self) -> None:
... ...
# let's sort some durations for each note
# in general I am thinking shorter rather than longer, but the
# beats available will likely affect that decision
s_durs = self.getStepDurs(s_nbr)
# let's update the melody data structure
if len(s_durs) != len(ss_nts):
ss_nts.extend(ss_nts)
for i, nt in enumerate(ss_nts):
self.mldy[self.c_bar].append((nt, s_durs[i], self.n_durs.n_dur[s_durs[i]]))
self.m_dur[self.c_bar] += self.n_durs.n_dur[s_durs[i]]
# record process to date, update current bar if appropriate
self.last_what = f"step {s_dir} {s_nbr}"
self.prev_whats.append(self.last_what)
if self.m_dur[self.c_bar] == self.sec_b:
self.c_bar += 1
if self.p_dbg:
print(f"\t\tprev note: {p_nt}, step dir: {s_dir}, nbr steps: {s_nbr}, step to 1st note: {s2nt}, 1st note: {s1st}")
print(f"\t\tscale notes: {self.s_nts}")
print(f"\t\tstep sizes: {s_szs}, step dirs: {t_dir}")
print(f"\t\tstep notes: {ss_nts}")
print(f"\t\tstep durations: {s_durs}")
And in the terminal. Once again changed the key.
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='D', m_squ='major', m_nbr=8, m_oct=4, m_sig=(4, 4), m_spb=0.5, p_dbg=True)
fwStart() bar 1 -> first tonic mid: [('D4', 'hlf', 1.0)]
total: 1.0 of 2.0
fwFinish() -> v-i: [['A5', 'hlf', 1.0], ['D5', 'hlf', 1.0]]
-> bar 7: [('A5', 'hlf', 1.0), ('D5', 'hlf', 1.0)]
total: 2.0 of 2.0
fwQFinish() -> i-v: [['D4', 'qtr●', 0.75], ['A5', 'qtr●', 0.75]]
-> bar 3: [('?', '?', '?'), ('D4', 'qtr●', 0.75), ('A5', 'qtr●', 0.75)]
total: 1.5 of 2.0
fwAStart() -> vi: B5
-> bar 4: [('B5', 'hlf', 1.0)]
total: 1.0 of 2.0
getNextPattern(): wht_do = step
wrtMldyStep(c_bar=0):
s_dir: up
getNtSteps(1, up) -> prev note: D4 -> nxt note: E4
getNtSteps(1, up) -> prev note: E4 -> nxt note: F4
getNtSteps(2, up) -> prev note: F4 -> nxt note: A5
doStpRpt(3)
avl tm = 1.0, n_8 = 4.0, n_16 = 8.0
nbr 8ths per note = 1.3333333333333333, nbr 16ths per note = 2.6666666666666665
getStpDurs(3): doSRpt = True
avl nts: {'hlf': 0.5, 'qtr': 1.0, '8th': 2.0, '16th': 4.0, '32nd': 8.0}
nts use: {'hlf': 0, 'qtr●': 0, 'qtr': 0, '8th●': 0, '8th': 1.0, '16th●': 0, '16th': 2.0, '32nd': 0, 't_need': 0}
tmp durs: ['8th', '16th', '16th']
rtn durs: ['8th', '16th', '16th', '8th', '16th', '16th']
dur proc: 0.5, tot seq dur: 0.5
prev note: D4, step dir: up, nbr steps: 3, step to 1st note: 1, 1st note: E4
scale notes: ['D4', 'E4', 'F#4', 'G4', 'A5', 'B5', 'C#5']
step sizes: [1 2], step dirs: ['up' 'up']
step notes: ['E4', 'F4', 'A5', 'E4', 'F4', 'A5']
step durations: ['8th', '16th', '16th', '8th', '16th', '16th']
mldy: [[('D4', 'hlf', 1.0), ('E4', '8th', 0.25), ('F4', '16th', 0.125), ('A5', '16th', 0.125), ('E4', '8th', 0.25), ('F4', '16th', 0.125), ('A5', '16th', 0.125)], [], [], [('?', '?', '?'), ('D4', 'qtr●', 0.75), ('A5', 'qtr●', 0.75)], [('B5', 'hlf', 1.0)], [], [], [('A5', 'hlf', 1.0), ('D5', 'hlf', 1.0)]]
bars: [2.0, 0, 0, 1.5, 1.0, 0, 0, 2.0]
What’s That Sound Like
I used MuseScore to create a score. Then saved it to flac. An image of the score and the flac file follow. Note that I did not use the notes in the scale. I.E. that C is a natural C (♮) not a sharp as specified by the scale of F Major.

And it sounds like this.
You know, we might be heading in the right direction.
Done!
Just getting a single step series has taken a lot more effort than I was expecting—you’d think I’d have learned by now! But it does feel like a touch of progress has been made. And, there’s a lot of text and code in this post. So I believe it is time to let it rest.
Next time I want to fix something in the case of a small, repeated sequence. Right now it just repeats the same notes. But I think it would be much better if the repeated series was stepped up or down slightly but using the same rhythym.
I also plan to move on to coding a jump method and, hopefully, the repeat old stuff method. That latter will look at taking some pattern already in the melody and reuse it. Same notes different rhythm, notes moved up or down with same rhythm, or exactly the same notes and rhythm.
Until then, do enjoy your successes; no matter how small nor how hard fought.
Resources
Not sure these all belong here, but it will save me putting them somewhere else.
- The 7 Music Modes Explained
- The Sequence
- Identifying sequences
- Diatonic Sequences of Triads
- Diatonic Sequences
- The Leaping Romanesca Vs the Stepwise Romanesca
- Melodic Sequences