Looking back at the list in the closing section of the last post, I think I will start with getting some command line parameters working. Each of the others on the list will likely take a bit more effort. That said, I have been wrong with this kind of assumption all too often in the past.

Add Command Line Parameters

I am currently thinking that I’d like parameters to:

  • determine the action or actions to be taken
  • specify any additional values required to complete any of those actions
  • confuse things by adding some options (likely also optional parameters)

The actions I am currently thinking of are:

  • create a chord progression for a random key and time signature, potential options:
    • use a known chord progression instead of creating a random progression
    • if random progression, the number of chords
    • specify key and form (e.g. F# natural minor)
    • number of bars for the music track
    • whether or not to save to wave format file
    • wave file name/path
    • whether or not to convert to midi
    • midi file name/path
    • whether or not to play the array of waves
  • convert a wave file to a midi file, potential required or optional parameters:
    • wave file name/path
    • instrument to use for each track (optional)
    • midi file name/path (optional, will likely have a default in the code)
  • change instruments in a given midi file, potential required or optional parameters:
    • midi file name/path
    • new instrument per track or, possibly, which existing instrument to change to which new instrument
    • new midi file name/path (optional, will likely have a default in the code)
  • generate a audio file from a midi file, potential required or optional parameters:
    • midi file name/path
    • audio file type (e.g. wave or flac, likely optional code will default to one or the other)
    • audio file name/path (optional, will likely have a default in the code)

I am sure I have missed some or many actions and related required or optional parameters. But this looks like a good start. Though likely to quickly get rather complicated.

I believe I will write individual functions to check the additional required or optional parameters for each activity. Probably a lot tidier that way.

And, I am beginning to consider the idea of saving the key meta-information that would be required to generate a particular chord progression whenever desired. Maybe as a way to avoid always needing to save to wave.

That said, let’s get started on a function to create the argparse object/instantiation for our command line parameters. And, a new package, em_ui. Bit lengthy. And, I did have to add a couple of global variables to the em_chords and em_bars packages—though their values are certainly subject to change.

I am also using custom validation types to ensure the number of chords in the progression or bars in the track fall within the values specified in the appropriate package. Something new for me. As is the metavar parameter in the add_argument method. Good to learn new things and how to provide better help and error messages. But am wondering if can use a single validation function for both arguments. Will need to test that out down the road—if I remember to do so.

I used a dictionary for the list of actions for a couple of reasons. One, I wanted to generate the available options for the parameter’s help message programmatically. Secondly, I am thinking about trying to add code that would allow a user (me!) to get more detailed help for any parameter.

And, it was clear some of the parameters could be used for a number of different module actions/processes. So, I will definitely be writing functions to check that for a specified action any necessary additional parameters have also been provided. The function will also determine which optional parameters have been submitted. Will need to pass the information back to the calling module in some fashion. Will sort that out next.

get_parser()

# em_bars.py

""" Globals
  BR_MN: minimum number of bars for a track
  BR_MX: maximum number of bars for a trak
"""

BR_MN, BR_MX = 6, 50  # for now

# em_chords.py
... ...
""" Globals
  nbr_python: type hint
  # rng (np random gnerator)
  common_prog: dict of common progressions
  CP_MN: minimum length for chord progression
  CP_MX: maximum length for chord progression
"""

CP_MN, CP_MX = 3, 7

# em_ui.py
# pkgs/em_ui.py: a package to provide functionality related to user interface. E.G. command line arguments
#
# ver 0.1: rek, 2026.06.20, init, start with function to generat arg parser

# import sys
# # Insert the absolute or relative folder path to the search directory
# sys.path.insert(0, "../") 

import argparse
from pkgs import em_chords as emc

""" Current Classes """

""" Current Functions
  get_parser() -> argparse.ArgumentParser
"""

""" Globals
"""

def get_parser() -> argparse.ArgumentParser:
  """Create and return parser for generating random music. Whether that be some melody like
     music track or a chord progression or...
  """
  # dictionary of potential actions
  m_2do = {"m_cp": "generate a chord progression",
           "i_chg": "change the instrument for one or more channels/tracks in the specified midi file",
           "i_shw": "show the instrument for each channel/track in the specified midi file",
           "m2w": "export midi to wave file",
           "play": "play sound wave array",
           "w2m_pi": "wave to midi, specific instrument(s)",
           "w2m_ps": "wave to midi using predict_and_save function",
           }
  ky_2do = list(m_2do.keys())
  s_2do = (", ").join(ky_2do)
  # instantiate and set up command paramter parser
  parser = argparse.ArgumentParser()
  parser.add_argument("-wd", "--whatdo", required=True, help=f"Supply code for action to take: {s_2do}")
  # additonal/optional parameters
  # may apply to more than one actions
  parser.add_argument("-fpi", "--filepath_in", help="Supply name/path for input file (.wav, .mid) to be used in code")
  parser.add_argument("-fpw", "--filepath_wv", help="Supply new name/path for .wav file to be output/saved")
  parser.add_argument("-fpm", "--filepath_mid", help="Supply new name/path for .mid file to be output/saved")
  parser.add_argument("-mtk", "--track_key", help=f"Key and form for sound track to be generated, e.g. F# min-nat")
  parser.add_argument("-nbr", "--nbr_bars", type=nbars_type, metavar=f"[{emb.BR_MN}-{emb.BR_MX}]",
                      help=f"The number of bars of music to be generated, {emb.BR_MN}-{emb.BR_MX}")
  parser.add_argument("-ply", "--play_track", action="store_true",  help="If present play the wave array of a generated sound track")
  parser.add_argument("-nsw", "--no_save_wv", action="store_true", help="If present do not save sound track to .wav file")
  parser.add_argument("-nsm", "--no_save_mid", action="store_true", help="If present do not convert sound track wave file to midi")
  # for m_cp
  parser.add_argument("-cpt", "--cp_type", help=f"Chord progression type: specific name or 'random'")
  parser.add_argument("-cpn", "--cp_nbr", type=nchds_type, metavar=f"[{emc.CP_MN}-{emc.CP_MX}]",
                      help=f"Number of chords in random chord progression, {emc.CP_MN}-{emc.CP_MX}")
  # for i_chg
  parser.add_argument("-ift", "--instr", nargs="+", type=int, action="append",
                      help="Supply the new instrument for a given track, can be used multiple times")
  return(parser)


def nbars_type(a_val):
    """Validates if the input integer falls within the default range."""
    i_val = int(a_val)
    if i_val < emb.BR_MN or i_val > emb.BR_MX:
        raise argparse.ArgumentTypeError(f"must be an integer between {emb.BR_MN} and {emb.BR_MX} (given {i_val})")
    return i_val


def nchds_type(a_val):
    """ Validates if the input integer falls within the default range."""
    i_val = int(a_val)
    if i_val < emc.CP_MN or i_val > emc.CP_MX:
        raise argparse.ArgumentTypeError(f"must be an integer between {emc.CP_MN} and {emc.CP_MX} (given {i_val})")
    return i_val

And, in main.py I added some code to get the command line parameters. And some temporary test code to quickly check that things appear to be working correctly.

... ...
from pkgs import em_ui as emui
... ...
def main():

  cl_parse = emui.get_parser()
  # uv run main.py -wd m_cp -mtk 'F# major' -nbr 9 -cpt random -ply -svw -cpn 4
  cl_args = cl_parse.parse_args()
  print(f"\ncli args: {cl_args}")
  exit(0)

And in the terminal I got the following when running that command in the comment above.

(base) PS R:\learn\e_m_311> uv run main.py -wd m_cp -mtk 'F# major' -nbr 9 -cpt random -ply -svw -cpn 4

cli args: Namespace(whatdo='m_cp', filepath_in=None, filepath_wv=None, filepath_mid=None, track_key='F# major', nbr_bars=9, play_track=True, save_wv=True, save_mid=False, cp_type='random', cp_nbr=4, instr_f_2=None)

So, though not a definitive set of tests, I think we can safely move on.

Modify File Save Arguments

And as I was thinking about the next function and the related activity, generating a chord progression track of some length, I began to think that saving to wave and, perhaps, midi should be the default. And, not saving one or both should require the user to say so. As such, I have modified a couple of argument properties.

... ...
  # parser.add_argument("-svw", "--save_wv", action="store_true", help="If present save sound track to .wav file")
  # parser.add_argument("-svm", "--save_mid", action="store_true", help="If present convert sound track wave file to midi")
  parser.add_argument("-nsw", "--no_save_wv", action="store_false", help="If present do not save sound track to .wav file")
  parser.add_argument("-nsm", "--no_save_mid", action="store_false", help="If present do not convert sound track wave file to midi")

get_args_do_m_cp()

Having a bit of trouble with the name for this function. I looked at chk_action_m_cp, parse_action_m_cp and args_action_m_cp before settling on the name above. The function will also check that any required args are present along with looking for any optional ones. There will be similar functions for other requested module actions/processes. Trying to figure out how to handle default values for any argument.

Not really sure how to go about returning the argument values. Tuple? List? Dictionary? Will sort that out when necessary and refactor if needed.

Default Values for Arguments

I think I need to sort this out before going any further. I expect some of these defaults should be in various packages depending on their use in the code. But for now, I am going to create dictionaries for each task with a list of potential arguments and values. A modified copy of that dictionarly will be returned to the calling program.

To make life somewhat easier, I will have one dictionary for all possible arguments. The keys will be the long argument name.

This is, in fact, rather confusing to me. Should I just check things out and let the calling program use the argparse parameter object. But, I am thinking that for any unfilled, optional arguments, I will generate a value and add it to the dictionary. So, the dictionary will have all the options the module needs to run the activity. Will see how that goes once I start refactoring the code to make use of those values.

For now the dictionary is in the em_ui.py package. Seemed to make the most sense to me at present. Is the list complete? Don’t yet know.

... ...
""" Globals
  A_DEF: list of default value for all possible command line arguments
  }
"""

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,
    "instr": [],
  }

