I really wasn’t sure exactly what I was going to do next. But in the end I decided to see if I could get code working to convert a .wav file to midi. The web app I used for the last post was, in fact, a sample of a library/package written by a team at Spotify. So I decided to install that in my virtual environment and see if I could write some code to get it to work.

Install Basic Pitch

Basic Pitch is a Python library for Automatic Music Transcription (AMT), using lightweight neural network developed by Spotify’s Audio Intelligence Lab. It’s small, easy-to-use, pip install-able and npm install-able via its sibling repo.

Okay, that seems simple enough. Well, I was wrong.

Took me a while to realize I couldn’t install it in my Python 3.14 virtual environment. After some reading, well more careful reading, and some playing around, I started a new virtual environment using Python 3.11.9. And gave it a go.

(base) PS R:\learn\e_m_311> uv add basic-pitch
Using CPython 3.11.9
Creating virtual environment at: .venv
Resolved 81 packages in 1.54s
error: Distribution `tensorflow-io-gcs-filesystem==0.37.1 @ registry+https://pypi.org/simple` can't be installed because it doesn't have a source distribution or wheel for the current platform

hint: You're on Windows (`win_amd64`), but `tensorflow-io-gcs-filesystem` (v0.37.1) only has wheels for the following platforms: `manylinux_2_17_aarch64`, `manylinux_2_17_x86_64`, `manylinux2014_aarch64`, `manylinux2014_x86_64`, `macosx_10_14_x86_64`, `macosx_12_0_arm64`; consider adding "sys_platform == 'win32' and platform_machine == 'AMD64'" to `tool.uv.required-environments` to ensure uv resolves to a version with compatible wheels

So, I added the following to my pyproject.toml.

[tool.uv]
required-environments = [
    "sys_platform == 'win32' and platform_machine == 'AMD64'"
]

And tried again.

(base) PS R:\learn\e_m_311> uv add basic-pitch
Resolved 83 packages in 510ms
      Built pretty-midi==0.2.11
Prepared 61 packages in 20.54s
░░░░░░░░░░░░░░░░░░░░ [0/63] Installing wheels...                                                                        warning: Failed to hardlink files; falling back to full copy. This may lead to degraded performance.
         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 63 packages in 33.54s
 + absl-py==2.4.0
... ...
 + wrapt==1.14.2

I then modified the main module in the virtual environment.

import tensorflow as tf
from basic_pitch.inference import predict, Model
from basic_pitch import ICASSP_2022_MODEL_PATH


def main():
  print("Hello from e-m-311!")

  basic_pitch_model = Model(ICASSP_2022_MODEL_PATH)

  model_output, midi_data, note_events = predict("img\I-iii-IV-vii-iii-ii-iii-iii_4-4_odd_tri_o1_1.wav")
  print(midi_data)


if __name__ == "__main__":
    main()

And, in the terminal more trouble.

(base) PS R:\learn\e_m_311> uv run main.py
Traceback (most recent call last):
  File "R:\learn\e_m_311\main.py", line 1, in <module>
ModuleNotFoundError: No module named 'tensorflow'

In another attempt I got the following message.

WARNING:root:Tensorflow is not installed. If you plan to use a TF Saved Model, reinstall basic-pitch with `pip install 'basic-pitch[tf]'`

So, I tried that.

(base) PS R:\learn\e_m_311> uv add basic-pitch[tf]
Resolved 83 packages in 10ms
Audited 63 packages in 27ms
(base) PS R:\learn\e_m_311> uv run main.py
Traceback (most recent call last):
  File "R:\learn\e_m_311\main.py", line 1, in <module>
    import tensorflow as tf
ModuleNotFoundError: No module named 'tensorflow'

I gave up, uninstalled basic-pitch and tried adding it again with the tf flag.

(base) PS R:\learn\e_m_311> uv remove basic-pitch
Resolved 1 package in 27ms
Uninstalled 63 packages in 39.05s
 - absl-py==2.4.0
... ...
 - wrapt==1.14.2

(base) PS R:\learn\e_m_311> uv add basic-pitch[tf]
Resolved 83 packages in 1.25s
░░░░░░░░░░░░░░░░░░░░ [0/63] Installing wheels...                                                                        warning: Failed to hardlink files; falling back to full copy. This may lead to degraded performance.
         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 63 packages in 30.19s
 + absl-py==2.4.0
