Performance Utility Tools

Some of the exercise packages include several utility and test programs. These are all standalone scripts using Python 3 and a few additional packages as described in System Requirements.

The following scripts may be browsed in the Python tools directory on the course site.

list-MIDI-ports.py

Command-line test program for identifying available MIDI ports by printing a list of the MIDI input and output ports available via python-rtmidi. There are no options, just running it will print a list of port names:

python3 list-MIDI-ports.py

The full code follows, but may also be downloaded from list-MIDI-ports.py.

 1#!/usr/bin/env python3
 2
 3import rtmidi
 4midiout = rtmidi.MidiOut()
 5midiin = rtmidi.MidiIn()
 6
 7print("Available output ports:")
 8for idx, port in enumerate(midiout.get_ports()):
 9    print("  %d: %s" % (idx, port))
10
11print("Available input ports:")
12for idx, port in enumerate(midiin.get_ports()):
13    print("  %d: %s" % (idx, port))

virtual-mpd218.py

../_images/virtual_mpd218.png

Emulation of an Akai MPD218 Drum Pad Controller, generating local MIDI events and control changes.

merge-MIDI-files.py

Sample script to merge a set of MIDI files. This is intended as a script template to be customized for your specific application. A typical use within the course is to merge a set of single-track type 0 MIDI files exported from Ableton Live into a multi-track type 1 file for playing back on the theater system.

To run it:

python3 merge-MIDI-files.py

The default script reads five MIDI files named channel1.mid, channel2.mid, etc. It updates the channel numbers for every event, then merges all the tracks into a multi-track MIDI files named performance.mid.

The most obvious customization is to edit the list of source files, although this script could be extended to use different channel numbers, adjust event timing, filter particular messages, or other algorithmic transformations. The details of the output can be reviewed using the print-MIDI-file.py script.

Note for Ableton users: Live does not include tempo information when exporting clips as type 0 MIDI files, it instead assumes 120 BPM when generating timing tick values. If you are using a different tempo, you may wish to edit the file_tempo_in_BPM variable so your show executes at the correct pace.

The full code follows, but may also be downloaded from merge-MIDI-files.py.

 1#!/usr/bin/env python3
 2
 3# merge-MIDI-files.py
 4
 5# Sample script to merge a set of MIDI files.  This is intended as a script
 6# template to be customized per application.
 7
 8# Typical use: merge a set of single-track type 0 MIDI files exported from
 9# Ableton Live into a multi-track type 1 file.
10
11import sys
12
13# https://mido.readthedocs.io/en/latest/index.html
14import mido
15
16#================================================================
17# Load an input file.
18def load_midi_file(path):
19    mid = mido.MidiFile(path)
20    print(f"Opened MIDI file {path}: type {mid.type}")
21    if mid.type != 0:
22        print("Error: this script only supports type 0 (single-track) files.")
23        sys.exit(1)
24    print("  Track name: %s" % (mid.tracks[0].name))
25    return mid
26
27#================================================================
28def extract_metadata_track(track):
29    """Return a new track containing the metadata common to all tracks."""
30    track0 = mido.MidiTrack()
31    track0.name = 'metadata'
32    for event in track:
33        if event.is_meta:
34            if event.type in ['time_signature', 'key_signature', 'smpte_offset', 'set_tempo']:
35                track0.append(event)
36
37    return track0
38
39#================================================================
40def filter_track(track, channel):
41    """Process a track for merging, returning a new track.  Filters out
42    particular metadata events.  Rewrites channel numbers for message events in
43    the track."""
44
45    result = mido.MidiTrack()
46    for event in track:
47        if event.is_meta:
48            if event.type not in ['time_signature', 'key_signature', 'smpte_offset', 'set_tempo']:
49                result.append(event)
50
51        else:
52            result.append(event.copy(channel=channel))
53    return result
54
55#================================================================
56#================================================================
57# Script execution begins here.
58
59# Load the source files in channel order.
60paths = ["channel1.mid", "channel2.mid", "channel3.mid", "channel4.mid", "channel5.mid"]
61sources = [load_midi_file(path) for path in paths]
62
63#----------------------------------------------------------------
64# Use the first file to generate a metadata track.  Note: no check
65# is made to ensure that the subsequent files match.
66track0 = extract_metadata_track(sources[0].tracks[0])
67
68# Note: Ableton Live does not include tempo information when exporting clips as
69# type 0 MIDI files, it always assumes 120 BPM when generating timing tick
70# values.  The following line can be modified to insert your preferred tempo.
71file_tempo_in_BPM = 120
72track0.append(mido.MetaMessage(type='set_tempo', tempo=mido.bpm2tempo(file_tempo_in_BPM), time=0))
73
74#----------------------------------------------------------------
75# Create a new output file to contain all the tracks as individual tracks.
76output = mido.MidiFile()
77output.ticks_per_beat = sources[0].ticks_per_beat
78
79# Add the metadata track.
80output.tracks.append(track0)
81
82#----------------------------------------------------------------
83# Process each track and append to the output file.
84# Note that channel numbers for each track event are rewritten starting with
85# MIDI channel 1 (denoted with zero).
86
87for channel, src in enumerate(sources):
88    track = src.tracks[0]
89    processed = filter_track(track, channel)
90    output.tracks.append(processed)
91
92#----------------------------------------------------------------
93# Save the result.
94output_path = 'performance.mid'
95print(f"Writing output to {output_path}.")
96output.save(output_path)
97
98# All done.