Wasn’t sure where I was going to go next. I really do want to look at generating something like a melody to go with the chord progressions. Still not sure how to go about that. But, before I do, I want to look at a bit of a refactor.
Re-Use Chord Progression
A couple of posts back, I looked at producing two channel wave files. And, with a workaround, multi-track midi files. But, I had what I saw as a problem. Unless I used a named common chord progression, I could not make the second channel for the wave file use the chord progression randomly created in the call to em_chords.make_chord_prog() for the first channel. I would like to fix that. Though, as usual, not yet sure how to go about it.
Will have to rethink my arguments list and how I use those arguments, m’thinks. Well! Turns out to be simpler than I thought.
... ...
# em_chords.make_cp_chords()
... ...
if cp_nm[:3] == "ran":
# get a random progression
rn_prg = make_progression(n_chds)
else:
# refactored code block
if cp_nm in common_prog:
rn_prg = common_prog[cp_nm]
else:
rn_prg = cp_nm
... ...
# main.py
... ...
for o_i, cp_oct in enumerate(do_octs):
snd = None
args["r_oct"] = cp_oct
rn_prg, snd = emc.make_chord_prog(args)
# new code
if o_i == 0:
args["cp_type"] = rn_prg
A quick test says that worked just fine.
... ...
writing to wave file: img/I-vi-IV-V_4-4_seq_sqr_o2_s0.wav
Predicting MIDI for img\I-vi-IV-V_4-4_seq_sqr_o2_s0.wav...
Creating midi...
💅 Saved to img\I-vi-IV-V_4-4_seq_sqr_o2_s0_basic_pitch.mid
Creating midi sonification...
🎧 Saved to img\I-vi-IV-V_4-4_seq_sqr_o2_s0_basic_pitch.wav
... ...
writing to wave file: img/I-vi-IV-V_4-4_seq_sqr_o5_s1.wav
Predicting MIDI for img\I-vi-IV-V_4-4_seq_sqr_o5_s1.wav...
Creating midi...
💅 Saved to img\I-vi-IV-V_4-4_seq_sqr_o5_s1_basic_pitch.mid
Creating midi sonification...
🎧 Saved to img\I-vi-IV-V_4-4_seq_sqr_o5_s1_basic_pitch.wav
Added Dotted Note Values
I noticed in a number of articles, tutorials, etc. that dotted note values appeared all too frequently. So I think I need to refactor my code to allow for such note values. Again, have not sorted out my approach. Will tackle that now. But hopefully fairly easy to do.
I have decided to just add a “●” to the end of the note values already defined in the Note_durations class to generate new values and see what happens. Certainly extended the size of some of the class members. Then I had to modify the data structure supplying the frequency with which each duration would be selected.
# in the em_bars module
... ...
class Note_durations():
def __init__(self, t_sig:tuple[int, int]=(4, 4), s_bt:float=1.0):
self.n_dur = {"whl": t_sig[1]*s_bt, "hlf●": t_sig[1]*s_bt*3/4, "hlf": t_sig[1]*s_bt/2,
"qtr●": t_sig[1]*s_bt*3/8, "qtr": t_sig[1]*s_bt/4,
"8th●": t_sig[1]*s_bt*3/16, "8th": t_sig[1]*s_bt/8,
"16th●": t_sig[1]*s_bt*3/32, "16th": t_sig[1]*s_bt/16,
"32nd": t_sig[1]*s_bt/32}
self.dur2nt = {v: k for k, v in self.n_dur.items()}
self.n_hlf = {"whl": "hlf", "hlf": "qtr", "qtr": "8th", "8th": "16th", "16th": "32nd", "32nd": "64th"}
self.n_dbl = {"whl": "dbl", "hlf": "whl", "qtr": "hlf", "8th": "qtr", "16th": "8th", "32nd": "16th"}
self.rel_dur = {k: v / self.n_dur["whl"] for k, v in self.n_dur.items()}
... ...
# increased the default number of notes to use in generating a bar of notes/chords
def make_bar(cp_len:int, t_sig:tuple[int, int]=(4, 4), s_bt:float=1.0, nt_use:int=6) -> tuple[list[str], float]:
... ...
# set probability of selecting note values depending on base note value
match t_sig[1]:
case 4:
n_prb = [1, 1, 1, 6, 6, 4, 4, 2, 2, 1]
case 2:
n_prb = [1, 5, 5, 5, 5, 2, 2, 1, 1, 1]
case 8:
n_prb = [1, 1, 1, 2, 2, 6, 6, 4, 4, 2]
case _:
n_prb = [1, 2, 2, 6, 6, 5, 5, 1, 1, 1]
But, nowhere, currently in my code, do I specifically allow control of make_bar’s nt_use parameter. So I also modified the code to allow that control should I wish it. I started by expanding the default argument list to include such an argument. Then added a new argument to the command line parser. Though that may need some refactoring, just trying to get things to work. Added a check to the get_args_do_m_cp() function (in em_ui module).
# in the em_bars module
... ...
""" Globals
BR_MN: minimum number of bars for a track
BR_MX: maximum number of bars for a track
ND_MN: minimum number of note durations from list of available durations
ND_MX: maximum number of note durations from list of available durations
"""
BR_MN, BR_MX = 6, 50 # for now
ND_MN, ND_MX = 4, 10 # for now
... ...
# in em_ui module
... ...
A_DEF = {
"whatdo": "m_cp",
"filepath_in": "",
"filepath_wv": "",
"filepath_mid": "",
"track_key": "random",
"nbr_bars": 6,
"play_track": False,
"no_save_wv": False,
"no_save_mid": False,
"cp_type": "random",
"cp_nbr": 0,
"nbr_ndurs": 6, # new item and default value
"instr": [],
}
def get_parser() -> argparse.ArgumentParser:
... ...
parser.add_argument("-nnd", "--nbr_ndurs", type=ndur_type, metavar=f"[{emb.ND_MN}-{emb.ND_MX}]",
help=f"The number of possible note durations to be used generating bars of music, {emb.ND_MN}-{emb.ND_MX}")
... ...
def get_args_do_m_cp(cli_args:argparse.Namespace) -> tuple[bool, dict[str, Union[str, int, bool]]]:
... ...
case "nbr_ndurs":
if a_val is None:
# if no user specified value, for now go with the default in dictionary
continue
t_args[a_key] = a_val
... ...
def ndur_type(a_val):
""" Validates if the input integer falls within the default range of available note durations."""
i_val = int(a_val)
if i_val < emb.ND_MN or i_val > emb.ND_MX:
raise argparse.ArgumentTypeError(f"must be an integer between {emb.ND_MN} and {emb.ND_MX} (given {i_val})")
return i_val
... ...
# in em_bars module
... ...
# new parameter
def make_chd_rhythm(cp_len:int, t_sig:tuple[int, int]=(4, 4), n_bars:int=3, tempo:int=120, nt_use:int=6, retro:bool=False) -> list[float]:
... ...
m_nts, t_dur = make_bar(cp_len, t_sig=t_sig, s_bt=s_bt, nt_use=nt_use)
... ...
# in em_chords
... ...
def make_chord_prog(args:dict[str, Union[str, int, bool]]) -> tuple[str, npt.NDArray[np.number[int | float]]]:
... ...
rn_prg, c_prg, cp_base = make_cp_chords(s_rnt, s_frm, args["r_oct"], args["t_sig"], args["cp_type"],
args["cp_nbr"], dbg=args["dbg"])
A little test to see if anything else needs changing.
Bug Fixes
While trying to test the above changes I found that I had a number of bugs in the em_bars module. I had modified the NV_* global variables’ names in other modules using them. Had to fix in em_bars (thought I had done so long ago, but maybe I just modified the code above and not in em_bars). And the chk_bars() function was a complete mess. Guess I didn’t refactor it correctly when adding it to the module from some previous code.
Also added a debug parameter to control printing variable information to the terminal. That did involve doing the same in some other functions in different modules. As well, I added a new argument to the command line user interface. Not going to show those changes. And, that is still work in progress—will slowly modify all code to use that argument value.
def chk_bars(nts_t:npt.NDArray[np.str_], t_sig:tuple[int, int]=(4, 4), s_bt:float=1.0, p_dbg:bool=False) -> bool:
""" Check that multiple bars for generated music all have the same duration
:param nts_t: note values for the progression, e.g. whl, qtr, 16th
:param t_sig: time signature for the progression/music
:param s_bt: number of seconds per beat
:return: bool, True if bar durations are good, False otherwise
"""
n_durs = Note_durations(t_sig, s_bt)
dur_bar = t_sig[0] * s_bt
n_bars = 0
t_durs = []
for n_val in nts_t:
t_durs.append(n_durs.n_dur[n_val])
t_bars = sum(t_durs) // dur_bar + 1
t_bars = sum(t_durs) // dur_bar
if p_dbg:
print(f"\tchk_bars({t_sig}, {s_bt}) dur_bar: {dur_bar}; sum t_durs: {sum(t_durs)}; t_bars: {t_bars}")
n_nts = len(nts_t)
if t_bars == 2 or t_bars == 4:
if sum(t_durs[:(n_nts//2)]) != sum(t_durs[(n_nts//2):]):
if p_dbg:
print(f"\tsum(t_durs[:({n_nts//2})]) != sum(t_durs[({n_nts//2}):] -> {sum(t_durs[:(n_nts//2)])} != {sum(t_durs[(n_nts//2):])}")
return False
if t_bars == 3:
if sum(t_durs[:(n_nts // 3)]) != sum(t_durs[(n_nts//3):(n_nts//3)*2]):
if p_dbg:
print(f"\tsum(t_durs[:({n_nts//3})]) != sum(t_durs[({n_nts//3}):({n_nts//3*2})] -> {sum(t_durs[:(n_nts//3)])} != {sum(t_durs[(n_nts//3):(n_nts//3*2)])}")
return False
return True
Test
Okay, I ran a couple of tests and things look okay.
PS R:\learn\e_m_311> uv run main.py -wd m_cp -nbr 8
... ...
command line args are ok: True; augmented args are:
whatdo: m_cp
filepath_in:
filepath_wv:
filepath_mid:
track_key: ('F', 'major')
nbr_bars: 8
play_track: False
no_save_wv: False
no_save_mid: False
cp_type: random
cp_nbr: 4
nbr_ndurs: 6
instr: []
dbg: False
fs: <midi2audio.FluidSynth object at 0x00000205ADCA8ED0>
r_oct: 4
t_sig: (4, 4)
s_rate: 44100
ot_s: seq
ot_w: saw
n_ot: 10
save_wave: snd.shape (683550,)
writing to wave file: img/I-vii-IV-iii_4-4_seq_saw_o4_1.wav
PS R:\learn\e_m_311> uv run main.py -wd m_cp -nbr 8 -dbg
... ...
command line args are ok: True; augmented args are:
whatdo: m_cp
filepath_in:
filepath_wv:
filepath_mid:
track_key: ('D#', 'major')
nbr_bars: 8
play_track: False
no_save_wv: False
no_save_mid: False
cp_type: random
cp_nbr: 3
nbr_ndurs: 6
instr: []
dbg: True
fs: <midi2audio.FluidSynth object at 0x000001E426708F10>
r_oct: 4
t_sig: (3, 4)
s_rate: 44100
ot_s: even
ot_w: saw
n_ot: 10
selected key: D# major (octave: 4)
scale notes: ['D#4', 'F4', 'G4', 'G#4', 'A#4', 'C5', 'D5']
key chords: [('D#', 'major'), ('F', 'minor'), ('G', 'minor'), ('G#', 'major'), ('A#', 'major'), ('C', 'minor'), ('D', 'dim')]
chord progression (roman numerals, random): I-ii-V
chord progression: [('D#', 'major'), ('F', 'minor'), ('A#', 'major')]
[
D# major -> ['D#4', 'G4', 'A#4']
F minor -> ['F4', 'G#4', 'C5']
A# major -> ['A#4', 'D5', 'F5']
]
multipliers: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
amplitudes: [0.2477, 0.1638, 0.1256, 0.0997, 0.0832, 0.0714, 0.0613, 0.0538, 0.0493, 0.0441]
chk_bars((3, 4), 0.5) dur_bar: 1.5; sum t_durs: 3.0; t_bars: 2.0
sum(t_durs[:(1)]) != sum(t_durs[(1):] -> 0.5 != 2.5
chk_bars((3, 4), 0.5) dur_bar: 1.5; sum t_durs: 3.0; t_bars: 2.0
{'whl': 2.0, 'hlf': 1.0, 'hlf●': 1.5, 'qtr': 0.5, 'qtr●': 0.75, '8th': 0.25, '8th●': 0.375, '16th': 0.125, '16th●': 0.1875, '32nd': 0.0625}
n_bars: 8, cp_len: 3, len cp_rhy: 7, len cp_durs: 10, len c_amps: 24
calling make_cp_sound
['qtr', 'qtr●', '8th']
0: qtr -> 0.5 * 0.3003 -> 0: ('D#', 'major')
1: qtr● -> 0.75 * 0.5952 -> 1: ('F', 'minor')
2: 8th -> 0.25 * 0.2739 -> 2: ('A#', 'major')
['hlf●', 'qtr●', 'qtr●']
0: hlf● -> 1.5 * 0.7031 -> 0: ('D#', 'major')
1: qtr● -> 0.75 * 0.2161 -> 1: ('F', 'minor')
2: qtr● -> 0.75 * 0.5110 -> 2: ('A#', 'major')
['qtr', 'qtr', 'qtr']
0: qtr -> 0.5 * 0.4909 -> 0: ('D#', 'major')
1: qtr -> 0.5 * 0.4197 -> 1: ('F', 'minor')
2: qtr -> 0.5 * 0.5075 -> 2: ('A#', 'major')
['8th', '8th', 'hlf']
0: 8th -> 0.25 * 0.5631 -> 0: ('D#', 'major')
1: 8th -> 0.25 * 0.5083 -> 1: ('F', 'minor')
2: hlf -> 1.0 * 0.4316 -> 2: ('A#', 'major')
['qtr', 'qtr', 'qtr']
0: qtr -> 0.5 * 0.5732 -> 0: ('D#', 'major')
1: qtr -> 0.5 * 0.3818 -> 1: ('F', 'minor')
2: qtr -> 0.5 * 0.6882 -> 2: ('A#', 'major')
['qtr', '8th', 'qtr●']
0: qtr -> 0.5 * 0.6728 -> 0: ('D#', 'major')
1: 8th -> 0.25 * 0.6043 -> 1: ('F', 'minor')
2: qtr● -> 0.75 * 0.5899 -> 2: ('A#', 'major')
['qtr', 'qtr●', '8th']
0: qtr -> 0.5 * 0.7363 -> 0: ('D#', 'major')
1: qtr● -> 0.75 * 0.5424 -> 1: ('F', 'minor')
2: 8th -> 0.25 * 0.2459 -> 2: ('A#', 'major')
make_c_sound done: 0.0156
save_wave: snd.shape (507150,)
writing to wave file: img/I-ii-V_3-4_even_saw_o4_1.wav
And, that seemed to work correctly for \({}^{3}_{4}\) and \({}^{4}_{4}\) time signatures.
Think This One Is Done
I was going to continue on and try to code something to generate a melody. A nicer sounding bit to go along with a chord progression. Though I am beginning to believe my chord progression code is generating a melody like progression rather than what would be thought of, by a real musician, as a chord progression.
But, quite seriously, I at the moment have no idea how to go about that in any kind of a meaningful way. And, I expect it could get to be a lengthy discussion on my part. So, I think I will call this post finished. And, spend some time pondering the future.
Until next time, be happy, be safe.