Think this post will focus on the past rather than moving toward the future.

As I mentioned in closing the last post, I want to see if I can get my tst_chords module from the earlier virtual environment working in the one I used to get those wave to midi and midi to wave libraries working. Needed to revert to an older version of Python for the latter.

After that I want to take a bit of a look at the soundfont file. Don’t want to have to manually look up instrument progs or vice versa.

Finally, I want to refactor all the code from the last few posts. Clean things up. And, I’d like to use more command line parameters or add some sort of user interface. The latter is not something I am particularly good at. But might make life easier than a whole bunch of command line parameters.

Expect most of this will be of little interest to anyone but me. That said, I do use these posts as my personal documentation for each of my projects. So, I like to record what I do (no matter what) and, if necessary, why I did it.

Let’s start with getting the code for both virtual environments working in the one from the last post. I.E. Python 3.11.9 rather than 3.14.x. (Though I really wish I could be doing this in Python 3.14 or the pre-release 3.15.)

Add Chord Progression Code to Current UV Environament

I started by copying tst_chords.py and the ../pkgs directory from ./e_music to ./e_m_311. I also renamed main.py to main_em.py and copied it over just in case. I then added a couple libraries to the uv environment. Don’t think I any longer need matplotlib but…

PS R:\learn\e_m_311> uv add matplotlib sounddevice
Resolved 89 packages in 1.21s
Prepared 5 packages in 2.04s
         If the cache and target directories are on different filesystems, hardlinking may not be supported.
         If this is intentional, set `export UV_LINK_MODE=copy` or use `--link-mode=copy` to suppress this warning.
Installed 9 packages in 5.82s
 + contourpy==1.3.3
 + cycler==0.12.1
 + fonttools==4.63.0
 + kiwisolver==1.5.0
 + matplotlib==3.11.0
 + pillow==12.2.0
 + pyparsing==3.3.2
 + python-dateutil==2.9.0.post0
 + sounddevice==0.5.5

Then I tried running tst_chords.py.