... ...
 + wrapt==1.14.2
(base) PS R:\learn\e_m_311> uv run main.py
Traceback (most recent call last):
  File "R:\learn\e_m_311\main.py", line 1, in <module>
    import tensorflow as tf
ModuleNotFoundError: No module named 'tensorflow'

So, I checked the state of the tensorflow package. And found I might be missing a required dependency.

(base) PS R:\learn\e_m_311> uv pip show tensorflow
Name: tensorflow
Version: 2.14.0
Location: R:\learn\e_m_311\.venv\Lib\site-packages
Requires: tensorflow-intel
Required-by: basic-pitch
(base) PS R:\learn\e_m_311> uv add tensorflow-intel
Resolved 79 packages in 1.94s
Uninstalled 1 package in 5.36s
░░░░░░░░░░░░░░░░░░░░ [0/2] Installing wheels...                                                                         warning: Failed to hardlink files; falling back to full copy. This may lead to degraded performance.
         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.
 - numpy==2.4.6
 + numpy==1.26.4
 + tensorflow-intel==2.14.1

But still no go.

(base) PS R:\learn\e_m_311> uv run main.py
WARNING:root:Coremltools is not installed. If you plan to use a CoreML Saved Model, reinstall basic-pitch with `pip install 'basic-pitch[coreml]'`
 'basic-pitch tflite-runtime'` or `pip install 'basic-pitch[tf]'
WARNING:root:onnxruntime is not installed. If you plan to use an ONNX Model, reinstall basic-pitch with `pip install 'basic-pitch[onnx]'`
Traceback (most recent call last):
  File "R:\learn\e_m_311\main.py", line 2, in <module>
  File "R:\learn\e_m_311\.venv\Lib\site-packages\basic_pitch\inference.py", line 67, in <module>
  File "R:\learn\e_m_311\.venv\Lib\site-packages\basic_pitch\note_creation.py", line 23, in <module>
    import resampy
    from . import filters
  File "R:\learn\e_m_311\.venv\Lib\site-packages\resampy\filters.py", line 50, in <module>
    import pkg_resources
ModuleNotFoundError: No module named 'pkg_resources'

Took a while, but I eventually found the help that I needed to resolve the issue.

(base) PS R:\learn\e_m_311> uv pip show setuptools
Name: setuptools
Version: 82.0.1
Location: R:\learn\e_m_311\.venv\Lib\site-packages
Requires:
Required-by: tensorboard, tensorflow-intel
(base) PS R:\learn\e_m_311> uv remove setuptools
error: The dependency `setuptools` could not be found in `project.dependencies`
(base) PS R:\learn\e_m_311> uv pip uninstall setuptools
Uninstalled 1 package in 3.18s
 - setuptools==82.0.1
(base) PS R:\learn\e_m_311> uv add setuptools==81.0.0
Resolved 79 packages in 1.20s
Prepared 1 package in 1.26s
░░░░░░░░░░░░░░░░░░░░ [0/1] Installing wheels...                                                                         warning: Failed to hardlink files; falling back to full copy. This may lead to degraded performance.
         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 1 package in 4.79s
 + setuptools==81.0.0

And, this test more or less worked.

