Well, let’s see if we can get on to generating sound files of our chord progressions with somewhat varying note values for each chord. Eventually I would also like to add some variation in each chord’s amplitude/volume. But one thing at a time.
Bug Fix
While working on things I got a rhythm that, for a 4/4 signature with 60 bpm, had a bar with a total duration of 3.5 seconds followed by one with a duration of 4.5 seconds. Well, we just can’t have that. Each bar should have been 4 seconds long. I also found that for a 4/4 signature, I didn’t like the use of a whole note in the generated ryhthm. So a bit of refactoring.
I ended up adding a new function, chk_bars, to check the case where the progression was spread over multiple bars. It is meant to ensure the bars are all of equal length. Though for now I am doing a bit of fudging.
In make_bar, for the signatures of x/4 and x/8, I set the probability for a whole note to zero. And then whenever the progression ran more than one bar, I called chk_bars to ensure the bars in the progression rhythm are of equal duration.
... ...
def chk_bars(nts_t:np.typing.NDArray[np.str_], t_sig:tuple[int, int]=(4, 4), s_bt:float=1.0) -> 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] / t_sig[1]) * 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
n_nts = len(nts_t)
if t_bars == 2 or t_bars == 4:
if sum(t_bars[:(n_nts//2)]) != sum(t_bars[(n_nts//2):]):
return False
if t_bars == 3:
if sum(t_bars[:(n_nts // 3)]) != sum(t_bars[(n_nts//3):(n_nts//3)*2]):
return False
return True
... ...
def make_bar(cp_len:int, t_sig:tuple[int, int]=(4, 4), s_bt:float=1.0, nt_use:int=4) -> tuple[list[str], float]:
"""
Generate a set of note values to for a sequence of chords of cp_len length.
The list of note values may in fact have a duration of 1 or more full bars.
param cp_len: chord progression length,
assumes progression does not end with the initial/root chord
param t_sig: time signature, tuple(upper int, lower int)
param s_bt: number of seconds per beat
param nt_use: number of note durations to use from list of available durations
Return: a list of note values and total duration of sequence of note values
"""
n_durs = Note_durations(t_sig, s_bt)
# set probability of selecting note values depending on base note value
match t_sig[1]:
case 4:
n_prb = [0, 2, 6, 4, 1, 1]
case 2:
n_prb = [1, 5, 5, 2, 1, 1]
case 8:
n_prb = [0, 1, 2, 6, 4, 2]
case _:
n_prb = [1, 2, 6, 5, 1, 1]
m_dur = t_sig[0] * s_bt
# select allowed note values
t_avl = np.array(list(n_durs.n_dur.keys())[:nt_use])
# convert probablilities to sereies floats summing to 1.0
t_prb = np.array(n_prb[:nt_use])
sum_prb = sum(t_prb)
t_prb = t_prb / sum_prb
while True:
# keep sorting note values until we have a suitable duration
nts_t = rng.choice(t_avl, cp_len-1, p=t_prb)
t_dur = 0
for nt in nts_t:
t_dur += n_durs.n_dur[nt]
# are we generating more than 1 bar
t_bars = (t_dur // m_dur) + 1
f_n_dur = (m_dur * t_bars) - t_dur
if f_n_dur not in n_durs.dur2nt.keys():
continue
if n_durs.dur2nt[f_n_dur] == "whl" and t_prb[0] == 0:
continue
nts_t = np.append(nts_t, n_durs.dur2nt[f_n_dur])
t_dur += f_n_dur
if t_bars > 1 and (not chk_bars(nts_t, t_sig=t_sig, s_bt=s_bt)):
continue
break
return nts_t, t_dur
Some testing seemed to indicate it works. Though perhaps not all that necessary for my case.
Apply Note Values to Chords and Generate Sound File
Okay, let’s use the refactored functions and see about modifying the duration of each chord in each bar. Duplication of bars likely in many cases.
Note the added information in the file name.
... ...
if do_mk_play_cprog:
... ...
cp_rhy = make_chd_rhythm(cp_len, t_sig=t_sig, n_bars=6, tempo=60, retro=False)
cp_durs = Note_durations((4, 4), 1.0)
print(f"\t{cp_durs.n_dur}")
a_bars = []
i_bar_ln = len(p_chds[0])
for cp_bar in cp_rhy:
print(f" {cp_bar}")
for i, n_val in enumerate(cp_bar):
c_chd = i % cp_len
print(f" {i}: {n_val} -> {cp_durs.n_dur[n_val]} -> {c_chd}: {c_prg[c_chd]}")
if cp_durs.n_dur[n_val] < 1:
aclen = int(i_bar_ln * cp_durs.n_dur[n_val])
a_bars.append(p_chds[c_chd][:aclen])
elif cp_durs.n_dur[n_val] > 1:
for _ in range(int(cp_durs.n_dur[n_val])):
a_bars.append(p_chds[c_chd])
else:
a_bars.append(p_chds[c_chd])
a_bars.append(nosnd)
do_sav_wav = False
if do_sav_wav:
snd = np.hstack(a_bars)
snd = emu.normalize_wave(snd, do_typ=True)
w_fl_nm = f"{rn_prg}_{t_sig[0]}-{t_sig[1]}_{ot_s}_{ot_w}_o{r_oct}_1.wav"
# Open a WAV file
with wave.open(f'img/{w_fl_nm}', 'w') as wav_file:
print(f"writing to wave file: {w_fl_nm}")
# Define audio parameters
wav_file.setnchannels(1) # Mono
wav_file.setsampwidth(2) # Two bytes per sample
wav_file.setframerate(sample_rate)
# Convert the NumPy array to bytes and write it to the WAV file
wav_file.writeframes(snd.tobytes())
if False:
sd.play(snd)
sd.wait()
And, in the terminal the following output was displayed for a single run.
(base) PS R:\learn\e_music> uv run tst_chords.py
chord progression with overtones on each chord
selected key: A major
scale notes: ['A1', 'B1', 'C#2', 'D2', 'E2', 'F#2', 'G#2']
key chords: [('A', 'major'), ('B', 'minor'), ('C#', 'minor'), ('D', 'major'), ('E', 'major'), ('F#', 'minor'), ('G#', 'dim')]
chord progression (roman numerals): I-iii-vi-IV-I
chord progression: [('A', 'major'), ('C#', 'minor'), ('F#', 'minor'), ('D', 'major'), ('A', 'major')]
[
A major -> ['A1', 'C#2', 'E2']
C# minor -> ['C#1', 'E1', 'G#1']
F# minor -> ['F#1', 'A1', 'C#2']
D major -> ['D1', 'F#1', 'A1']
A major -> ['A1', 'C#2', 'E2']
]
multipliers: [3, 5, 7, 9, 11, 13, 15, 17, 19, 21]
amplitudes: [0.4817, 0.2576, 0.1433, 0.064, 0.0271, 0.0146, 0.0067, 0.0028, 0.0014, 0.0008]
{'whl': 4.0, 'hlf': 2.0, 'qtr': 1.0, '8th': 0.5, '16th': 0.25, '32nd': 0.125}
['qtr', 'qtr', '8th', 'qtr', '8th']
0: qtr -> 1.0 -> 0: ('A', 'major')
1: qtr -> 1.0 -> 1: ('C#', 'minor')
2: 8th -> 0.5 -> 2: ('F#', 'minor')
3: qtr -> 1.0 -> 3: ('D', 'major')
4: 8th -> 0.5 -> 4: ('A', 'major')
['hlf', 'hlf', 'qtr', 'qtr', 'hlf']
0: hlf -> 2.0 -> 0: ('A', 'major')
1: hlf -> 2.0 -> 1: ('C#', 'minor')
2: qtr -> 1.0 -> 2: ('F#', 'minor')
3: qtr -> 1.0 -> 3: ('D', 'major')
4: hlf -> 2.0 -> 4: ('A', 'major')
['8th', 'qtr', 'qtr', 'qtr', '8th']
0: 8th -> 0.5 -> 0: ('A', 'major')
1: qtr -> 1.0 -> 1: ('C#', 'minor')
2: qtr -> 1.0 -> 2: ('F#', 'minor')
3: qtr -> 1.0 -> 3: ('D', 'major')
4: 8th -> 0.5 -> 4: ('A', 'major')
['qtr', '8th', 'qtr', '8th', 'qtr']
0: qtr -> 1.0 -> 0: ('A', 'major')
1: 8th -> 0.5 -> 1: ('C#', 'minor')
2: qtr -> 1.0 -> 2: ('F#', 'minor')
3: 8th -> 0.5 -> 3: ('D', 'major')
4: qtr -> 1.0 -> 4: ('A', 'major')
['qtr', '8th', '8th', 'qtr', 'qtr']
0: qtr -> 1.0 -> 0: ('A', 'major')
1: 8th -> 0.5 -> 1: ('C#', 'minor')
2: 8th -> 0.5 -> 2: ('F#', 'minor')
3: qtr -> 1.0 -> 3: ('D', 'major')
4: qtr -> 1.0 -> 4: ('A', 'major')
Okay, let’s save a run to a wav file and have a listen. It was nothing special to listen to, so not including an example in the post.
My apologies!
The following content is likely going to be a little less than my norm. I did a bunch of coding without making notes or adding content to this post. So I am working backwards usinggit diffs.
Add Control Over Base Octave for Chord Progression
I personally found that sometimes the chords seemed too highly pitched. So, I decided I should refactor my code to allow for the selection of a base octave, relative to a piano, for the musical scale for the chord progression. Instead of always having the root note for the scale in the 4th octave I would allow for a start in octaves 1-4. A bit more work than I thought it would be.
I started with the get_2_oct_piano function. It is sort of the base of the tree. I was just going to show the diffs for each refactored function, but have decided to make it easier for everyone and include the complete refactored code.
def get_2_oct_piano(r_nt:Notes_scale, r_oct:int=4) -> list[Notes_scale]:
""" Return two octaves of notes on the piano beginning at r_nt and r_oct.
Note: A0 <= r_nt+r_oct <= C6
:param r_nt: root note symbol for the desired chromatic scale, str in Notes_scale
:param r_oct: starting octave, relative to piano, for root note, 0 <= r_oct <= 6
:return: list of the notes, with octave indicated, in the appropriate order for 2 octaves
"""
h_scale = []
r_no = f"{r_nt}{r_oct}"
param_ok = r_nt in C_SCALE and (r_no == "A0" or r_no == "C6" or (r_oct >= 1 and r_oct < 6))
if param_ok:
h_off = get_half_tone(r_nt)
h_scale.extend(C_SCALE[h_off:])
h_scale.extend(C_SCALE[:h_off])
s_oct = r_oct
for i, nt in enumerate(h_scale):
if nt == "C" and i > 0:
s_oct += 1
h_scale[i] = f"{nt}{s_oct}"
h_sc2 = h_scale[:]
for i, nt in enumerate(h_sc2):
n_nw = f"{nt[:-1]}{int(nt[-1]) + 1}"
h_scale.append(n_nw)
return h_scale
Then I went on to the primary function that calls the one above, get_scale_4_note.
def get_scale_4_note(r_nt:Notes_scale, r_oct:int=4, s_mode:Scale_forms="major") -> list[Notes_scale]:
""" Get the sequence of scale notes for the specific root note
Default is that the root note is in the 4th octave of a piano.
Returning 2 octaves to simplify generating lengthier chords.
Note: A0 <= r_nt+r_oct <= C6
:param r_nt: root note symbol for the desired chromatic scale, str
:param r_oct: starting octave, relative to piano, for root note, 0 <= r_oct <= 6
: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 = Scale_forms()
# bloody hell!, 3 minor scales, who knew?? ascending/descending?
# melodic minor also has descending version which is natural minor
ok_scl = ["major", "min_nat", "min_har", "min_mel"]
step_s_2_int = {"W": 2, "H": 1, "W½": 3}
h_scale, h_steps = [], []
if s_mode not in ok_scl:
return h_scale
# get notes for two piano octaves starting at tonic note
t_scale = get_2_oct_piano(r_nt, r_oct=r_oct)
# 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]
# print(c_step, end="")
h_steps.append(c_step)
h_scale.append(t_scale[c_step])
return h_scale
Not sure about the refactoring order for the next two. But, while testing could easily have been either one first.
Let’s start with the function used to generate the notes in a chord, make_chord.
def make_chord(nt:Notes_scale, c_qual:ChordFormula, r_oct:int=4) -> list[Notes_scale]:
""" Determine and return notes for specified root note and chord quality.
Note: A0 <= r_nt+r_oct <= C6
:param nt: the root note for the chord, str
:param c_qual, the chord quality, str, one of enum ChordFormula
:param r_oct: starting octave, relative to piano, for root note, 0 <= r_oct <= 6
:return: list of notes for the chord, str, note from C-SCALE plus octave number
"""
h_tones = ChordFormula.__getitem__(c_qual).value
h_scale = get_2_oct_piano(nt, r_oct=r_oct)
chord = [h_scale[v] for v in h_tones]
return chord
And, finally, the function that turns the roman numerals of a chord progression in a list of actual chords.
def convert_cprog(cp_rn:str, r_nt:Notes_scale, r_oct:int=4, p_qual:Chords_ok="major") -> list[tuple[Notes_scale, ChordFormula]]:
""" Convert a list of roman numerals into the actual chord progression.
But only chord root and quality, not actual chord notes.
Note: A0 <= r_nt+r_oct <= C6
:param cp_rn: string of roman numerals + modifier symbols for chord progression separated by a dash
:param r_nt: root note including any incidental for the music scale
:param r_oct: starting octave, relative to piano, for root note, 0 <= r_oct <= 6
:param s_qual: type/quality of scale, e.g. major, min_nat, etc.
return: list of tuples identifying the specific chords in the progression
"""
# chord location in sumbitted chords
f2loc = {"i": 0, "ii": 1, "iii": 2, "iv": 3, "v": 4, "vi": 5, "vii": 6}
chd_prg = []
# let's start by getting the notes for the appropriate scale
s_nts = get_scale_4_note(r_nt, r_oct=r_oct, s_mode=p_qual)
s_chds = scale_chords(s_nts, p_qual)
# only want romman numerals, need to remove dashes
for rn in cp_rn.split("-"):
# for now only deal with following modifiers: leading ♭, trailing 7 or °
if "°" in rn and "7" in rn:
...
elif "°" in rn:
t_rn = rn[:-1]
c_rt = s_chds[f2loc[t_rn.lower()]][0]
chd_prg.append((c_rt, "dim"))
elif "7" in rn:
t_rn = rn[:-1]
c_rt = s_chds[f2loc[t_rn.lower()]][0]
c_qual = "major" if t_rn.isupper() else "minor"
chd_prg.append((c_rt, f"{c_qual}7"))
elif "♭" in rn:
t_rn = rn[1:]
# convert roman numeral to chord's root note
c_rt = s_chds[f2loc[t_rn.lower()]][0]
c_qual = "major" if t_rn.isupper() else "minor"
# for now I don't deal with notes labelled flat, bad choice perhaps
chd_prg.append((convert_2_flat(c_rt), f"{c_qual}"))
else:
chd_prg.append(s_chds[f2loc[rn.lower()]])
return chd_prg
And I think that’s it. No test code, terminal output, sound files, etc. My tests appeared to work as desired. So, let’s move on to what I had planned this post to be about.
Modify Amplitude/Volume of Each Chord
For now I am going to use a random series of amplitudes. I will likely have limits on the amplitude extremes. And am thinking I will try to have a higher probability of getting values in the middle section of the range.
At this point all the code is in the test block. No new function. Though that will likely, eventually, change. Some older code for reference.
... ...
# let's get a rhythm and see if we can get the sound array sorted appropriately
cp_len = len(p_chds)
cp_rhy = make_chd_rhythm(cp_len, t_sig=t_sig, n_bars=6, tempo=60, retro=False)
cp_durs = Note_durations((4, 4), 1.0)
print(f"\t{cp_durs.n_dur}")
# let's get a series of amplitutes to match the series of chord note values
# for now I don't want it too low or too high say 10-90% with a
# preference for the middle values
c_bars = len(cp_rhy)
c_n_bar = len(cp_rhy[0]) # should be equal to cp_len
print(f"\tc_n_bar: {c_n_bar} ?= cp_len: {cp_len} * c_bars: {c_bars} -> {c_bars * c_n_bar}")
mn_amp, mx_amp = .25, .8
mn_amp, mx_amp = .1, .9
md_amp = (mn_amp + mx_amp) / 2
c_amps = np.random.triangular(mn_amp, md_amp, mx_amp, int(c_bars * c_n_bar))
a_bars = []
i_bar_ln = len(p_chds[0])
for j, cp_bar in enumerate(cp_rhy):
print(f" {cp_bar}")
for i, n_val in enumerate(cp_bar):
c_chd = i % cp_len
c_pos = j * cp_len + i
print(f" {i}: {n_val} -> {cp_durs.n_dur[n_val]} * {c_amps[c_pos]:.4f} -> {c_chd}: {c_prg[c_chd]}")
if cp_durs.n_dur[n_val] < 1:
aclen = int(i_bar_ln * cp_durs.n_dur[n_val])
a_bars.append(p_chds[c_chd][:aclen] * c_amps[c_pos])
elif cp_durs.n_dur[n_val] > 1:
for _ in range(int(cp_durs.n_dur[n_val])):
a_bars.append(p_chds[c_chd])
else:
a_bars.append(p_chds[c_chd])
a_bars.append(nosnd)
do_sav_wav = True
do_play_wav = False
if do_sav_wav:
snd = np.hstack(a_bars)
snd = emu.normalize_wave(snd, do_typ=True)
w_fl_nm = f"{rn_prg}_{t_sig[0]}-{t_sig[1]}_{ot_s}_{ot_w}_o{r_oct}_1.wav"
# Open a WAV file
with wave.open(f'img/{w_fl_nm}', 'w') as wav_file:
print(f"writing to wave file: {w_fl_nm}")
# Define audio parameters
wav_file.setnchannels(1) # Mono
wav_file.setsampwidth(2) # Two bytes per sample
wav_file.setframerate(sample_rate)
# Convert the NumPy array to bytes and write it to the WAV file
wav_file.writeframes(snd.tobytes())
if do_play_wav:
sd.play(snd)
sd.wait()
And, in the terminal, I got the following output for a sample run.
(base) PS R:\learn\e_music> uv run tst_chords.py
chord progression with overtones on each chord
selected key: C min_nat (octave: 3)
scale notes: ['C3', 'D3', 'D#3', 'F3', 'G3', 'G#3', 'A#3']
key chords: [('C', 'minor'), ('D', 'dim'), ('D#', 'major'), ('F', 'minor'), ('G', 'minor'), ('G#', 'major'), ('A#', 'major')]
chord progression (roman numerals): I-vii-vi
chord progression: [('C', 'minor'), ('A#', 'major'), ('G#', 'major')]
[
C minor -> ['C3', 'D#3', 'G3']
A# major -> ['A#3', 'D4', 'F4']
G# major -> ['G#3', 'C4', 'D#4']
]
multipliers: [3, 5, 7, 9, 11, 13, 15, 17, 19, 21]
amplitudes: [0.2378, 0.1727, 0.1269, 0.0975, 0.0821, 0.0706, 0.0614, 0.0557, 0.0502, 0.0449]
{'whl': 4.0, 'hlf': 2.0, 'qtr': 1.0, '8th': 0.5, '16th': 0.25, '32nd': 0.125}
c_n_bar: 3 ?= cp_len: 3 * c_bars: 6 -> 18
['hlf', 'qtr', 'qtr']
0: hlf -> 2.0 * 0.6089 -> 0: ('C', 'minor')
1: qtr -> 1.0 * 0.5488 -> 1: ('A#', 'major')
2: qtr -> 1.0 * 0.5573 -> 2: ('G#', 'major')
['qtr', 'qtr', 'hlf']
0: qtr -> 1.0 * 0.7107 -> 0: ('C', 'minor')
1: qtr -> 1.0 * 0.1678 -> 1: ('A#', 'major')
2: hlf -> 2.0 * 0.5505 -> 2: ('G#', 'major')
['qtr', 'qtr', 'hlf']
0: qtr -> 1.0 * 0.4180 -> 0: ('C', 'minor')
1: qtr -> 1.0 * 0.5024 -> 1: ('A#', 'major')
2: hlf -> 2.0 * 0.4028 -> 2: ('G#', 'major')
['qtr', 'qtr', 'hlf']
0: qtr -> 1.0 * 0.3823 -> 0: ('C', 'minor')
1: qtr -> 1.0 * 0.5154 -> 1: ('A#', 'major')
2: hlf -> 2.0 * 0.7373 -> 2: ('G#', 'major')
['qtr', 'qtr', 'hlf']
0: qtr -> 1.0 * 0.6903 -> 0: ('C', 'minor')
1: qtr -> 1.0 * 0.6040 -> 1: ('A#', 'major')
2: hlf -> 2.0 * 0.5024 -> 2: ('G#', 'major')
['hlf', 'qtr', 'qtr']
0: hlf -> 2.0 * 0.5573 -> 0: ('C', 'minor')
1: qtr -> 1.0 * 0.3426 -> 1: ('A#', 'major')
2: qtr -> 1.0 * 0.3597 -> 2: ('G#', 'major')
writing to wave file: I-vii-vi_4-4_odd_saw_o3_1.wav
For now, no sample .wav file.
Last Note of Chord Progression
I have been led to understand that it is considered best practice to end a chord progression on its root chord. To that end, I am going to, on reaching the last chord of the series of bars, either replace it with the root chord or split the duration in half for the current chord and again for the root chord to finish. Not sure what criteria to use for the decision. Though duration of that last chord is likely a meaningful consideration. So, for now, I will replace if duration is less than a quarter note. Split if quarter note or longer.
A lot of messing around, so have decided to move the relevant code to produce the sound array into a new function or two. We’ll start with one to generate a series of amplitudes. Then the one that actually generates the sound array using all the other pertinent data. make_cp_sound is a touch lengthy, but it really only does one thing so I feel okay with that situation. And the code to deal with the last chord in the last bar is included.
... ...
from typing import Any, Union
... ...
def get_amplitudes(s_len:int, mn_amp:float=.1, mx_amp:float=.9):
""" Get a series of 'random' amplitudes of the desired length
:param s_len: series len
:return: series of amplitudes
"""
# let's get a series of amplitutes to match the series of chord note values
# for now I don't want it too low or too high say 10-90% with a
# preference for the middle values
if mn_amp <= 0 or mn_amp >= mx_amp or mn_amp > 1:
mn_amp = .1
if mx_amp <= mn_amp or mn_amp > 1:
mx_amp = .9
md_amp = (mn_amp + mx_amp) / 2
c_amps = np.random.triangular(mn_amp, md_amp, mx_amp, s_len)
return c_amps
... ...
def make_cp_sound(p_chds:np.ndarray[Any, np.dtype[np.float64]],
cp_rhy:np.ndarray[Any, np.dtype[np.str_]],
cp_durs:dict[str, int], c_amps: np.ndarray[np.float64],
c_prg:list[tuple[emn.Notes_scale, ChordFormula]],
nosnd:np.ndarray[np.float64]=[])-> np.ndarray[np.dtype[np.float64]]:
""" Using all the available data, write the sound array for the chord progression provided.
:param p_chds: array of arrays of 1 second sine waves for chords in progression
:param cp_rhy: array of arrays with note duration for chords in each 'bar'
:param cp_durs: dictionary of durations for each note type, e.g. "qtr": 1.0
:param c_amps: array of amplitude for each chord
:param c_prg: list of chords in the progression, debug only
:param no_snd: array of zeros of some length, space of no sound in array - not really used anymore
:return: array of arrays containing the sine waves for each element in the
sound of the progression (chord or silence)
"""
a_bars = []
cp_len = len(p_chds)
i_bar_ln = len(p_chds[0])
for j, cp_bar in enumerate(cp_rhy):
print(f" {cp_bar}")
for i, n_val in enumerate(cp_bar):
c_chd = i % cp_len
c_pos = j * cp_len + i
# if last chord, decide how to end with root chord if necessary
print(f" {i}: {n_val} -> {cp_durs.n_dur[n_val]} * {c_amps[c_pos]:.4f} -> {c_chd}: {c_prg[c_chd]}")
if c_pos == len(c_amps) - 1 and c_prg[c_chd] != c_prg[0]:
if cp_durs.n_dur[n_val] < 1:
aclen = int(i_bar_ln * cp_durs.n_dur[n_val])
a_bars.append(p_chds[0][:aclen] * c_amps[c_pos])
print(f" {n_val} -> {cp_durs.n_dur[n_val]} * {c_amps[c_pos]:.4f} -> {c_chd}: {c_prg[0]}")
elif cp_durs.n_dur[n_val] == 1:
n_hlf = cp_durs.n_hlf[n_val]
aclen = int(i_bar_ln * cp_durs.n_dur[n_hlf])
a_bars.append(p_chds[c_chd][:aclen] * c_amps[c_pos])
a_bars.append(p_chds[0][aclen:] * c_amps[c_pos])
print(f" {n_hlf} -> {cp_durs.n_dur[n_hlf]} * {c_amps[c_pos]:.4f} -> {c_chd}: {c_prg[c_chd]}")
print(f" {n_hlf} -> {cp_durs.n_dur[n_hlf]} * {c_amps[c_pos]:.4f} -> {c_chd}: {c_prg[0]}")
else:
n_hlf = cp_durs.n_hlf[n_val]
for _ in range(int(cp_durs.n_dur[n_hlf])):
a_bars.append(p_chds[c_chd] * c_amps[c_pos])
for _ in range(int(cp_durs.n_dur[n_hlf])):
a_bars.append(p_chds[0] * c_amps[c_pos])
print(f" {n_hlf} -> {cp_durs.n_dur[n_hlf]} * {c_amps[c_pos]:.4f} -> {c_chd}: {c_prg[c_chd]}")
print(f" {n_hlf} -> {cp_durs.n_dur[n_hlf]} * {c_amps[c_pos]:.4f} -> {c_chd}: {c_prg[0]}")
else:
if cp_durs.n_dur[n_val] < 1:
aclen = int(i_bar_ln * cp_durs.n_dur[n_val])
a_bars.append(p_chds[c_chd][:aclen] * c_amps[c_pos])
elif cp_durs.n_dur[n_val] > 1:
for _ in range(int(cp_durs.n_dur[n_val])):
a_bars.append(p_chds[c_chd] * c_amps[c_pos])
else:
a_bars.append(p_chds[c_chd] * c_amps[c_pos])
return a_bars
And the relevant portion of the test block now looks like this.
... ...
# let's get a rhythm and see if we can get the sound array sorted appropriately
cp_len = len(p_chds)
cp_rhy = make_chd_rhythm(cp_len, t_sig=t_sig, n_bars=6, tempo=60, retro=False)
cp_durs = Note_durations(t_sig, 1.0)
print(f"\t{cp_durs.n_dur}")
# print(f"\t{cp_rhy}")
# let's get a series of amplitutes to match the series of chord note values
# for now I don't want it too low or too high say 10-90% with a
# preference for the middle values
c_bars = len(cp_rhy)
c_amps = get_amplitudes(int(c_bars * cp_len))
a_bars = make_cp_sound(p_chds, cp_rhy, cp_durs, c_amps, nosnd, c_prg)
do_sav_wav = False
do_play_wav = False
if do_sav_wav:
... ...
And the terminal output for a sample run (not saving series of bars to file).
(base) PS R:\learn\e_music> uv run tst_chords.py
chord progression with overtones on each chord
selected key: C min_har (octave: 2)
scale notes: ['C2', 'D2', 'D#2', 'F2', 'G2', 'G#2', 'B2']
key chords: [('C', 'minor'), ('D', 'dim'), ('D#', 'aug'), ('F', 'minor'), ('G', 'major'), ('G#', 'major'), ('B', 'dim')]
chord progression (roman numerals): I-iii-iii-IV-vii
chord progression: [('C', 'minor'), ('D#', 'aug'), ('D#', 'aug'), ('F', 'minor'), ('B', 'dim')]
[
C minor -> ['C2', 'D#2', 'G2']
D# aug -> ['D#2', 'F#2', 'B2']
D# aug -> ['D#2', 'F#2', 'B2']
F minor -> ['F2', 'G#2', 'C3']
B dim -> ['B2', 'D3', 'F3']
]
multipliers: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
amplitudes: [0.4567, 0.2561, 0.1348, 0.0694, 0.0398, 0.0239, 0.0106, 0.0053, 0.0023, 0.001]
{'whl': 4.0, 'hlf': 2.0, 'qtr': 1.0, '8th': 0.5, '16th': 0.25, '32nd': 0.125}
['qtr', 'qtr', '8th', '8th', 'qtr']
0: qtr -> 1.0 * 0.4384 -> 0: ('C', 'minor')
1: qtr -> 1.0 * 0.6578 -> 1: ('D#', 'aug')
2: 8th -> 0.5 * 0.5379 -> 2: ('D#', 'aug')
3: 8th -> 0.5 * 0.3524 -> 3: ('F', 'minor')
4: qtr -> 1.0 * 0.5983 -> 4: ('B', 'dim')
['8th', 'qtr', '8th', 'qtr', 'qtr']
0: 8th -> 0.5 * 0.7416 -> 0: ('C', 'minor')
1: qtr -> 1.0 * 0.4331 -> 1: ('D#', 'aug')
2: 8th -> 0.5 * 0.4724 -> 2: ('D#', 'aug')
3: qtr -> 1.0 * 0.6276 -> 3: ('F', 'minor')
4: qtr -> 1.0 * 0.5313 -> 4: ('B', 'dim')
['8th', '8th', 'qtr', 'qtr', 'qtr']
0: 8th -> 0.5 * 0.5723 -> 0: ('C', 'minor')
1: 8th -> 0.5 * 0.7599 -> 1: ('D#', 'aug')
2: qtr -> 1.0 * 0.5848 -> 2: ('D#', 'aug')
3: qtr -> 1.0 * 0.5270 -> 3: ('F', 'minor')
4: qtr -> 1.0 * 0.6011 -> 4: ('B', 'dim')
['8th', '8th', '8th', 'hlf', '8th']
0: 8th -> 0.5 * 0.6546 -> 0: ('C', 'minor')
1: 8th -> 0.5 * 0.4374 -> 1: ('D#', 'aug')
2: 8th -> 0.5 * 0.5090 -> 2: ('D#', 'aug')
3: hlf -> 2.0 * 0.4761 -> 3: ('F', 'minor')
4: 8th -> 0.5 * 0.3192 -> 4: ('B', 'dim')
['hlf', 'hlf', 'qtr', 'qtr', 'hlf']
0: hlf -> 2.0 * 0.2310 -> 0: ('C', 'minor')
1: hlf -> 2.0 * 0.4589 -> 1: ('D#', 'aug')
2: qtr -> 1.0 * 0.6443 -> 2: ('D#', 'aug')
3: qtr -> 1.0 * 0.4784 -> 3: ('F', 'minor')
4: hlf -> 2.0 * 0.2509 -> 4: ('B', 'dim')
qtr -> 1.0 * 0.2509 -> 4: ('B', 'dim')
qtr -> 1.0 * 0.2509 -> 4: ('C', 'minor')
Post Finished
And, you know, I think that brings this post to a close. Lots of code/refactoring. Lots of bumbling about. And just over a week working on the draft of this post and the related code. Though I may yet add a sample audio file if I can generate one I more or less like.
May your bumbling be more efficient and productive than mine seems to have been.