PS R:\learn\e_m_311> uv run tst_chords.py
Traceback (most recent call last):
  File "R:\learn\e_m_311\tst_chords.py", line 230, in <module>
    def add_overtones_chord(c_wv:np.typing.NDArray[np.float], ot_seq:list[int],
                                                   ^^^^^^^^
  File "R:\learn\e_m_311\.venv\Lib\site-packages\numpy\__init__.py", line 338, in __getattr__
    raise AttributeError(__former_attrs__[attr])
AttributeError: module 'numpy' has no attribute 'float'.
`np.float` was a deprecated alias for the builtin `float`. To avoid this error in existing code, use `float` by itself. Doing this will not modify any behavior and is safe. If you specifically wanted the numpy scalar type, use `np.float64` here.
The aliases was originally deprecated in NumPy 1.20; for more details and guidance see the original release note at:
    https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations. Did you mean: 'cfloat'?

Looks like I have made some changes without yet testing them. Fixed things up and copied the corrected file back to e_music/. And things appeared to work after that.

(base) PS R:\learn\e_m_311> uv run tst_chords.py
chord progression with overtones on each chord

selected key: F min_nat (octave: 3)
  scale notes: ['F3', 'G3', 'G#3', 'A#3', 'C4', 'C#4', 'D#4']
  key chords: [('F', 'minor'), ('G', 'dim'), ('G#', 'major'), ('A#', 'minor'), ('C', 'minor'), ('C#', 'major'), ('D#', 'major')]
  chord progression (roman numerals): I-V-vi
  chord progression: [('F', 'minor'), ('C', 'minor'), ('C#', 'major')]
    [
      F minor -> ['F3', 'G#3', 'C4']
      C minor -> ['C3', 'D#3', 'G3']
      C# major -> ['C#3', 'F3', 'G#3']
    ]
        multipliers: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
        amplitudes: [0.3458, 0.1724, 0.1115, 0.0852, 0.0679, 0.0561, 0.0479, 0.0422, 0.0372, 0.0335]
        {'whl': 4.0, 'hlf': 2.0, 'qtr': 1.0, '8th': 0.5, '16th': 0.25, '32nd': 0.125}
  ['hlf', 'qtr', 'qtr']
    0: hlf -> 2.0 * 0.6491 -> 0: ('F', 'minor')
    1: qtr -> 1.0 * 0.2625 -> 1: ('C', 'minor')
    2: qtr -> 1.0 * 0.4820 -> 2: ('C#', 'major')
  ['qtr', 'qtr', 'hlf']
    0: qtr -> 1.0 * 0.7354 -> 0: ('F', 'minor')
    1: qtr -> 1.0 * 0.2541 -> 1: ('C', 'minor')
    2: hlf -> 2.0 * 0.3353 -> 2: ('C#', 'major')
  ['qtr', 'qtr', 'hlf']
    0: qtr -> 1.0 * 0.6487 -> 0: ('F', 'minor')
    1: qtr -> 1.0 * 0.5448 -> 1: ('C', 'minor')
    2: hlf -> 2.0 * 0.4888 -> 2: ('C#', 'major')
  ['qtr', 'qtr', 'hlf']
    0: qtr -> 1.0 * 0.3455 -> 0: ('F', 'minor')
    1: qtr -> 1.0 * 0.6512 -> 1: ('C', 'minor')
    2: hlf -> 2.0 * 0.2852 -> 2: ('C#', 'major')
  ['qtr', 'hlf', 'qtr']
    0: qtr -> 1.0 * 0.4365 -> 0: ('F', 'minor')
    1: hlf -> 2.0 * 0.4880 -> 1: ('C', 'minor')
    2: qtr -> 1.0 * 0.2994 -> 2: ('C#', 'major')
  ['qtr', 'qtr', 'hlf']
    0: qtr -> 1.0 * 0.6882 -> 0: ('F', 'minor')
    1: qtr -> 1.0 * 0.3874 -> 1: ('C', 'minor')
    2: hlf -> 2.0 * 0.2982 -> 2: ('C#', 'major')
         qtr -> 1.0 * 0.2982 -> 2: ('C#', 'major')
         qtr -> 1.0 * 0.2982 -> 2: ('F', 'minor')
writing to wave file: I-V-vi_4-4_even_sqr_o3_1.wav

Soundfont File

In order to see the prog value and the instrument assigned to it, I started FluidSynth at the command line, specifying a midi file. Once I had the fluidsynth prompt, I loaded the soundfont. And listed the instruments in the soundfont file.

PS R:\learn\e_m_311> fluidsynth -n img/I-vi-ii-vii-iii_4-4_even_saw_o4_1_basic_pitch_harpsichord.mid
FluidSynth runtime version 2.5.4
Copyright (C) 2000-2026 Peter Hanappe and others.
Distributed under the LGPL license.
SoundFont(R) is a registered trademark of Creative Technology Ltd.

Type 'help' for help topics.

> fluidsynth: warning: No preset found on channel 0 [bank=0 prog=6]
load R:/learn/e_music/rek/GeneralUser_GS_v2.0.3--doc_r6/GeneralUser-GS/GeneralUser-GS.sf2
loaded SoundFont has ID 1 and bankofs=0

### note at this point it played the midi file, use player_stop if don't want midi file played

> inst 1
000-000 Grand Piano
000-001 Bright Grand Piano
000-002 Electric Grand Piano
000-003 Honky-Tonk Piano
000-004 Tine Electric Piano
000-005 FM Electric Piano
000-006 Harpsichord
... ...
000-122 Seashore
000-123 Birds
000-124 Telephone 1
000-125 Helicopter
000-126 Applause
000-127 Gun Shot
001-038 Synth Bass 101
001-044 Tremolo Strings Mono
... ...
128-032 Jazz
128-040 Brush
128-048 Orchestral
128-056 SFX
128-127 CM-64/32L

I really don’t want to look up instruments in that list whenever I am thinking about selecting a different one to play the midi file or generate an audio file. So, I converted the instrument list to a CSV format (a couple regexs) and saved it to file, sf_gu_gs.csv. I then started a new module, sf_gu_gs.py, in which I created a class for handling that CSV file. The class, GS_soundfile, is at this point in an early stage of development.

# sf_gu_gs.py: provides class for accessing instruments in the GeneralUser GS soundfont.
#   https://www.schristiancollins.com/generaluser.php
# version 0.1: 2026.06.09, init code, rek, only look at bank 1 (000) instruments

import csv

SF_CSV = "sf_gu_gs.csv"

class GS_soundfile():
  def __init__(self):
    # load first bank from sound file into a dictionary. For now ignore other sound banks.
    self.sf_instr = {}
    with open(SF_CSV, mode='r') as infile:
      reader = csv.reader(infile)
      for rows in reader:
        if rows[0] == "000":
          self.sf_instr[int(rows[1])] = rows[2].lower()
        else:
          break
    # reverse the prog/instrument in new dictionary      
    self.sf_rev = dict((v,k) for k,v in self.sf_instr.items())


  def is_valid_instr(self, i_key:int) -> bool:
    return i_key in self.sf_instr


  def get_instr_nm(self, i_key:int) -> str:
    return self.sf_instr[i_key]
  

  def get_nm2key(self, i_nm:str) -> int:
    return self.sf_rev[i_nm.lower()]

I have a bit of an issue with the current situation. For example there is no piano instrument. Ditto for guitar, bass, saxaphone, etc. So somewhere down the road, I will write a method to get a random guitar if only guitar is asked for. That will have to wait.

And a bit of test code.

  sfgs = GS_soundfile()
  print(sfgs.sf_instr[40])
  sf_kys = list( sfgs.sf_instr.keys())
  for i in sf_kys[:5]:
    print(f"key: {i} -> {sfgs.sf_instr[i]}")
  for i in sf_kys[-5:]:
    print(f"key: {i} -> {sfgs.sf_instr[i]}")
  
  for i in ["grand piano", "trombone", "English Horn"]:
    print(f"\t{i} -> {sfgs.get_nm2key(i)}")
PS R:\learn\e_m_311> uv run main.py
... ...
violin
key: 0 -> grand piano
key: 1 -> bright grand piano
key: 2 -> electric grand piano
key: 3 -> honky
key: 4 -> tine electric piano
key: 123 -> birds
key: 124 -> telephone 1
key: 125 -> helicopter
key: 126 -> applause
key: 127 -> gun shot
        grand piano -> 0
        trombone -> 57
        English Horn -> 69

Okay, for now that seems to work. Though I am not yet using it in any of my other modules.

Major Refactor

Before I look at using that new soundfont class or adding to the command line parameters or creating a user interface of some sort, I am going to do some refactoring. I have way too many so called development/test modules. I want to move a lot of the functions in those modules into separate utility packages. I want to take some of the development code and put it into new functions in a suitable package. Then I want to look at refactoring the main module to use those packages and functions to generate audio and/or midi files containing chord progressions. I.E. get it doing the things we have been doing in those development/test modules.

I am not sure how many packages there will be. I am currently thinking one for basic wave handling, one for chords and chord progressions, and one for midi related functions. We will see if others might be needed to keep things tidier. Almost certainly there will be one or two more once we move on to trying to generate melodies to go along with the chord progressions.

My apologies for the mess of unwanted information and/or code that is likely to follow.

New github Repo

Wasn’t using github for this virtual environment. Expected I would go back to coding in the e_music environment. But that is a no go, so going create a new github repo and commit the current state of this project environment. Okay, created empty repo, no readme, .gitignore, license, etc.

PS R:\learn\e_m_311> git remote add origin git@github.com:xxx_311.git
PS R:\learn\e_m_311> git branch -M main
PS R:\learn\e_m_311> git st
On branch main
nothing to commit, working tree clean
PS R:\learn\e_m_311> git push -u origin main
Enumerating objects: 65, done.
Counting objects: 100% (65/65), done.
Delta compression using up to 16 threads
Compressing objects: 100% (63/63), done.
Writing objects: 100% (65/65), 195.95 KiB | 2.02 MiB/s, done.
Total 65 (delta 28), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (28/28), done.
To github.com:xxx_311.git
 * [new branch]      main -> main
Branch 'main' set up to track remote branch 'main' from 'origin'.

New Packages

Then I created some new packages and moved the code from the modules tst_chords.py and e_music/pgks/em_utils.py to those packages. Don’t want to repeat all that code here, but I will include the metadata from the new package files.

em_bars.py

# ./pkgs/em_bars.py: package to provide code to generate measures of music (chords progressions, melodies, etc.)
# ver 0.1: rek, 2026.06.15, move appropriate code from developmental module to this package

from enum import Enum
import functools, time, wave
from typing import Any, Union
import numpy as np
import numpy.typing as npt

from pkgs import em_waves as emw

""" Current Classes
  Note_durations
"""

""" Current Functions
  chk_bars(nts_t:np.typing.NDArray[np.str_], t_sig:tuple[int, int]=(4, 4), s_bt:float=1.0) -> bool
  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]
  ### need to rename the following one
  make_chd_rhythm(cp_len:int, t_sig:tuple[int, int]=(4, 4), n_bars:int=3, tempo:int=60, retro:bool=False) -> list[float]
"""

em_chords.py

# em_chords.py: package to provide code to generate chords or chord progressions
# ver 0.1: rek, 2026.06.15, move appropriate code from developmental module to this package

from enum import Enum
import functools, time, wave
from typing import Any, Union
import numpy as np
import numpy.typing as npt

from pkgs import em_notes as emn
from pkgs import em_waves as emw

""" Current Classes
  Chords_ok: Order of chords for a given scale type (see Scale_forms)
  Intervals(Enum): Standard intervals in western music in number of half-steps
  ChordFormula(Enum): Formulae for chords using Intervals
"""

""" Current Functions
  add_overtones_chord(c_wv:npt.NDArray[np.float_], ot_seq:list[int], ot_amps:list[float]) -> npt.NDArray[np.float_]
  convert_2_flat(nt:Notes_scale) -> Notes_scale
  convert_cprog(cp_rn:str, r_nt:Notes_scale, r_oct:int=4, p_qual:Chords_ok="major") -> list[tuple[Notes_scale, ChordFormula]]
  get_chord_array(c_nts:list[str], n_tm:float, n_vol:float=1,
                    s_rate:int=44100, do_norm:bool=True,
                    do_np16:bool=True) -> np.typing.NDArray[np.int16]:
  get_progression(n_prg:str) -> list[str]
  make_chord(nt:Notes_scale, c_qual:ChordFormula, r_oct:int=4) -> list[Notes_scale]
  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],
                  nosnd: np.ndarray[np.float64],
                  c_prg: list[tuple[Notes_scale, ChordFormula]])-> np.ndarray[np.dtype[np.float64]]
  make_progression(chords:list[tuple[str, str]], n_chd:int) -> list[tuple[str, str]]
  make_progression(n_chd:int) -> str
  scale_chords(scale:list[Notes_scale], s_mode:Scale_forms="major") -> list[tuple[str, str]]
"""

""" Globals
  nbr_python: type hint
  # rng (np random gnerator)
  common_prog: dict of common progressions
"""

em_notes.py

# pkgs/em_notes.py: a package to provide related functionality for generating and/or playing notes and scales.
#
# ver 0.1: rek, 2026.06.15, move appropriate code from developmental module to this package

from enum import Enum
import functools, time, wave
from typing import Any, Union
import numpy as np

from pkgs import em_waves as emw

""" Current Classes
  Notes_scale: The twelve standard notes in a piano octave.
  Scale_forms: Currently acceptable scale modes and their formulae
"""

""" Current Functions
  apply_overtones(n_f:int, n_tm:float, n_vl:float, ot_seq:list[int], ot_amps:list[float], s_rt:int=44100) -> np.typing.NDArray[np.float_]
  convert_2_flat(nt:Notes_scale) -> Notes_scale
  get_2_oct_piano(r_nt:Notes_scale, r_oct:int=4) -> list[Notes_scale]
  get_half_tone(nt_sym:Notes_scale) -> int
  get_nt_sym(h_tone:int) -> Notes_scale
  get_rand_key() -> tuple[str, str]
  get_scale_4_note(r_nt:Notes_scale, r_oct:int=4, s_mode:Scale_forms="major") -> list[Notes_scale]
"""

""" Globals
  nbr_python: type hint
  C_SCALE (list of keys in one piano octave)
"""

em_wave.py

# pkgs/em_waves.py: 

import functools
from typing import Union
import numpy as np

""" Current Classes
"""

""" Current Functions
  add_overtones(n_f:int, n_tm:float, n_vl:float=1, s_rt:int=44100, ot_typ:str="seq",
                  h_typ:str="saw", ot_nb:int=10) -> np.typing.NDArray[np.int16]
  apply_amplitude(np_wv:np.typing.NDArray[np.number[int | float]], n_amp:float,
                    do_typ:bool=True) -> np.typing.NDArray[np.number[int | float] | np.int16
  get_amplitudes(s_len:int, mn_amp:float=.1, mx_amp:float=.9) -> np.ndarray[np.float64]
  get_note_freq(sym: str) -> float
  get_ot_amps(f_amp:int, ot_typ:str, h_typ:str="saw", n_ot:int=10) -> list[float]
  get_ot_mults(ot_typ:str, n_ot:int=10) -> list[int]
  get_sine_wave(n_fq: float, n_tm: float, n_vol:float=1,
                  s_rate:int=44100) -> np.typing.NDArray[np.number[int | float]]
  make_note_seq(f_seq:list[float], t_seq:list[float], v_seq:list[float],
                   ot:str, ht:str, n_h:int=10, s_rt:int=44100,
                   nosnd:np.typing.NDArray[np.number[int | float]]=[]) -> np.typing.NDArray[np.int16]
  normalize_wave(np_wv:np.typing.NDArray[np.number[int | float]],
                   do_typ:bool=False) -> np.typing.NDArray[np.number[int | float]]
"""

""" Globals
  nbr_python: type hint
  rng (np random gnerator)
"""

Code Not Yet in Function or Package

Okay, I now plan to look at the code in e_music/main.py and the local tst_chords.py and see what should likely go into package functions. Expect that will strain my mental faculties, but let’s give it a go. I will start with the tst_chords.py module as it is the one I have been working on of late and really has stuff I expect to use in future. Not so sure the latter is true with respect to the earlier e_music/main.py.

do_mk_play_cprog

The only test block that I can see needs to be kept, converted into a useable function, or two, is the if do_mk_play_cprog: block. The focus of the previous couple of posts. Not sure but expect in the end it may be more than one function. And at least the primary one will be put in the em_chords package. And, I will definitely separate the make, play and save parts.

Added the following to the em_chords package.

... ...
def get_cp_size() -> int:
  """ Get random size for chord progression.

    returns: number of chords for progression
  """
  avl_nc = [3, 4, 5, 6, 7, 8]
  pi_nc = [6, 6, 5, 1, 1, 1]
  pi_tot = sum(pi_nc)
  p_nc = [p/pi_tot for p in pi_nc]
  n_chds = emw.rng.choice(avl_nc, p=p_nc)

  return n_chds
... ...
def make_chords_overtones(chrds:list[emn.Notes_scale], ot_mlts:list[int], ot_amps:list[int],
                          c_dur:float=1.0, c_amp:float=1.0, s_rate:int=44100,
                          do_norm:bool=False, do_np16:bool=False) -> npt.NDArray[np.number[int | float]]:
  """ Generate array of chord wave forms, apply overtones as specified
  
    :param chrds: list of chords in progression, str, note from C-SCALE plus octave number
    :param ot_mlts: list of overtone multipliers, of suitable length
    :param ot_amps: list of overtone amplitudes, of suitable length
    :param c_dur: default duration for each chord
    :param c_amp: default amplitude for each chord
    :param s_rate: sampling rate to use when generating since wave for chord
    :param do_norn: normalize chord array
    :param do_np16: convert chord array values to np.int16

    :returns: array of individual chord sine wave arrays
  """
  p_chds = []
  for i, chd in enumerate(chrds):
    c_chd = get_chord_array(chd, c_dur, c_vol=c_amp,
                s_rate=s_rate, do_norm=False, do_np16=False)
    n_elms = len(c_chd)
    chd_ots = [c_chd]
    for i, ot_mlt in enumerate(ot_mlts):
      # generate multiplied frequency wave form
      t_f = c_chd[::ot_mlt].copy()       # explicit copy, O(n)
      n_frq = np.tile(t_f, ot_mlt)
      n_frq = n_frq[:n_elms]
      # apply overtone amplitude and save
      chd_ots.append(n_frq * ot_amps[i])
      # generate wave with all overtones
    chd_ot = functools.reduce(np.add, chd_ots)
    p_chds.append(chd_ot)
    # no longer do this, adds clicking sound to wav file
    # if i < len(p_chrds) - 1:
    #   p_chds.append(nosnd)
  p_chds = np.array(p_chds)
  return p_chds
... ...
def make_cp_chords(s_rnt:emn.Notes_scale, s_frm:emn.Scale_forms, r_oct:int=4,
                      t_sig:tuple[int, int]=(4, 4), n_chds:int=3,
                      dbg:bool=False) -> list[emn.Notes_scale]:
  """ Generate list of chords for a random chord progression of some number of chords.

    :param s_rnt: root note for scale (str)
    :param s_frm: scale form (str)
    :param r_oct: root (piano) octave for first note of scale
    :param t_sig: time signature for progression/measures
    :param n_chds: number of chords in progression
    :param dbg: print debug messages to terminal window

    :returns: tuple, string with roman numerals for chord progression separated by a dash
        list chords in progression in playing order and
        list of notes for each chord in progression, str, note from C-SCALE plus octave number
  """

  print(f"\nselected key: {s_rnt} {s_frm} (octave: {r_oct})")
  # get the chords for this scale
  s_nts = emn.get_scale_4_note(s_rnt, r_oct=r_oct, s_mode=s_frm)
  if dbg:
    print(f"  scale notes: {s_nts}")
  k_chds = scale_chords(s_nts, s_frm)
  if dbg:
    print(f"  key chords: {k_chds}")

  # get a random progression
  rn_prg = make_progression(n_chds)
  if dbg:
    print(f"  chord progression (roman numerals): {rn_prg}")
  c_prg = convert_cprog(rn_prg, s_rnt, r_oct=r_oct, p_qual=s_frm)
  if dbg:
    print(f"  chord progression: {c_prg}")
  # generate chords and add overtones
  p_chrds = []
  for c_nt, c_frm in c_prg:
    c_nts = make_chord(c_nt, c_frm, r_oct=r_oct)
    p_chrds.append(c_nts)
  if dbg:
    print(f"    [")
    for i, chd in enumerate(p_chrds):
      print(f"      {c_prg[i][0]} {c_prg[i][1]} -> {chd}")
    print(f"    ]")
  
  return rn_prg, c_prg, p_chrds

Test New Functions and Packages

And, a quick test, in a new main.py, of a couple of the above functions—one step at a time.

import numpy as np

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


def main():

  do_mk_cprog = True

  if do_mk_cprog:
    # generate a random chord progression
    t_sig = (emw.rng.choice([3,4]), 4)
    # get a key for the chord progression
    s_rnt, s_frm = emn.get_rand_key()
    r_oct = emw.rng.choice([1, 2, 3, 4])
    n_chds = emc.get_cp_size()
    print(f"\nselected key: {s_rnt} {s_frm} (octave: {r_oct})")
    # generate a random chord progression
    cp_chrds = emc.make_cp_chords(s_rnt, s_frm, r_oct, t_sig, n_chds, dbg=True)


if __name__ == "__main__":
  main()

A day or two later I realized I have not allowed for using a known chord progression from the list we created in a prior post.

And in the terminal the following output was displayed.

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

selected key: A min_nat (octave: 2)
  scale notes: ['A2', 'B2', 'C3', 'D3', 'E3', 'F3', 'G3']
  key chords: [('A', 'minor'), ('B', 'dim'), ('C', 'major'), ('D', 'minor'), ('E', 'minor'), ('F', 'major'), ('G', 'major')]
  chord progression (roman numerals): I-iii-vi
  chord progression: [('A', 'minor'), ('C', 'major'), ('F', 'major')]
    [
      A minor -> ['A2', 'C3', 'E3']
      C major -> ['C2', 'E2', 'G2']
      F major -> ['F2', 'A2', 'C3']
    ]

That appeared to work as expected. Will run more tests eventually. But, I now have a possible issue with the code. Seems to me if the A minor chord started at A2, that the C major should be starting at C3 and F3 for the last chord. That would be the sequence from the notes in the specified scale. Not sure how much, if any, difference it makes, but sort of makes sense to me. Will see how much work it will take to make that happen.

But I will worry about that possible problem later. Let’s write some code to save a chord progression to a wave file and then convert to a midi file.

... ...
    # let's generate base chords with overtones
    sample_rate = 44100
    c_dur, c_amp = 1.0, 1.0
    p_tempo = 120
    s_bt = 60 / p_tempo
    n_bars = 6
    ot_s = emw.rng.choice(["even", "odd", "seq"])
    ot_w = emw.rng.choice(["half", "saw", "sqr", "tri"])
    n_ot = 10

    # for now use same multipliers and frequencies for overtones
    ot_mlts = emw.get_ot_mults(ot_s, n_ot=n_ot)
    ot_amps = emw.get_ot_amps(c_amp, ot_s, h_typ=ot_w, n_ot=n_ot)
    print(f"\tmultipliers: {ot_mlts}\n\tamplitudes: {[round(oa, 4) for oa in ot_amps]}")

    """make_chords_overtones(chrds:list[emn.Notes_scale], ot_mlts:list[int], ot_amps:list[int],
                          c_dur:float=1.0, c_amp:float=1.0, s_rate:int=44100,
                          do_norm:bool=False, do_np16:bool=False) -> npt.NDArray[np.number[int | float]]"""
    p_chds = emc.make_chords_overtones(cp_base, ot_mlts, ot_amps,
                          c_dur=c_dur, c_amp=c_amp, s_rate=sample_rate,
                          do_norm=False, do_np16=False)
    
    if True:
      snd = np.hstack(p_chds)
      snd = emw.normalize_wave(snd, do_typ=True)
      sd.play(snd)
      sd.wait()

And for a couple tests, I definitely got the required number of chords playing out my speakers. Not particularly pretty, but…

Well, let’s get our bars generated and play the result.

... ...
    # let's get a rhythm and see if we can get the sound array sorted appropriately
    cp_len = len(p_chds)
    cp_rhy = emb.make_chd_rhythm(cp_len, t_sig=t_sig, n_bars=n_bars, tempo=p_tempo, retro=False)
    cp_durs = emb.Note_durations(t_sig, s_bt)
    print(f"\t{cp_durs.n_dur}")
    c_amps = emw.get_amplitudes(int(n_bars * cp_len))

    a_bars = emc.make_cp_sound(p_chds, cp_rhy, cp_durs, c_amps, c_prg, nosnd=[])

    if True:
      snd = np.hstack(a_bars)
      snd = emw.normalize_wave(snd, do_typ=True)
      sd.play(snd)
      sd.wait()

And, that appears to work. But I was having issues with very long progressions. Run time extreme, I cntl-Ced in every case. Will need to do some investigating down the road.

That said, let’s write our progression to wave file.

... ...
    do_sav_wav = True
    do_play_wav = False

    if do_sav_wav or do_play_wav:
      snd = np.hstack(a_bars)
      snd = emw.normalize_wave(snd, do_typ=True)

    if do_sav_wav:
      # snd = np.hstack(p_chds)
      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()
(base) PS R:\learn\e_m_311> uv run main.py

selected key: G min_nat (octave: 3)
  scale notes: ['G3', 'A3', 'A#3', 'C4', 'D4', 'D#4', 'F4']
  key chords: [('G', 'minor'), ('A', 'dim'), ('A#', 'major'), ('C', 'minor'), ('D', 'minor'), ('D#', 'major'), ('F', 'major')]
  chord progression (roman numerals): I-vii-IV-V
  chord progression: [('G', 'minor'), ('F', 'major'), ('C', 'minor'), ('D', 'minor')]
    [
      G minor -> ['G3', 'A#3', 'D4']
      F major -> ['F3', 'A3', 'C4']
      C minor -> ['C3', 'D#3', 'G3']
      D minor -> ['D3', 'F3', 'A3']
    ]
        multipliers: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
        amplitudes: [0.2506, 0.1658, 0.1201, 0.0983, 0.0841, 0.0707, 0.062, 0.0548, 0.0494, 0.0443]
        {'whl': 2.0, 'hlf': 1.0, 'qtr': 0.5, '8th': 0.25, '16th': 0.125, '32nd': 0.0625}

calling make_chd_rhythm
make_chd_rhythm done: 0.0156

calling make_cp_sound
  ['qtr', 'hlf', 'hlf', 'qtr']
    0: qtr -> 0.5 * 0.5507 -> 0: ('G', 'minor')
    1: hlf -> 1.0 * 0.5598 -> 1: ('F', 'major')
    2: hlf -> 1.0 * 0.4116 -> 2: ('C', 'minor')
    3: qtr -> 0.5 * 0.4751 -> 3: ('D', 'minor')
  ['hlf', 'qtr', 'hlf', 'qtr']
    0: hlf -> 1.0 * 0.4892 -> 0: ('G', 'minor')
    1: qtr -> 0.5 * 0.5476 -> 1: ('F', 'major')
    2: hlf -> 1.0 * 0.6914 -> 2: ('C', 'minor')
    3: qtr -> 0.5 * 0.6629 -> 3: ('D', 'minor')
  ['qtr', '8th', '8th', 'qtr']
    0: qtr -> 0.5 * 0.4789 -> 0: ('G', 'minor')
    1: 8th -> 0.25 * 0.5982 -> 1: ('F', 'major')
    2: 8th -> 0.25 * 0.2760 -> 2: ('C', 'minor')
    3: qtr -> 0.5 * 0.7777 -> 3: ('D', 'minor')
  ['qtr', 'qtr', '8th', '8th']
    0: qtr -> 0.5 * 0.8387 -> 0: ('G', 'minor')
    1: qtr -> 0.5 * 0.5354 -> 1: ('F', 'major')
    2: 8th -> 0.25 * 0.6166 -> 2: ('C', 'minor')
    3: 8th -> 0.25 * 0.4512 -> 3: ('D', 'minor')
         8th -> 0.25 * 0.4512 -> 3: ('G', 'minor')
make_c_sound done: 0.0000
writing to wave file: I-vii-IV-V_3-4_seq_sqr_o3_1.wav

And, it appears to work as intended. And did so for a number more tests (playing but not saving to file).

Fini, Je Crois

Still lots more to go; but I think this post is plenty long enough and has taken me a while to get to this stage. Draft or otherwise.

For the near future:

  • I would like to sort out that concern regarding the order of octaves for chords within a chord progression.
  • Refactor to allow selecting known chord progressions (something I forgot about during this current refactor).
  • Add command line parameters to control the initial set of actions the main module can take. Not quite ready to tackle a user interface—simple or otherwise.
  • Get the conversion of wave to midi working. And, allow for managing the instrument assigned to each track. And, of course, once that is done, generating an updated wave audio file. Or perhaps a FLAC file. The latter will result in better file sizes once these sound tracks get longer and more complicated.

Long term, I want to get to generating a random melody sound track to go with a chord progression. Not sure how much that will complicate saving to and converting between music and audio file formats. But way too early to worry about that.