While continuing work on the function, I realized there were a number of values I hadn’t dealt with in the above list of defaults: time signature, number of overtones to use, and the root octave. I will for now ignore those.

Back to the Function

It is rather lengthy and that is because I check out each element in the dictionary of default argument values. Then update each one as necessary. Will likely need to rethink how I am doing this down the road. Perhaps small functions for each case? Especially those cases that will be checked in each activity’s argument parsing function. At the moment that is for the future to sort out.

def get_args_do_m_cp(cli_args:argparse.Namespace) -> tuple[bool, dict[str, Union[str, int, bool]]]:
  """ Parse the passed command line arguments to make sure that any required args are
      present and any optional arguments that have values are recorded.

    :param cli_args: argparse container with the current values of the command line parameters

    :return: boolean indicating whether or not arguments are good,
        a dictionary containing the value for each possible argument
  """
  is_ok = True
  t_args = A_DEF.copy()
  t_args["whatdo"] = "m_cp"
  for a_key in t_args.keys():
    a_val = getattr(cli_args, a_key)
    if a_key == "whatdo":
      continue
    match a_key:
      case "filepath_in":
        if a_val is None:
          # no filepath specified is okay for this activity
          continue
        # else chk path exists
        f_pth = Path(a_key)
        if f_pth.exists():
          t_args[a_key] = a_val
        else:
          # if not flage error
          t_args[a_key] = f"bad path: {a_val}"
          is_ok = False
      case "filepath_wv" | "filepath_mid":
        if a_val is None:
          # no filepath specified is okay for this activity
          continue
        # otherwise check that any parent directories exist
        f_dir = a_val.rsplit("/", 1)
        d_pth = Path(f_dir[0])
        if d_pth.exists():
          t_args[a_key] = a_val
        else:
          t_args[a_key] = f"bad path dir: {a_val}"
      case "track_key":
        if a_val is None:
          t_args[a_key] = emn.get_rand_key()
        else:
          nkey, s_form = a_val.split(" ")
          # validate both
          t_ok = nkey in emn.C_SCALE
          t_ok &= s_form in emn.Scale_forms().s_forms.keys()
          if t_ok:
            t_args[a_key] = (nkey, s_form)
          else:
            t_args[a_key] = f"bad scale or form: {a_val}"
            is_ok = False
      case "nbr_bars":
        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
      case "play_track" | "no_save_wv" | "no_save_mid":
        t_args[a_key] = a_val
      case "cp_type":
        if a_val is None or a_val[:3] == "ran":
          t_args[a_key] = "random"
        # elif a_val != "random":
        else:
          if a_val in emc.common_prog.keys():
            t_args[a_key] = a_val
          else:
            t_args[a_key] = f"bad common progression name: {a_val}"
            is_ok = False
      case "cp_nbr":
        if a_val is None:
          t_args[a_key] = emc.get_cp_size()
        else:
          t_args[a_key] = a_val
      case "instr":
        # doesn't apply here, ignore?
        ...

  return is_ok, t_args

