Not sure this is going to help down the road, but I think I am going to look at getting multiple channels working in wave and midi files. Midi allows fifteen I believe, with one more, channel 10, for precussion. Well that is 16 channels per track, whatever a track is! Wave, apparently allows considerably more. But let’s start with two in a wave file.

Two Channel Wave File

I will for now write the same chord progression, with different rhythms, to one wave file and see what we get. Note, I am not trying to create a stereo like sound output. Will be cluttering up my files with a lot of play/test code. May use functions from the get go or refactor at a later time.

New Command Line Argument Value

I am going to add a new value for the command line argument --whatdo in em_ui.get_parser(). Specifically, m_wv_mch.

# in em_ui.py
... ...
  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",
           "m_wv_mch": "generate wave file with multiple channels"
           }
.. ...
# in main.py
... ...
  (do_mk_cprog, do_w2m_pi, do_w2m_ps, do_m_play, do_w_play, do_sav_mid, do_m2w,
   do_i_chg, do_i_shw, do_wv_mch) = (False, False, False, False, False, False,
                                        False, False, False, False)
  match cl_args.whatdo:
    case "m_cp":
      do_mk_cprog = True
      args_ok, args = emui.get_args_do_m_cp(cl_args)
      do_w_play = args["play_track"]
    case "m_wv_mch":
      do_wv_mch = True
      args_ok, args = emui.get_args_do_m_cp(cl_args)
      do_w_play = args["play_track"]
... ...
  if do_wv_mch:
    print("generate a multi-channel wave file")

And quick test seems to indicate we are on the right track.

Initial Attempt to Generate Wave File With Two Channels of Sound

I am going to, in that if block, generate two Numpy arrays containing the wave data for two chord progressions. Both will use the same progression, but the rythym, volumes and such for each will be a little different. I will also ensure that they start in two different octaves.

I did get something coded that more or less worked. The wave file with two channels was in fact produced. And when played you could hear both channels. But when I converted to a midi, trying to specify a different instrument for each channel, no luck.

There was one other issue to resolve as well. When I have the progression type set as random, I get a random and, 99% of the time, different chord progression for each channel’s sound. That did not sound particularly pleasing to me. For now I need to specify one of the common progressions. Then it is used for both channels.

I am not going to show you any of the code I wrote, but here’s the terminal output and the audio file for a wee test.

PS R:\learn\e_m_311> uv run main.py -wd m_wv_mch -cpt "four magic chords" -ply
... ...
command line args are ok: True; augmented args are:
whatdo: m_wv_mch
filepath_in:
filepath_wv:
filepath_mid:
track_key: ('F', 'major')
nbr_bars: 6
play_track: True
no_save_wv: False
no_save_mid: False
cp_type: four magic chords
cp_nbr: 4
instr: []
fs: <midi2audio.FluidSynth object at 0x000002A98930AD50>
r_oct: 2
t_sig: (4, 4)
s_rate: 44100
ot_s: even
ot_w: half
n_ot: 10
dbg: True

