Theater System Scripts¶
The theater control system includes several executable scripts. These may be individually browsed in Python/theater.
Brief summary:
lighting-server.py: Translates lighting commands from OSC messages to the DMX output device.
motion-server.py: Translates motion commands from OSC messages to the Arduino stepper motor drivers.
MIDI-bridge.py: Translates real-time MIDI events into OSC messages. This is used for linking MIDI-based performance tools to the core system.
Debugging tools:
MIDI-player.py: Translates a MIDI performance file into real-time OSC messages. This is primarily a means of testing a single performance.
set-lights.py: Maintenance utility to directly send commands to the DMX output device.
send-motion-command.py: Demonstration of sending a single OSC mesage to the motion control server.
Other Tools¶
The Performance Utility Tools may be individually browsed in Python/tools.
virtual-mpd218.py: Graphical simulation of a MIDI drum pad (requires PyQt6).
list-MIDI-ports.py: Console script to report available MIDI ports (requires rtmidi).
spline-plot.py: Graphicall plot a single-channel Bezier spline (required matplotlib).
print-MIDI-file.py: Display all events in a MIDI file (uses mido).
merge-MIDI-files.py: Sample script to merge a set of MIDI files (uses mido).
MIDI-player.py¶
Script to perform a MIDI files on the theater by translating MIDI events to OSC messages at the correct times. This uses the theater.midi module (Theater MIDI Processing) which you may have customized for use with midi-bridge.py. This ensures the interpretation is consistent between the two scripts.
Example use from your own laptop:
python3 MIDI-player.py --ip 172.24.26.199 performance.mid
The full code follows, but may also be downloaded from MIDI-player.py.
1#!/usr/bin/env python3
2
3cmd_desc = "Replay a MIDI file in real time as system commands."
4
5import argparse
6import time
7import logging
8
9# Mido 'MIDI objects' package
10# https://mido.readthedocs.io/en/latest/index.html
11import mido
12
13# common MIDI event processing
14from theater.midi import MIDItoOSC
15
16# common logging functions
17import theater.logging
18
19# library schedule
20import theater.HuntLibrary as HuntLibrary
21
22# initialize logging for this module
23log = logging.getLogger('player')
24
25#================================================================
26
27class Player:
28 """MIDI file player."""
29
30 def __init__(self, args):
31
32 self.verbose = args.verbose
33
34 self.midifile = mido.MidiFile(args.input)
35 self.log_midifile_metadata()
36
37 # MIDI files express tempo as microseconds per quarter note; this sets
38 # a default tempo of 120 BPM (0.5 sec per beat).
39 self.midi_tempo = 500000
40
41 # MIDI files express times in integer ticks per quarter note.
42 self.ticks_per_beat = self.midifile.ticks_per_beat
43
44 # Merge all tracks (if more than one) into a single track in time order.
45 # This assumes that events are identified by channel number for routing
46 # and not simply by track.
47 self.playtrack = mido.merge_tracks(self.midifile.tracks)
48
49 # Create the networking output.
50 self.bridge = MIDItoOSC(args)
51
52 return
53
54 def close(self):
55 self.bridge.close()
56
57 #--------------------------------------------------------------------
58 def log_midifile_metadata(self):
59 log.info("Opened MIDI file %s, MIDI format %d, %d tracks, %d ticks per beat",
60 args.input, self.midifile.type, len(self.midifile.tracks), self.midifile.ticks_per_beat)
61
62 for i, track in enumerate(self.midifile.tracks):
63 log.info("MIDI file track %d: %s", i, track.name)
64
65 # report some diagnostics on the current midifile to the log
66 for track in self.midifile.tracks:
67 log.info("Contents of track '%s'", track.name)
68 event_messages = 0
69 for msg in track:
70 if msg.is_meta:
71 log.info("MIDI metadata: %s", msg)
72 else:
73 event_messages += 1
74 # end of track
75 log.info("Track '%s' includes %d events.", track.name, event_messages)
76
77 #--------------------------------------------------------------------
78 def perform_event(self, event):
79 # ignore metadata messages track_name, instrument_name, key_signature, smpte_offset, etc.
80 if event.type == 'set_tempo':
81 log.info("MIDI tempo change: %d usec/beat (%f BPM).", event.tempo, mido.tempo2bpm(event.tempo))
82 self.midi_tempo = event.tempo
83
84 elif event.type in ['note_on', 'note_off']:
85 log.debug("Note On: channel %d, note %d, velocity %d", event.channel, event.note, event.velocity)
86 self.bridge.perform_event(event)
87
88 #--------------------------------------------------------------------
89 def run(self, skip=0):
90 """Run one performance of the MIDI file and return. Eeach MIDI event is
91 issued in real time as OSC network messages translated via the MIDI to
92 OSC bridge object."""
93
94 # Keep track of target times in integer nanoseconds to avoid roundoff
95 # errors. Following an absolute clock will maintain overall precision
96 # in the presence of sleep time jitter.
97 start_t = time.monotonic_ns()
98 next_timepoint = start_t
99
100 # play through all the events in the sequence, waiting the specified
101 # number of ticks before each issue
102 for event in self.playtrack:
103
104 if self.verbose:
105 log.debug("Playing event: %s", event)
106
107 # don't perform the end of track, it can have an unreasonable delay
108 if event.type == 'end_of_track':
109 log.info("Found end event: %s", event)
110 return
111
112 if skip > 0:
113 skip -= 1
114
115 else:
116 # if the next event has a preceding delay, convert from ticks
117 # at the current tempo to an absolute time in nanoseconds
118 if event.time > 0:
119 event_ns = event.time * 1000 * self.midi_tempo / self.ticks_per_beat
120 next_timepoint += event_ns
121 delay_ns = next_timepoint - time.monotonic_ns()
122 if delay_ns > 0:
123 time.sleep(delay_ns * 1e-9)
124
125 self.perform_event(event)
126
127#================================================================
128# The following section is run when this is loaded as a script.
129if __name__ == "__main__":
130
131 # set up logging
132 theater.logging.open_log_file('logs/MIDI-player.log')
133 log.info("Starting MIDI-player.py")
134
135 # Initialize the command parser.
136 parser = argparse.ArgumentParser(description = cmd_desc)
137 parser.add_argument( '--ip', default="127.0.0.1", help="IP address of the OSC receivers (default: %(default)s).")
138 parser.add_argument( '--scheduled',action='store_true', help="Enable continuous operation on schedule.")
139 parser.add_argument( '--skip',type=int, default=0, help="Specify number of events to skip.")
140 theater.logging.add_logging_args(parser)
141 parser.add_argument( 'input', help='MIDI file to perform.')
142
143 # Parse the command line, returning a Namespace.
144 args = parser.parse_args()
145
146 # Modify logging settings as per common arguments.
147 theater.logging.configure_logging(args)
148
149 # Create the MIDI event player.
150 player = Player(args)
151
152 # Run the performance until done. This may be safely interrupted by the user pressing Control-C.
153 try:
154
155 if args.scheduled:
156 # run the file repeatedly whenever the scheduler indicates show time
157 while True:
158 if HuntLibrary.current_time_properties().get('is_show_time'):
159 log.info("Starting scheduled iteration.")
160 player.run()
161 log.info("Starting brief intermission pause.")
162 time.sleep(5)
163 else:
164 time.sleep(60) # wait before checking time again
165 else:
166 log.info("Starting a single performance.")
167 player.run(args.skip)
168
169 except KeyboardInterrupt:
170 log.info("User interrupted operation.")
171 print("User interrupt, shutting down.")
172 player.close()
173
174 log.info("Exiting MIDI-player.py")