And a wee test in main.py.

def main():

  cl_parse = emui.get_parser()
  cl_args = cl_parse.parse_args()
  print(f"\ncli args: {cl_args}")

  args_ok, args = emui.get_args_do_m_cp(cl_args)
  print(f"\ncommand line args are ok: {args_ok}")
  for ky, val in args.items():
    print(f"{ky}: {val}")

  exit(0)

Here’s the terminal output for a couple test cases.

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

cli args: Namespace(whatdo='m_cp', filepath_in=None, filepath_wv=None, filepath_mid=None, track_key=None, nbr_bars=None, play_track=False, no_save_wv=False, no_save_mid=False, cp_type=None, cp_nbr=None, instr=None)

command line args are ok: True
whatdo: m_cp
filepath_in:
filepath_wv:
filepath_mid:
track_key: ('G', 'min_nat')
nbr_bars: 6
play_track: False
no_save_wv: False
no_save_mid: False
cp_type: random
cp_nbr: 7
instr: []

PS R:\learn\e_m_311> uv run main.py -wd m_cp -nbr 20 -fpw tst/audio/test_1.wv -cpt ballad

cli args: Namespace(whatdo='m_cp', filepath_in=None, filepath_wv='tst/audio/test_1.wv', filepath_mid=None, track_key=None, nbr_bars=20, play_track=False, no_save_wv=False, no_save_mid=False, cp_type='ballad', cp_nbr=None, instr=None)