selected key: F major (octave: 2)
  scale notes: ['F2', 'G2', 'A2', 'A#2', 'C3', 'D3', 'E3']
  key chords: [('F', 'major'), ('G', 'minor'), ('A', 'minor'), ('A#', 'major'), ('C', 'major'), ('D', 'minor'), ('E', 'dim')]
  chord progression (roman numerals, four magic chords): I-V-vi-IV
  chord progression: [('F', 'major'), ('C', 'major'), ('D', 'minor'), ('A#', 'major')]
    [
      F major -> ['F2', 'A2', 'C3']
      C major -> ['C2', 'E2', 'G2']
      D minor -> ['D2', 'F2', 'A2']
      A# major -> ['A#2', 'D3', 'F3']
    ]
        multipliers: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
        amplitudes: [0.474, 0.2796, 0.1351, 0.0618, 0.0265, 0.0112, 0.0059, 0.0032, 0.0017, 0.0009]
        {'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
  ['qtr', '8th', 'hlf', '8th']
    0: qtr -> 0.5 * 0.3670 -> 0: ('F', 'major')
    1: 8th -> 0.25 * 0.4226 -> 1: ('C', 'major')
    2: hlf -> 1.0 * 0.6756 -> 2: ('D', 'minor')
    3: 8th -> 0.25 * 0.2503 -> 3: ('A#', 'major')
  ['qtr', 'qtr', 'qtr', 'qtr']
    0: qtr -> 0.5 * 0.3576 -> 0: ('F', 'major')
    1: qtr -> 0.5 * 0.6625 -> 1: ('C', 'major')
    2: qtr -> 0.5 * 0.6476 -> 2: ('D', 'minor')
    3: qtr -> 0.5 * 0.5133 -> 3: ('A#', 'major')
  ['qtr', 'qtr', 'qtr', 'qtr']
    0: qtr -> 0.5 * 0.3342 -> 0: ('F', 'major')
    1: qtr -> 0.5 * 0.7107 -> 1: ('C', 'major')
    2: qtr -> 0.5 * 0.5326 -> 2: ('D', 'minor')
    3: qtr -> 0.5 * 0.5564 -> 3: ('A#', 'major')
  ['qtr', 'qtr', 'qtr', 'qtr']
    0: qtr -> 0.5 * 0.4145 -> 0: ('F', 'major')
    1: qtr -> 0.5 * 0.3331 -> 1: ('C', 'major')
    2: qtr -> 0.5 * 0.1986 -> 2: ('D', 'minor')
    3: qtr -> 0.5 * 0.5617 -> 3: ('A#', 'major')
  ['qtr', 'hlf', '8th', '8th']
    0: qtr -> 0.5 * 0.4613 -> 0: ('F', 'major')
    1: hlf -> 1.0 * 0.3359 -> 1: ('C', 'major')
    2: 8th -> 0.25 * 0.4567 -> 2: ('D', 'minor')
    3: 8th -> 0.25 * 0.5404 -> 3: ('A#', 'major')
  ['hlf', '8th', '8th', 'qtr']
    0: hlf -> 1.0 * 0.2777 -> 0: ('F', 'major')
    1: 8th -> 0.25 * 0.4779 -> 1: ('C', 'major')
    2: 8th -> 0.25 * 0.6806 -> 2: ('D', 'minor')
    3: qtr -> 0.5 * 0.4844 -> 3: ('A#', 'major')
         qtr -> 0.5 * 0.4844 -> 3: ('F', 'major')
make_c_sound done: 0.0000

selected key: F major (octave: 5)
  scale notes: ['F5', 'G5', 'A5', 'A#5', 'C6', 'D6', 'E6']
  key chords: [('F', 'major'), ('G', 'minor'), ('A', 'minor'), ('A#', 'major'), ('C', 'major'), ('D', 'minor'), ('E', 'dim')]
  chord progression (roman numerals, four magic chords): I-V-vi-IV
  chord progression: [('F', 'major'), ('C', 'major'), ('D', 'minor'), ('A#', 'major')]
    [
      F major -> ['F5', 'A5', 'C6']
      C major -> ['C5', 'E5', 'G5']
      D minor -> ['D5', 'F5', 'A5']
      A# major -> ['A#5', 'D6', 'F6']
    ]
        multipliers: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
        amplitudes: [0.5439, 0.2181, 0.12, 0.0568, 0.0338, 0.0148, 0.0074, 0.0033, 0.0014, 0.0006]
        {'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.4163 -> 0: ('F', 'major')
    1: qtr -> 0.5 * 0.7933 -> 1: ('C', 'major')
    2: 8th -> 0.25 * 0.2229 -> 2: ('D', 'minor')
    3: hlf -> 1.0 * 0.3552 -> 3: ('A#', 'major')
  ['hlf', 'qtr', '8th', '8th']
    1: qtr -> 0.5 * 0.5129 -> 1: ('C', 'major')
    2: 8th -> 0.25 * 0.2282 -> 2: ('D', 'minor')
    3: 8th -> 0.25 * 0.6894 -> 3: ('A#', 'major')
  ['qtr', '8th', 'hlf', '8th']
    0: qtr -> 0.5 * 0.5880 -> 0: ('F', 'major')
    1: 8th -> 0.25 * 0.5084 -> 1: ('C', 'major')
    2: hlf -> 1.0 * 0.2353 -> 2: ('D', 'minor')
    3: 8th -> 0.25 * 0.1437 -> 3: ('A#', 'major')
  ['qtr', 'qtr', 'qtr', 'qtr']
    0: qtr -> 0.5 * 0.3136 -> 0: ('F', 'major')
    1: qtr -> 0.5 * 0.6835 -> 1: ('C', 'major')
    2: qtr -> 0.5 * 0.4321 -> 2: ('D', 'minor')
    3: qtr -> 0.5 * 0.7385 -> 3: ('A#', 'major')
  ['8th', 'hlf', '8th', 'qtr']
    0: 8th -> 0.25 * 0.4430 -> 0: ('F', 'major')
    1: hlf -> 1.0 * 0.4220 -> 1: ('C', 'major')
    2: 8th -> 0.25 * 0.3217 -> 2: ('D', 'minor')
    3: qtr -> 0.5 * 0.5273 -> 3: ('A#', 'major')
  ['hlf', '8th', '8th', 'qtr']
    0: hlf -> 1.0 * 0.6022 -> 0: ('F', 'major')
    1: 8th -> 0.25 * 0.4588 -> 1: ('C', 'major')
    2: 8th -> 0.25 * 0.4346 -> 2: ('D', 'minor')
    3: qtr -> 0.5 * 0.5017 -> 3: ('A#', 'major')
         qtr -> 0.5 * 0.5017 -> 3: ('F', 'major')
make_c_sound done: 0.0000
2 channels: ['I-V-vi-IV', 'I-V-vi-IV'], snd1: (529200,), snd2: (529200,)
        (529200, 2) - channels: 2
writing to wave file: img/I-V-vi-IV_I-V-vi-IV_4-4_even_half_o5_1.wav

Investigate Wave/Midi File Data

In order to have a look at things to see where they were going awry, I wrote or refactored an if block or two. And added a new option or two for the --whatdo command line parameter. The if blocks display the meta-data for wave or midi files.

The coding got a little ahead of my blog content, so hopefully I will recall all the code changes (don’t feel like playing with git diff).

# in em_ui.py
... ...
# list 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",
           "w_shw": "show meta data, etc for a wave 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",
           "m_wv_mch": "generate wave file with multiple channels"
           }
... ...

# in main.py
... ...
  (do_mk_cprog, do_w2m_pi, do_w2m_ps, do_m_play, do_w_play, do_sav_mid, do_m2w,
   do_i_chg, do_i_shw, do_wv_mch, do_wv_shw) = (False, False, False, False, False, False,
                                        False, False, False, False, False)
... ...
    case "i_shw":
      do_i_shw = True
      args_ok, args = emui.get_args_do_play(cl_args)
    case "w_shw":
      do_wv_shw = True
      args_ok, args = emui.get_args_do_play(cl_args)
      wv_fl = args["filepath_in"]
.. ...
  # reworking block, copied then refactored
  do_wv_mch_1 = False

  if do_wv_shw:
    with sf.SoundFile(wv_fl) as sf_obj:
      print(f"\nSoundFile: {sf_obj}")
      print(f"\tnbr channels: {sf_obj.channels}")
      print(f"\tnbr frames: {sf_obj.frames}")
      print(f"\tsample rate: {sf_obj.samplerate}")
      print(f"\tformat: {sf_obj.format}")
      print(f"\tformat_info: {sf_obj.format_info}")
      print(f"\tsubtype_info: {sf_obj.subtype_info}")
      print(f"\tnbr sections: {sf_obj.sections}")
      print(f"\tendian: {sf_obj.endian}")
      print(f"\tcompression_level: {sf_obj.compression_level}")
      print(f"\textra_info: {sf_obj.extra_info}")
    
    data, s_rate = sf.read(wv_fl)
    print(f"\tshape data array: {data.shape}")
    print(f"\tchannel data: {data[:5]}")

  if do_i_chg or do_i_shw:
    mid = MidiFile(str(audio_fl), clip=True)

  if do_i_shw:
    print(f"midi type: {mid.type}, playback length: {mid.length}")
    for i, trk in enumerate(mid.tracks):
      if i==0:
        print(f"track {i}: {trk}")
      else:
        for m_i, msg in enumerate(trk):
          if hasattr(msg, "program"):
            print(f"track {i}: {trk[m_i:m_i+2]}")

Now, let’s have a look at the wave file and then the midi generated from it.

PS R:\learn\e_m_311> uv run main.py -wd w_shw -fpi img/I-V-vi-IV_I-V-vi-IV_4-4_even_half_o5_1.wav
... ...
SoundFile: SoundFile('img/I-V-vi-IV_I-V-vi-IV_4-4_even_half_o5_1.wav', mode='r', samplerate=44100, channels=2, format='WAV', subtype='PCM_16', endian='FILE')
        nbr channels: 2
        nbr frames: 529200
        sample rate: 44100
        format: WAV
        format_info: WAV (Microsoft)
        subtype_info: Signed 16 bit PCM
        nbr sections: 1
        endian: FILE
        compression_level: None
        extra_info: File : 'img/I-V-vi-IV_I-V-vi-IV_4-4_even_half_o5_1.wav' (utf-8 converted from ucs-2)
Length : 2116844
RIFF : 2116836
WAVE
fmt  : 16
  Format        : 0x1 => WAVE_FORMAT_PCM
  Channels      : 2
  Sample Rate   : 44100
  Block Align   : 4
  Bit Width     : 16
  Bytes/sec     : 176400
data : 2116800
End

        shape data array: (529200, 2)
        channel data: [[0.         0.        ]
 [0.02597046 0.20932007]
 [0.05172729 0.33154297]
 [0.07699585 0.38519287]
 [0.1015625  0.41140747]]

PS R:\learn\e_m_311> uv run main.py -wd i_shw -fpi img/I-V-vi-IV_I-V-vi-IV_4-4_even_half_o5_1_w2m_i-finger-bass.mid
... ...
midi type: 1, playback length: 11.920454545454568
track 0: MidiTrack([
  MetaMessage('set_tempo', tempo=500000, time=0),
  MetaMessage('time_signature', numerator=4, denominator=4, clocks_per_click=24, notated_32nd_notes_per_beat=8, time=0),
  MetaMessage('end_of_track', time=1)])
track 1: MidiTrack([
  Message('program_change', channel=0, program=33, time=0),
  Message('note_on', channel=0, note=41, velocity=80, time=5)])

Clearly two channels in the wave file, but only one in the midi file. And when I open the midi file in MuseScore Studio I get the following.

screen capture showing musical notation for midi file opened in MuseScore Studio

Pretty clearly not what I wanted.

Rethink, Refactor and New Code

What I am going to look at is reworking my code to save both of the single channel wave files. Convert those to midi. Then combine the two midi files into a single midi file. Hopefully with two tracks/channels; one for each of the two wave files. I expect the process should work for any number of wave files. Perhaps even flac files (but will leave that flac file thing for another day).

Refactor the if do_wv_mch: Block

I am going to, for each set of parameters, currently only the starting octave, generate a chord progression and save to wave. I will then convert that wave file to a midi file. Once all the wave/midi files are done, I will save the combined set of chord progressions to a multi-channel wave file.

I will eventually attempt to code the midi merging process at a later date in a separate test if block. One thing at a time.

I introduced a loop to go through the process for each chord progression. Most options are currently hard-coded in the if block. Sorry a lot of repeated code from above. Note, I no longer convert the multi-channel wave file to a midi file.

  if do_wv_mch:
    print("generate a multi-channel wave file")

    args["track_key"] = ('F', 'major')
    args["nbr_bars"] = 8
    args["r_oct"] = 2
    args["t_sig"] = (emw.rng.choice([3,4]), 4)
    args["s_rate"] = sample_rate
    args["ot_s"] = emw.rng.choice(["even", "odd", "seq"])
    args["ot_w"] = emw.rng.choice(["half", "saw", "sqr", "tri"])
    # should perhaps make this a random selection over some small range
    args["n_ot"] = 10
    # print debug statements in various functions
    args["dbg"] = True
    # add acces to a basic pitch model
    args["bpm"] = Model(ICASSP_2022_MODEL_PATH)
    
    # probably always the case, but...
    do_sav_wav = not args["no_save_wv"]

    if args["dbg"]:
      print(f"\ncommand line args are ok: {args_ok}; augmented args are:")
      for ky, val in args.items():
        print(f"{ky}: {val}")

    # which starting octaves for the chord progressions
    do_octs = [2, 5]
    rns, snds = [], []

    for o_i, cp_oct in enumerate(do_octs):
      snd = None
      args["r_oct"] = cp_oct
      rn_prg, snd =  emc.make_chord_prog(args)
      rns.append(rn_prg)
      snds.append(snd)
      snd = emw.normalize_wave(snd, do_typ=True)
      w_fl_nm = f"{aud_mid_dir}/{rn_prg}_{args['t_sig'][0]}-{args['t_sig'][1]}_{args['ot_s']}_{args['ot_w']}_o{args['r_oct']}_s{o_i}.wav"
      sv_ok = emf.save_wave(w_fl_nm, snd)
      # convert wave file to midi
      mm_args = args.copy()
      mm_args["filepath_in"] = w_fl_nm
      m_fl_nm = emf.convert_save_w2m(mm_args)

    # generate array for multi-channel wave file
    chnls = np.column_stack(snds)
    if args["dbg"]:
      print(f"2 channels: {rns}, snd1: {snds[0].shape}, snd2: {snds[0].shape}")
      print(f"\t{chnls.shape} - channels: {chnls.shape[1]} -> {chnls.sum(axis=1)}")

    if do_sav_wav or do_w_play:
      snd = emw.normalize_wave(chnls, do_typ=True)

    if do_sav_wav:
      # save multi-channel wave file
      if args["filepath_wv"] == "":
        if rns:
          rn_prg = "_".join(rns)
        w_fl_nm = f"{aud_mid_dir}/{rn_prg}_{args['t_sig'][0]}-{args['t_sig'][1]}_{args['ot_s']}_{args['ot_w']}_o{args['r_oct']}_m1.wav"
      else:
        w_fl_nm = args["filepath_wv"]
      # Save WAV file
      sv_ok = emf.save_wave(w_fl_nm, snd)
      if do_wv_mch:
        mm_args["filepath_in"] = w_fl_nm
        m_fl_nm = emf.convert_save_w2m(mm_args)

    if do_w_play:
      sd.play(snd)
      sd.wait()

In the terminal I got a lot of output. You don’t likely need to see it, but cheap post content, eh!

PS R:\learn\e_m_311> uv run main.py -wd m_wv_mch -cpt "four magic chords"
... ...
command line args are ok: True; augmented args are:
whatdo: m_wv_mch
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: four magic chords
cp_nbr: 5
instr: []
fs: <midi2audio.FluidSynth object at 0x000001DA7F6106D0>
r_oct: 2
t_sig: (3, 4)
s_rate: 44100
ot_s: odd
ot_w: sqr
n_ot: 10
dbg: True
bpm: <basic_pitch.inference.Model object at 0x000001DA022D8910>

selected key: F major (octave: 2)
  scale notes: ['F2', 'G2', 'A2', 'A#2', 'C3', 'D3', 'E3']
  key chords: [('F', 'major'), ('G', 'minor'), ('A', 'minor'), ('A#', 'major'), ('C', 'major'), ('D', 'minor'), ('E', 'dim')]
  chord progression (roman numerals, four magic chords): I-V-vi-IV
  chord progression: [('F', 'major'), ('C', 'major'), ('D', 'minor'), ('A#', 'major')]
    [
      F major -> ['F2', 'A2', 'C3']
      C major -> ['C2', 'E2', 'G2']
      D minor -> ['D2', 'F2', 'A2']
      A# major -> ['A#2', 'D3', 'F3']
    ]
        multipliers: [3, 5, 7, 9, 11, 13, 15, 17, 19, 21]
        amplitudes: [0.282, 0.1725, 0.1218, 0.093, 0.0756, 0.0653, 0.056, 0.0495, 0.0443, 0.04]
        {'whl': 2.0, 'hlf': 1.0, 'qtr': 0.5, '8th': 0.25, '16th': 0.125, '32nd': 0.0625}

n_bars: 8, cp_len: 4, len cp_rhy: 6, len cp_durs: 6, len c_amps: 32

calling make_cp_sound
  ['qtr', '8th', 'qtr', '8th']
    0: qtr -> 0.5 * 0.6418 -> 0: ('F', 'major')
    1: 8th -> 0.25 * 0.5678 -> 1: ('C', 'major')
    2: qtr -> 0.5 * 0.7382 -> 2: ('D', 'minor')
    3: 8th -> 0.25 * 0.5272 -> 3: ('A#', 'major')
  ['qtr', 'hlf', 'qtr', 'hlf']
    0: qtr -> 0.5 * 0.5621 -> 0: ('F', 'major')
    1: hlf -> 1.0 * 0.2504 -> 1: ('C', 'major')
    2: qtr -> 0.5 * 0.3755 -> 2: ('D', 'minor')
    3: hlf -> 1.0 * 0.3070 -> 3: ('A#', 'major')
  ['hlf', 'qtr', 'qtr', 'hlf']
    0: hlf -> 1.0 * 0.4710 -> 0: ('F', 'major')
    1: qtr -> 0.5 * 0.5280 -> 1: ('C', 'major')
    2: qtr -> 0.5 * 0.5273 -> 2: ('D', 'minor')
    3: hlf -> 1.0 * 0.5841 -> 3: ('A#', 'major')
  ['8th', 'qtr', '8th', 'qtr']
    0: 8th -> 0.25 * 0.4464 -> 0: ('F', 'major')
    1: qtr -> 0.5 * 0.4955 -> 1: ('C', 'major')
    2: 8th -> 0.25 * 0.5748 -> 2: ('D', 'minor')
    3: qtr -> 0.5 * 0.4815 -> 3: ('A#', 'major')
  ['qtr', 'qtr', '8th', '8th']
    0: qtr -> 0.5 * 0.5464 -> 0: ('F', 'major')
    1: qtr -> 0.5 * 0.3945 -> 1: ('C', 'major')
    2: 8th -> 0.25 * 0.6959 -> 2: ('D', 'minor')
    3: 8th -> 0.25 * 0.7831 -> 3: ('A#', 'major')
  ['8th', 'qtr', '8th', 'qtr']
    0: 8th -> 0.25 * 0.5404 -> 0: ('F', 'major')
    1: qtr -> 0.5 * 0.4488 -> 1: ('C', 'major')
    2: 8th -> 0.25 * 0.5320 -> 2: ('D', 'minor')
    3: qtr -> 0.5 * 0.4837 -> 3: ('A#', 'major')
make_c_sound done: 0.0000
        0: snd: (529200,) -> <class 'numpy.int16'>
save_wave: snd.shape (529200,)
writing to wave file: img/I-V-vi-IV_3-4_odd_sqr_o2_s0.wav

Predicting MIDI for img\I-V-vi-IV_3-4_odd_sqr_o2_s0.wav...

  Creating midi...
  💅 Saved to img\I-V-vi-IV_3-4_odd_sqr_o2_s0_basic_pitch.mid
  Creating midi sonification...
  🎧 Saved to img\I-V-vi-IV_3-4_odd_sqr_o2_s0_basic_pitch.wav
audio to midi took 7.7945 sec

selected key: F major (octave: 5)
  scale notes: ['F5', 'G5', 'A5', 'A#5', 'C6', 'D6', 'E6']
  key chords: [('F', 'major'), ('G', 'minor'), ('A', 'minor'), ('A#', 'major'), ('C', 'major'), ('D', 'minor'), ('E', 'dim')]
  chord progression (roman numerals, four magic chords): I-V-vi-IV
  chord progression: [('F', 'major'), ('C', 'major'), ('D', 'minor'), ('A#', 'major')]
    [
      F major -> ['F5', 'A5', 'C6']
      C major -> ['C5', 'E5', 'G5']
      D minor -> ['D5', 'F5', 'A5']
      A# major -> ['A#5', 'D6', 'F6']
    ]
        multipliers: [3, 5, 7, 9, 11, 13, 15, 17, 19, 21]
        amplitudes: [0.29, 0.1663, 0.1196, 0.0946, 0.0764, 0.0639, 0.0565, 0.0492, 0.0439, 0.0397]
        {'whl': 2.0, 'hlf': 1.0, 'qtr': 0.5, '8th': 0.25, '16th': 0.125, '32nd': 0.0625}

n_bars: 8, cp_len: 4, len cp_rhy: 6, len cp_durs: 6, len c_amps: 32

calling make_cp_sound
  ['8th', 'qtr', 'qtr', '8th']
    0: 8th -> 0.25 * 0.5512 -> 0: ('F', 'major')
    1: qtr -> 0.5 * 0.6032 -> 1: ('C', 'major')
    2: qtr -> 0.5 * 0.5665 -> 2: ('D', 'minor')
    3: 8th -> 0.25 * 0.7828 -> 3: ('A#', 'major')
  ['qtr', '8th', '8th', 'qtr']
    0: qtr -> 0.5 * 0.3688 -> 0: ('F', 'major')
    1: 8th -> 0.25 * 0.2758 -> 1: ('C', 'major')
    2: 8th -> 0.25 * 0.2424 -> 2: ('D', 'minor')
    3: qtr -> 0.5 * 0.3059 -> 3: ('A#', 'major')
  ['qtr', 'qtr', '8th', '8th']
    0: qtr -> 0.5 * 0.4323 -> 0: ('F', 'major')
    1: qtr -> 0.5 * 0.4266 -> 1: ('C', 'major')
    2: 8th -> 0.25 * 0.8536 -> 2: ('D', 'minor')
    3: 8th -> 0.25 * 0.7033 -> 3: ('A#', 'major')
  ['hlf', 'qtr', 'qtr', 'hlf']
    0: hlf -> 1.0 * 0.5431 -> 0: ('F', 'major')
    1: qtr -> 0.5 * 0.5792 -> 1: ('C', 'major')
    2: qtr -> 0.5 * 0.5849 -> 2: ('D', 'minor')
    3: hlf -> 1.0 * 0.4651 -> 3: ('A#', 'major')
  ['qtr', 'qtr', 'hlf', 'hlf']
    0: qtr -> 0.5 * 0.4826 -> 0: ('F', 'major')
    1: qtr -> 0.5 * 0.5011 -> 1: ('C', 'major')
    2: hlf -> 1.0 * 0.4661 -> 2: ('D', 'minor')
    3: hlf -> 1.0 * 0.3447 -> 3: ('A#', 'major')
  ['qtr', '8th', 'qtr', '8th']
    0: qtr -> 0.5 * 0.6339 -> 0: ('F', 'major')
    1: 8th -> 0.25 * 0.5717 -> 1: ('C', 'major')
    2: qtr -> 0.5 * 0.6470 -> 2: ('D', 'minor')
    3: 8th -> 0.25 * 0.5170 -> 3: ('A#', 'major')
make_c_sound done: 0.0000
save_wave: snd.shape (529200,)
writing to wave file: img/I-V-vi-IV_3-4_odd_sqr_o5_s1.wav

Predicting MIDI for img\I-V-vi-IV_3-4_odd_sqr_o5_s1.wav...

  Creating midi...
  💅 Saved to img\I-V-vi-IV_3-4_odd_sqr_o5_s1_basic_pitch.mid

  Creating midi sonification...
  🎧 Saved to img\I-V-vi-IV_3-4_odd_sqr_o5_s1_basic_pitch.wav
audio to midi took 0.6135 sec

2 channels: ['I-V-vi-IV', 'I-V-vi-IV'], snd1: (529200,), snd2: (529200,)
        (529200, 2) - channels: 2 -> [0.         1.56016296 1.6839924  ... 2.14554117 2.40701949 2.34417312]
save_wave: snd.shape (529200, 2)
writing to wave file: img/I-V-vi-IV_I-V-vi-IV_3-4_odd_sqr_o5_m1.wav

Not including any of the audio files, but I assure you they were generated and all make sound when played.

Combining Two Midi Files Into One File

My goal is to get multiple tracks so that I can assign different instruments to each one. Let’s see if we can get something to work.

Basically, I am going to read the two midi files into Mido MidiFile objects. I will create a new output MidiFile object. I will add a track 0 containing only meta data from one of the input files to the output file. I will then go through the other tracks in each input midi file, event by event, and write those events to the output midi object. For each event, I will, if appropriate, update the channel member. Finally I will write the output object to a file. Here’s my most recent version of that process.

I stuck the following test block into main.py so that it would run given a valid set of command line arguments. And then terminated it before the code requested by the command line arguments gets executed.

... ...
  # test block
  if True:
    t_nbr = 1
    mf_dir = "img/"
    mids_2_merge = [
      "I-V-vi-IV_3-4_odd_sqr_o2_s0_basic_pitch.mid",
      "I-V-vi-IV_3-4_odd_sqr_o5_s1_basic_pitch.mid"
    ]
    mids_open = [MidiFile(f"{mf_dir}{mf}") for mf in mids_2_merge]
    mrgd = MidiFile()
    
    mrgd.ticks_per_beat = mids_open[0].ticks_per_beat
    track = mids_open[0].tracks[0]
    track0 = MidiTrack()
    track0.name = 'metadata'
    for evnt in track:
      if evnt.is_meta:
        if evnt.type in ['time_signature', 'key_signature', 'smpte_offset', 'set_tempo']:
          track0.append(evnt)
    mrgd.tracks.append(track0)

    # for i, mf in enumerate(mids_2_merge):
    for i, mf in enumerate(mids_open):
      # trk = MidiTrack()
      # print(f"\t{i}, {mf.tracks[0]}")
      # t_mid = MidiFile(f"{mf_dir}{mf}")
      for j, mf_trk in enumerate(mf.tracks):
        if j == 0:
          print(f"\n{i}, {mf.tracks[0]}")
          continue
        print(f"\tprocessing file {i} track {j}")
        trk = MidiTrack()
        # for k, evnt in mf.tracks[1:]:
        c_ch = i+j
        for evnt in mf_trk:
          if evnt.is_meta:
            trk.append(evnt)
          else:
            trk.append(evnt.copy(channel=c_ch))
      mrgd.tracks.append(trk)
    mrgd.save(f"img/I-V-vi-IV_3-4_odd_sqr_o5_s0_s1_x{t_nbr}.mid")
    exit(0)

And, in the terminal the following was produced when executing main.py. Note, the specified input file (-fpi) has to exist, but which midi file it is really doesn’t matter

PS R:\learn\e_m_311> uv run main.py -wd i_shw -fpi img/I-iii-IV-I_4-4_odd_sqr_o4_1_basic_pitch.mid
... ...
command line args are ok: True
whatdo: i_shw
filepath_in: R:\learn\e_m_311/img/I-iii-IV-I_4-4_odd_sqr_o4_1_basic_pitch.mid
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: []
fs: <midi2audio.FluidSynth object at 0x0000017DEF43F910>

0, MidiTrack([
  MetaMessage('set_tempo', tempo=500000, time=0),
  MetaMessage('time_signature', numerator=4, denominator=4, clocks_per_click=24, notated_32nd_notes_per_beat=8, time=0),
  MetaMessage('end_of_track', time=1)])
        processing file 0 track 1

1, MidiTrack([
  MetaMessage('set_tempo', tempo=500000, time=0),
  MetaMessage('time_signature', numerator=4, denominator=4, clocks_per_click=24, notated_32nd_notes_per_beat=8, time=0),
  MetaMessage('end_of_track', time=1)])
        processing file 1 track 1

The midi file I-V-vi-IV_3-4_odd_sqr_o5_s0_s1_x1.mid was saved to disk. When I opened that file in MuseScore Studio I saw the following.

screen capture showing musical notation for midi file opened in MuseScore Studio

That’s pretty much what I was hoping for. Two tracks with the ability to change the instrument on each one. But, when I closed the score’s tab and reopened it without leaving MuseScore, I got something I didn’t expect. And don’t understand. But it does look like the bass clef notes have been split between two staves. And, two channels? Is this just something to do with MuseScore itself rather than the midi file?

screen capture showing musical notation for midi file opened in MuseScore Studio

Looks like we now have 3 tracks and each one, at least in MuseScore Studio, can be set to a different instrument. Though I have not done so. I exported that 3 track score to a midi file. And when I loaded that new file, it still had three tracks. Let’s see what my code says about that file. (Note: a wee refactoring of the if do_i_shw: block.)

PS R:\learn\e_m_311> uv run main.py -wd i_shw -fpi img/I-V-vi-IV_3-4_odd_sqr_o5_s0_s1_x1_t3maybe.mid
... ...
midi type: 1, playback length: 12.0

track 0:
        MetaMessage('track_name', name='Electric Piano', time=0)
        MetaMessage('time_signature', numerator=3, denominator=4, clocks_per_click=24, notated_32nd_notes_per_beat=8, time=0)
        MetaMessage('key_signature', key='F', time=0)
        MetaMessage('set_tempo', tempo=500000, time=0)
        MetaMessage('midi_port', port=0, time=0)
        MetaMessage('end_of_track', time=1)
        track 0[10:12]: MidiTrack([
  Message('program_change', channel=0, program=4, time=0),
  Message('control_change', channel=0, control=7, value=100, time=0)])
        track 0 has channels {0}
        [Message('program_change', channel=0, program=4, time=0)]

track 1:
        MetaMessage('track_name', name='Electric Piano', time=0)
        MetaMessage('key_signature', key='F', time=0)
        MetaMessage('midi_port', port=0, time=0)
        MetaMessage('end_of_track', time=1)
        track 1[8:10]: MidiTrack([
  Message('program_change', channel=1, program=4, time=0),
  Message('control_change', channel=1, control=7, value=100, time=0)])
        track 1 has channels {1}
        [Message('program_change', channel=1, program=4, time=0)]

track 2:
        MetaMessage('track_name', name='Electric Piano', time=0)
        MetaMessage('key_signature', key='F', time=0)
        MetaMessage('midi_port', port=1, time=0)
        MetaMessage('end_of_track', time=1)
        track 2[8:10]: MidiTrack([
  Message('program_change', channel=0, program=4, time=0),
  Message('control_change', channel=0, control=7, value=100, time=0)])
        track 2 has channels {0}
        [Message('program_change', channel=0, program=4, time=0)]

Are those two channels labelled as \(0\) really just one channel spread across two tracks/staves?

I certainly didn’t expect to see any notes playing in track 0!? And, my ears are just not trained enough to tell if I am hearing all three tracks when playing that file with my code or in MuseScore. That said it does look like things are going in the correct direction.

I have used MuseScore to export a flac version of the above file. Here it is.

Done M’thinks

I was going to look at using my code to change the instruments in that new file and see if it works or not. But, this post, with its code and terminal output, is more than long enough for me. So, I think I am calling it done and leaving that bit of playing around until the next post.

Until then, may you experience a little bit of surprise most days of your life.