(base) PS R:\learn\e_m_311> uv run main.py
WARNING:root:Coremltools is not installed. If you plan to use a CoreML Saved Model, reinstall basic-pitch with `pip install 'basic-pitch[coreml]'`
WARNING:root:tflite-runtime is not installed. If you plan to use a TFLite Model, reinstall basic-pitch with `pip install 'basic-pitch tflite-runtime'` or `pip install 'basic-pitch[tf]'
WARNING:root:onnxruntime is not installed. If you plan to use an ONNX Model, reinstall basic-pitch with `pip install 'basic-pitch[onnx]'`
R:\learn\e_m_311\.venv\Lib\site-packages\resampy\filters.py:50: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
  import pkg_resources
Hello from e-m-311!
2026-05-27 16:43:11.201037: I tensorflow/core/platform/cpu_feature_guard.cc:182] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations.
To enable the following instructions: SSE SSE2 SSE3 SSE4.1 SSE4.2 AVX AVX2 FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags.
Predicting MIDI for img\I-iii-IV-vii-iii-ii-iii-iii_4-4_odd_tri_o1_1.wav...
<pretty_midi.pretty_midi.PrettyMIDI object at 0x00000220BA5FC250>

Still don’t know how to get it to stop printing all those warnings. And, I have no idea what to do about that last warning regarding Tensorflow.

Let’s Convert .wav to MIDI and Save to File

I decided I would add a command line argument to allow me to specify the .wav file name/path. I also decided to time the process…curiousity. A bit more code than the test above. And a new Basic Pitch function.

import argparse, time
from pathlib import Path

import tensorflow as tf
from basic_pitch.inference import predict, predict_and_save, Model
from basic_pitch import ICASSP_2022_MODEL_PATH


def get_parser():
  """Create and return parser for wav file conversion.
  """
  # instantiate and set up command paramter parser
  parser = argparse.ArgumentParser()
  parser.add_argument("-fn", "--filename", help="Supply name of .wav file to be converted to MIDI")
  return(parser)


def main():
  cl_parse = get_parser()
  cl_args = cl_parse.parse_args()

  basic_pitch_model = Model(ICASSP_2022_MODEL_PATH)
  
  if cl_args.filename is not None:
    audio_fl = Path(cl_args.filename)
    if not audio_fl.exists() or audio_fl.suffix != ".wav":
      print(f"\nfile, {cl_args.filename}, does not exist or is not a .wav file!")
      exit(1)
    # save mid file to same directory wav file was in
    fl_pth = audio_fl.parent
    st = time.perf_counter()
    predict_and_save(
      [audio_fl],
      fl_pth,
      True,
      True,
      False,
      True,
      basic_pitch_model
  )
    et = time.perf_counter()
    print(f"audio to midi took {(et-st):.4f} sec")


if __name__ == "__main__":
    main()

And here’s the terminal output for that wave file we played with in the last post.

(base) PS R:\learn\e_m_311> uv run main.py -fn img/I-vi-ii_4-4_even_tri_o3_1.wav
all 'basic-pitch[coreml]'`
WARNING:root:tflite-runtime is not installed. If you plan to use a TFLite Model, reinstall basic-pitch with `pip install 'basic-pitch tflite-runtime'` or `pip install 'basic-pitch[tf]'
WARNING:root:onnxruntime is not installed. If you plan to use an ONNX Model, reinstall basic-pitch with `pip install 'basic-pitch[onnx]'`
R:\learn\e_m_311\.venv\Lib\site-packages\resampy\filters.py:50: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
  import pkg_resources
2026-05-31 09:27:47.697339: I tensorflow/core/platform/cpu_feature_guard.cc:182] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations.
To enable the following instructions: SSE SSE2 SSE3 SSE4.1 SSE4.2 AVX AVX2 FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags.

Predicting MIDI for img\I-vi-ii_4-4_even_tri_o3_1.wav...


  Creating midi...
  💅 Saved to img\I-vi-ii_4-4_even_tri_o3_1_basic_pitch.mid


  Creating midi sonification...
  🎧 Saved to img\I-vi-ii_4-4_even_tri_o3_1_basic_pitch.wav


  Creating note events...
  🌸 Saved to img\I-vi-ii_4-4_even_tri_o3_1_basic_pitch.csv
audio to midi took 13.8792 sec

And I can assure you, that midi file was created and could be loaded and manipulated by MuseScore. Not that I have yet done much manipulation.

Not sure where I will go next. But I may try installing a package to load a midi file and generate a .wav file. Or an .mpg

MIDI to Wave

I plan to try the midi2audio package. It requires FluidSynth to be installed on the system.

FluidSynth

I downloaded the current version from the releases page. In my case I used fluidsynth-v2.5.4-win10-x64-glib.zip. I unzipped that into e:\appMisc\fluidsynth. Then added e:\appMisc\fluidsynth\bin to the system path via the control panel. Once I opened a new terminal window I was able to run FluidSynth (wouldn’t run in a previously opened terminal window as system path in that window would not have been updated).

Too make things work, I will need to provide FluidSynth with a soundfont file.

SoundFont is a file format for sample-based instrument sounds. You will need a SoundFont to use FluidSynth. If you are not familiar with them, check out Josh Green’s Introduction to SoundFonts and Soundfont 2.1 application note. About SoundFonts

They recommended a couple of SoundFonts on that page. I went with S. Christian Collins GeneralUser GS. I won’t bother describing downloading and unzipping the soundfont file. Or where I put it.

midi2audio

Next I installed the midi2audio package.

(base) PS R:\learn\e_m_311> uv add midi2audio
Resolved 80 packages in 1.54s
Prepared 1 package in 255ms
░░░░░░░░░░░░░░░░░░░░ [0/1] Installing wheels...                                                                         warning: Failed to hardlink files; falling back to full copy. This may lead to degraded performance.
         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 1 package in 232ms
 + midi2audio==0.1.1

Python Module Update

I then updated my main module to use and test the new midi2audio package. A few booleans to control what goes on. Will eventually add a command line arguement for that purpose. For now, hardcoded variables. Note the change to the argparser function code. I have also added code that can be modified to alter the gain used to generate the output wave (or other) file.

import argparse, time
from pathlib import Path

import tensorflow as tf
from basic_pitch.inference import predict, predict_and_save, Model
from basic_pitch import ICASSP_2022_MODEL_PATH
from midi2audio import FluidSynth

# globals

SFF = Path("R:/learn/e_music/rek/GeneralUser_GS_v2.0.3--doc_r6/GeneralUser-GS/GeneralUser-GS.sf2")
PPTH = Path("R:/learn/e_m_311")


def get_parser():
  """Create and return parser for wav file conversion.
  """
  # instantiate and set up command paramter parser
  parser = argparse.ArgumentParser()
  parser.add_argument("-fp", "--filepath", help="Supply name/path of .wav or .mid file to be used in code")
  return(parser)


def main():
  # what to do
  do_w2m = False
  do_m_play = False
  do_m2w = True

  # print("Hello from e-m-311!")
  cl_parse = get_parser()
  cl_args = cl_parse.parse_args()

  basic_pitch_model = Model(ICASSP_2022_MODEL_PATH)
  
  if cl_args.filepath is not None:

    audio_fl = Path(PPTH/cl_args.filepath)
    print(f"audio file: {audio_fl}")
    if not audio_fl.exists() or not (audio_fl.suffix == ".wav" or audio_fl.suffix == ".mid"):
      print(f"\nfile, {cl_args.filepath}, does not exist or is not a .wav file!")
      exit(1)

    # save mid file to same directory wav file was in
    fl_pth = audio_fl.parent

    if do_w2m:
      st = time.perf_counter()
      predict_and_save(
        [audio_fl],
        fl_pth,
        True,
        True,
        False,
        True,
        basic_pitch_model
    )
      et = time.perf_counter()
      print(f"audio to midi took {(et-st):.4f} sec")
    
    if do_m_play or do_m2w:
      # instantiate midi2audio's FluidSynth class, passing it the path to the soundfont file
      # if want higher gain, change following value
      w_gain = 0.5
      sf = FluidSynth(str(SFF), gain=w_gain)

    if do_m_play:
      sf.play_midi(str(audio_fl))

    if do_m2w:
      wv_fl = Path(f"{fl_pth}/{audio_fl.stem}_m2w_g{sf.gain}.wav")
      # print(f"wav file: {wv_fl}")
      sf.midi_to_audio(str(audio_fl), str(wv_fl))


if __name__ == "__main__":
    main()

When I ran the module with do_m_play = True everything worked just fine. Though the sound volume was pretty low. May play with the gain parameter down the road.

However, with do_m2w = True things did not go so well.

(base) PS R:\learn\e_m_311> uv run main.py -fp img/I-vi-ii_4-4_even_tri_o3_1_basic_pitch.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.

error: '-F' is an illegal option at this place, only -b option is allowed here.
fluidsynth: error: fluid_is_soundfont(): fopen() failed: 'File does not exist.'
Parameter 'R:\learn\e_m_311\img\I-vi-ii_4-4_even_tri_o3_1_basic_pitch_m2w.wav' not a SoundFont or MIDI file or error occurred identifying it.
error: '-r' is an illegal option at this place, only -b option is allowed here.
fluidsynth: error: fluid_is_soundfont(): fopen() failed: 'File does not exist.'
Parameter '44100' not a SoundFont or MIDI file or error occurred identifying it.

The problem was with this line in the midi2audio packages primary module. It no longer matched the API for FluidSynth.

        subprocess.call(
            ['fluidsynth', '-ni', '-g', str(self.gain), self.sound_font, midi_file, '-F', audio_file, '-r', str(self.sample_rate)], 
            stdout=stdout, 
        )

midi2audio Fix

So, I copied the original midi2audio.py file to my virtual environment directory. Then fixed that call to fluidsynth. It now looks as follows.

        subprocess.call(
            ['fluidsynth', '-ni', '-g', str(self.gain), '-F', audio_file, '-r', str(self.sample_rate), self.sound_font, midi_file], 
            stdout=stdout, 
        )

And when I tried that midi to wave conversion again I got the following.

(base) PS R:\learn\e_m_311> uv run main.py -fp img/I-vi-ii_4-4_even_tri_o3_1_basic_pitch.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.

Rendering audio to file 'R:\learn\e_m_311\img\I-vi-ii_4-4_even_tri_o3_1_basic_pitch_m2w_g0.2.wav'..

I did try generating a wave file of this same midi file using a gain of 0.5. Didn’t notice any significant difference when playing on the same player at the same volume. Tried a gain of 1.25 and the music sounded a fair bit louder with the same player settings. Though, not at all sure what gain actually does or is meant to do.

Change Instrument MIDI Told to Use

In general, a midi file will typically have a prog event for each track in the file. We currently only have one track. So one event. When I use BasicPitch to generate a midi file from my randomly generated wave files, it defaults to a prog of 4 (Tine Electric Piano). Now I can apparently change that during the conversion process if I use predict rather than predict_and_save. I have been using the latter. So let’s give the other a go.

Using Basic Pitch

The basic idea is we use predict to get Basic Pitch’s midi data object. We then use that object’s instruments collection to change the instrument for each channel. Finally we save the modified data object to a midi file. To start I will write some test code using hardcoded instrument values. Eventually that will change to use command line arguments or some interactive approach. For now I will also assume a single track in each midi file. That will down the road change and the code will need to be refactored accordingly.

... ...
  parser.add_argument("-wd", "--whatdo", help="Supply code for action to take: w2m_pi, w2m_ps, play, m2w")
... ...
  # what to do
  do_w2m_pi, do_w2m_ps, do_m_play, do_m2w = False, False, False, False
  match cl_args.whatdo:
    case "w2m_pi":
      do_w2m_pi = True
    case "w2m_ps":
      do_w2m_ps = True
    case "play":
... ...
    if not (do_w2m_pi or do_w2m_ps or do_m_play or do_m2w):
      print(f"Please, along with a file name, specify the action to take (-wd). One of: w2m_pi, w2m_ps, play, m2w")
      exit(1)
... ...
    if do_w2m_pi:
      # generate midi using a specific instrument
      # new instrument program number
      i_new_prog = 40
      st = time.perf_counter()
      # get midi data
      _, m_data, n_evnts = predict(
        audio_fl,
        basic_pitch_model
      )
      # replace current instrument with new one, i_new
      for instrument in m_data.instruments:
        print(f"\t{instrument} -> changing {instrument.program} to {i_new_prog}")
        instrument.program = i_new_prog
      # save midi file
      midi_fl = Path(f"{fl_pth}/{audio_fl.stem}_w2m_i-{i_new_prog}.mid")
      m_data.write(midi_fl)
      et = time.perf_counter()
      print(f"audio to midi took {(et-st):.4f} sec")
    
    if do_w2m_ps:
      st = time.perf_counter()
      predict_and_save(
... ...

And in the terminal the following output was displayed.

(base) PS R:\learn\e_m_311> uv run main.py -fp img/I-V-I-IV-I_4-4_seq_tri_o4_1.wav -wd w2m_pi
... ...
        cli: Namespace(filepath='img/I-V-I-IV-I_4-4_seq_tri_o4_1.wav', whatdo='w2m_pi', instr_f_2=None)
... ...
audio file: R:\learn\e_m_311\img\I-V-I-IV-I_4-4_seq_tri_o4_1.wav
fl_pth: R:\learn\e_m_311\img
Predicting MIDI for R:\learn\e_m_311\img\I-V-I-IV-I_4-4_seq_tri_o4_1.wav...
        Instrument(program=4, is_drum=False, name="") -> changing 4 to 40
audio to midi took 9.2714 sec

Loaded it in MuseScore and played it. Can’t say it sounds like a violin. But, when I change the instrument to violin in MuseScore, it sounds the same. And, in VLC Player it does sound a touch more like a violin. So, going to do it again with a glockenspiel and see how it sounds.

      # new instrument program number
      i_new_prog = 40 # violin
      i_new_prog = 9 # glockenspiel
(base) PS R:\learn\e_m_311> uv run main.py -fp img/I-V-I-IV-I_4-4_seq_tri_o4_1.wav -wd w2m_pi
... ...
audio file: R:\learn\e_m_311\img\I-V-I-IV-I_4-4_seq_tri_o4_1.wav
fl_pth: R:\learn\e_m_311\img
Predicting MIDI for R:\learn\e_m_311\img\I-V-I-IV-I_4-4_seq_tri_o4_1.wav...
        Instrument(program=4, is_drum=False, name="") -> changing 4 to 9
audio to midi took 6.9519 sec

And, this time, I do think it sounds a bit like a glockenspiel. Not going to convert and include in the post (at least as things stand at the moment).

I ended up taking I-vi-ii-vii-iii_4-4_even_saw_o4_1.wav and generating midi files using violin, harpsichord and glockenspiel. I then wrote a bit of code to play the midi files (the one generated by Basic Pitch with no instrument change and the three based on different instruments) one after the other. I could actually hear a difference between all four outputs.

The only problem with this approach is that I have to generate a new midi for each instrument change. Would be nice if I could change instruments before exporting to an audio file.

Using FluidSynth

It looked to me like I might be able to change the default instrument the midi files were using using a configuration file when calling FluidSynth to convert the midi file to an audio file (for now a wave file).

So I created a file, fs_midi.conf with the following contents.

load R:/learn/e_music/rek/GeneralUser_GS_v2.0.3--doc_r6/GeneralUser-GS/GeneralUser-GS.sf2
select 0 1 0 40

That select event tells FluidSynth to use bank 0 and prog 40 from the first soundfont for the first track (0). That bank 0 prog 40 specifies a violin. Unfortunately that did not work. The config file is loaded before the midi file. When the midi file is loaded any prog commands are used to specify the instrument for each track. In our case a tine electric piano, bank 0 prog 4 (though the instrument would likely depend on the soundfont being used).

Edit MIDI File With Mido

So, after some research I decided to try the Mido library to edit the midi file.

Okay, add Mido library to our virtual environment. Pretty quick and easy.

(base) PS R:\learn\e_m_311> uv add mido
Resolved 80 packages in 1.24s
Audited 65 packages in 22ms

I decided to add two new booleans to my dev module: do_i_shw and do_i_chg. I.E. show current instrument(s) or change the current instrument for each track (well maybe).

do_i_shw

In the end I decided to display a little more than the message specify the prog command.

... ...
import mido
from mido import Message, MidiFile, MidiTrack
... ...
  parser.add_argument("-wd", "--whatdo", help="Supply code for action to take: w2m_pi, w2m_ps, play, m2w, i_chg, i_shw")
... ...
  do_w2m_pi, do_w2m_ps, do_m_play, do_m2w, do_i_chg, do_i_shw = False, False, False, False, False, False
... ...
    case "m2w":
      do_m2w = True
    case "i_chg":
      do_i_chg = True
    case "i_shw":
      do_i_shw = True
... ...
  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:
        print(f"track {i}: {trk[0:2]}")

  if do_i_chg:
    ...

And for the Basic Pitch default midi file, I got the following.

(base) PS R:\learn\e_m_311> uv run main.py -fp img/I-vi-ii-vii-iii_4-4_even_saw_o4_1_basic_pitch.mid -wd i_shw
... ...
audio file: R:\learn\e_m_311\img\I-vi-ii-vii-iii_4-4_even_saw_o4_1_basic_pitch.mid
fl_pth: R:\learn\e_m_311\img
midi type: 1, playback length: 25.072727272727278
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=4, time=0),
  Message('note_on', channel=0, note=67, velocity=99, time=5)])

And for the harpsichord version, I got the following.

(base) PS R:\learn\e_m_311> uv run main.py -fp img/I-vi-ii-vii-iii_4-4_even_saw_o4_1_pi-6.mid -wd i_shw
... ...
audio file: R:\learn\e_m_311\img\I-vi-ii-vii-iii_4-4_even_saw_o4_1_pi-6.mid
fl_pth: R:\learn\e_m_311\img
midi type: 1, playback length: 25.072727272727278
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=6, time=0),
  Message('note_on', channel=0, note=67, velocity=99, time=5)])

Which is in keeping with our expectations.

do_i_chg

Okay let’s move on to editing the instrument directly in the midi file.

The idea is to open the midi file with Mido. Iterate through the tracks and if we find a specific instrument replace it with another instrument. If the old instrument was found and replaced, save the midi data to a new file.

I will eventually add instrument choices to the command line. But for now that info will be hard coded in my dev/test code.

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

  if do_i_shw:
... ...
  if do_i_chg:
    i_map = {4: 40, 40: 6, 6: 9}
    i_instr = {4: "tine_electric_piano", 40: "violin", 6: "harpsichord", 9: "glockenspiel"}
    i_fnd = False
    for track in mid.tracks:
      for i, msg in enumerate(track):
        if msg.type == 'program_change':
          print(f"\t{msg}")
          if msg.program in i_map:
            i_fnd = True
            old_instr = msg.program
            # Swap the instrument using .copy()
            track[i] = msg.copy(program=i_map[old_instr])
            print(f"Swapped instrument {old_instr} to {i_map[old_instr]} on Channel {msg.channel}")
          else:
            print(f"No instrument in {i_map} found in file")

    if i_fnd:
      f_stem = audio_fl.stem
      f_stem = f_stem.replace(f"_{i_instr[old_instr]}", "")
      ni_mid = Path(f"{fl_pth}/{f_stem}_ichg_{i_instr[i_map[old_instr]]}.mid")
      mid.save(ni_mid)

I am going to run this code twice. So I should get two new files ending in: _ichg_violin.mid and _ichg_harpsichord.mid. I will start with the default Basic Pitch midi used above. For the second run, I will use the _ichg_violin.mid file.

(base) PS R:\learn\e_m_311> uv run main.py -fp img/I-vi-ii-vii-iii_4-4_even_saw_o4_1_basic_pitch.mid -wd i_chg
... ...
audio file: R:\learn\e_m_311\img\I-vi-ii-vii-iii_4-4_even_saw_o4_1_basic_pitch.mid
fl_pth: R:\learn\e_m_311\img
        program_change channel=0 program=4 time=0
Swapped instrument 4 to 40 on Channel 0

(base) PS R:\learn\e_m_311> uv run main.py -fp img/I-vi-ii-vii-iii_4-4_even_saw_o4_1_basic_pitch_violin.mid -wd i_chg
... ...
audio file: R:\learn\e_m_311\img\I-vi-ii-vii-iii_4-4_even_saw_o4_1_basic_pitch_violin.mid
fl_pth: R:\learn\e_m_311\img
        program_change channel=0 program=40 time=0
Swapped instrument 40 to 6 on Channel 0

And those two files are in the target directory. And when I played the Basic Pitch generated files for those two instruments against the Mido edited ones they sounded, to me, pretty much the same.

Done

I think that’s it for this post. It is getting to be rather lengthy and I have spent a fair bit of time on it. Started drafting it June 6th and it is now June 13th. Perhaps not an extraordinary length of time, but certainly longer than many previous posts.

Couple of things I want to do. I want to get some command line parameters to control/facilitate the new code options. And, I want to see if I can get the earlier code, used to generate the wave files and convert them, working in the earlier version of Python I am using in this virtual environment—forced to use out of necessity.

Until we meet again, do enjoy your experimenting, researching and learning.

Resources