command line args are ok: False
whatdo: m_cp
filepath_in:
filepath_wv: bad path: tst/audio/test_1.wv
filepath_mid:
track_key: ('E', 'major')
nbr_bars: 20
play_track: False
no_save_wv: False
no_save_mid: False
cp_type: ballad
cp_nbr: 4
instr: []

PS R:\learn\e_m_311> uv run main.py -wd m_cp -mtk 'H# major' -nbr 10 -cpt do-wop -ply

cli args: Namespace(whatdo='m_cp', filepath_in=None, filepath_wv=None, filepath_mid=None, track_key='H# major', nbr_bars=10, play_track=True, no_save_wv=False, no_save_mid=False, cp_type='do-wop', cp_nbr=None, instr=None)

command line args are ok: False
whatdo: m_cp
filepath_in:
filepath_wv:
filepath_mid:
track_key: bad scale or form: H# major
nbr_bars: 10
play_track: True
no_save_wv: False
no_save_mid: False
cp_type: bad common progression name: do-wop
cp_nbr: 3
instr: []

I think, for now, that is it for this function. Though I am sure there will be some refactoring down the road. Let’s move on to refactoring main.py to actually use the values returned by this function.

Refactor main.py

No sense having that function do all that work if we don’t use it. So, let’s give that a go.

I will start by including a block of code that assigns the values returned by get_args_do_m_cp to the appropriate variables in main.py: in the main function and, more specifically, the if do_mk_cprog: block. Then deal with any variables for which the function does not yet provide values. And, finally, give it a go.

... ...
def main():

  sample_rate = 44100
  aud_mid_dir = "img"

  cl_parse = emui.get_parser()
  cl_args = cl_parse.parse_args()
  args_ok, args = emui.get_args_do_m_cp(cl_args)
  if not args_ok:
    print("\nOne or more supplied arguments are in error, please try again.")
    for ky, val in args.items():
      if isinstance(val, str) and val[:3] == "bad":
        print(f"\t{ky}: {val}")
    exit(1)

  do_mk_cprog = args["whatdo"] == "m_cp"

  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 = args["track_key"]
    r_oct = emw.rng.choice([1, 2, 3, 4])
    n_chds = args["cp_nbr"]
    c_dur, c_amp = 1.0, 1.0
    p_tempo = 120
    s_bt = 60 / p_tempo
    n_bars = args["nbr_bars"]
    ot_s = emw.rng.choice(["even", "odd", "seq"])
    ot_w = emw.rng.choice(["half", "saw", "sqr", "tri"])
    n_ot = 10
    do_sav_wav = not args["no_save_wv"]
    do_play_wav = not args["no_save_mid"]
... ...
    if do_sav_wav:
      if args["filepath_wv"] is None:
        w_fl_nm = f"{aud_mid_dir}/{rn_prg}_{t_sig[0]}-{t_sig[1]}_{ot_s}_{ot_w}_o{r_oct}_1.wav"
      else:
        w_fl_nm = args["filepath_wv"]
      # Open a WAV file
      with wave.open(w_fl_nm, 'w') as wav_file:

And a test with some bad arguments.

(base) PS R:\learn\e_m_311> uv run main.py -wd m_cp -mtk 'H# major' -nbr 10 -nsw -cpn 6 -cpt do-wop -ply -fpw audio/test.wav

One or more supplied arguments are in error, please try again.
        filepath_wv: bad path dir: audio/test.wav
        track_key: bad scale or form: H# major
        cp_type: bad common progression name: do-wop

Now, let’s test a good set of parameters. Note: only generating chord progression and saving to wave file.

(base) PS R:\learn\e_m_311> uv run main.py -wd m_cp -mtk 'F# major' -nbr 6 -cpn 4 -ply

selected key: F# major (octave: 3)
  scale notes: ['F#3', 'G#3', 'A#3', 'B3', 'C#4', 'D#4', 'F4']
  key chords: [('F#', 'major'), ('G#', 'minor'), ('A#', 'minor'), ('B', 'major'), ('C#', 'major'), ('D#', 'minor'), ('F', 'dim')]
  chord progression (roman numerals): I-iii-IV-I
  chord progression: [('F#', 'major'), ('A#', 'minor'), ('B', 'major'), ('F#', 'major')]
    [
      F# major -> ['F#3', 'A#3', 'C#4']
      A# minor -> ['A#3', 'C#4', 'F4']
      B major -> ['B3', 'D#4', 'F#4']
      F# major -> ['F#3', 'A#3', 'C#4']
    ]
        multipliers: [3, 5, 7, 9, 11, 13, 15, 17, 19, 21]
        amplitudes: [0.2461, 0.1604, 0.1237, 0.0991, 0.085, 0.0716, 0.0627, 0.0558, 0.0497, 0.0459]
        {'whl': 2.0, 'hlf': 1.0, 'qtr': 0.5, '8th': 0.25, '16th': 0.125, '32nd': 0.0625}

n_bars: 6, cp_len: 4, len cp_rhy: 6, len cp_durs: 6, len c_amps: 24

calling make_cp_sound
  ['8th', 'qtr', '8th', 'hlf']
    0: 8th -> 0.25 * 0.3943 -> 0: ('F#', 'major')
    1: qtr -> 0.5 * 0.3293 -> 1: ('A#', 'minor')
    2: 8th -> 0.25 * 0.7466 -> 2: ('B', 'major')
    3: hlf -> 1.0 * 0.2154 -> 3: ('F#', 'major')
  ['8th', '8th', 'qtr', 'hlf']
    0: 8th -> 0.25 * 0.6321 -> 0: ('F#', 'major')
    1: 8th -> 0.25 * 0.3120 -> 1: ('A#', 'minor')
    2: qtr -> 0.5 * 0.5077 -> 2: ('B', 'major')
    3: hlf -> 1.0 * 0.6538 -> 3: ('F#', 'major')
  ['8th', 'qtr', '8th', 'hlf']
    0: 8th -> 0.25 * 0.8148 -> 0: ('F#', 'major')
    1: qtr -> 0.5 * 0.6044 -> 1: ('A#', 'minor')
    2: 8th -> 0.25 * 0.5929 -> 2: ('B', 'major')
    3: hlf -> 1.0 * 0.6986 -> 3: ('F#', 'major')
  ['8th', 'hlf', 'qtr', '8th']
    0: 8th -> 0.25 * 0.5166 -> 0: ('F#', 'major')
    1: hlf -> 1.0 * 0.5314 -> 1: ('A#', 'minor')
    2: qtr -> 0.5 * 0.5505 -> 2: ('B', 'major')
    3: 8th -> 0.25 * 0.5721 -> 3: ('F#', 'major')
  ['8th', 'qtr', '8th', 'hlf']
    0: 8th -> 0.25 * 0.8598 -> 0: ('F#', 'major')
    1: qtr -> 0.5 * 0.5930 -> 1: ('A#', 'minor')
    2: 8th -> 0.25 * 0.2184 -> 2: ('B', 'major')
    3: hlf -> 1.0 * 0.5108 -> 3: ('F#', 'major')
  ['8th', 'qtr', '8th', 'hlf']
    0: 8th -> 0.25 * 0.5445 -> 0: ('F#', 'major')
    1: qtr -> 0.5 * 0.7822 -> 1: ('A#', 'minor')
    2: 8th -> 0.25 * 0.6944 -> 2: ('B', 'major')
    3: hlf -> 1.0 * 0.6998 -> 3: ('F#', 'major')
make_c_sound done: 0.0156
writing to wave file: img/I-iii-IV-I_4-4_odd_saw_o3_1.wav

This One Done

I know we are nowhere near where we want to be; but, this post is long enough and the related code has taken me a fair bit of time and work.

The first thing I plan to tackle next is refactoring the code to allow me to request a common progression at the command line and have it actually used. As the code sits, a random progression is always used. I will also need to make sure that, if the number of chords in the progression is specified along with a common progression, the number of chords requested agrees with those in the specified progression. Might take more work than I think—or not!

Then I will move on to getting the midi related processes coded in the current main module. And, if necessary I will add or refactor argument parser functions accordingly.

Until then, do put some play in your daily routine.