Source code for theater.midi
"""midi.py
MIDI event processing objects common to several scripts."""
import logging
import mido
from pythonosc import osc_message_builder
from pythonosc import udp_client
from .network import TheaterNetwork
# initialize logging for this module
log = logging.getLogger('MIDI')
#================================================================
# Predefined spline paths useful for MIDI cues. Each path implicitly begins with a
# zero (not included). Every three points in each row specifies a Bezier spline
# segment. The knots are specified in degrees of rotation relative to the
# position at the start of the path.
spline_table = [
[0, 0, -90, -90, 0, 0],
[0, 90, -90, -180, -180, -180, -180, 0, 0],
[0, -90, -90, -90, -180, -180, -180, -270, -270, -270, -360, -360],
[0, -60, -180, -300, -360, -360]
]
#================================================================
# Predefined lighting levels useful for MIDI cues. Each
# defines an array of fixture DMX levels.
color_table = [
[ 0, 0, 255, 0], # RGBA fixture blue
[ 255, 0, 255, 0], # RGBA fixture purple
[ 0, 0, 0, 255], # RGBA fixture amber
[ 255, 255, 255, 255], # RGBA fixture all
]
#================================================================
[docs]def decode_mpd218_key(key):
"""Interpret a MPD218 pad event note value as a row, column, and bank position.
Row 0 is the front/bottom row (Pads 1-4), row 3 is the back/top row (Pads 13-16).
Column 0 is the left, column 3 is the right.
Bank 0 is the A bank, bank 1 is the B bank, bank 2 is the C bank.
:param key: an integer MIDI note value
:return: (row, column, bank)
"""
# Decode the key into coordinates on the 4x4 pad grid.
bank = (key - 36) // 16
pos = (key - 36) % 16
row = pos // 4
col = pos % 4
return row, col, bank
#================================================================
[docs]class MIDItoOSC:
"""Manage translation of MIDI events into OSC messages for either the live MIDI stream processing or file playback."""
def __init__(self, args):
# Create the networking outputs.
self.network = TheaterNetwork(args)
self.num_motion_units = self.network.num_motion_units()
return
#---------------------------------------------------------------
[docs] def perform_event(self, event):
"""Interpret a mido MIDI event object and generate OSC messaging output."""
# Channel 9 is shown as channel 10 in Logic Pro X, and is conventionally the
# general percussion channel. The MPD218 drum pad emits events on this channel,
# but individual Logic Pro X channel strips can be set to different channels.
if event.channel == 9:
# log.warning("Received event on MIDI percussion channel 9, remapping to lighting.")
self.perform_lighting_event(event)
# log.warning("Received event on MIDI percussion channel 9, remapping to motion.")
# self.perform_motion_event(event, 0)
# channel 0 is shown as channel 1 in Logic Pro X
elif event.channel == 0:
self.perform_lighting_event(event)
# channel 1 is shown as channel 2 in Logic Pro X
elif event.channel <= self.num_motion_units:
self.perform_motion_event(event, event.channel-1)
else:
log.warning("Ignoring event on channel %d", event.channel)
#---------------------------------------------------------------
[docs] def perform_lighting_event(self, event):
if event.type == 'note_on' or event.type == 'note_off':
row, col, bank = decode_mpd218_key(event.note)
if row == 0 or row == 1:
# the bottom two rows on the MPD218 drum pad are mapped to
# individual control of mono lighting channels
channel = 4*row + col
if event.type == 'note_on':
level = event.velocity*255.0/127.0
else:
level = 0
self.network.lights.send_message("/fixture", [channel, level])
log.debug("Sending lighting /fixture %d with level %f", channel, level)
elif row == 2 or row == 3:
# the upper two rows of the MPD218 are mapped to footlight colors
if event.type == 'note_on':
color = color_table[col]
else:
color = [0, 0, 0, 0]
if color is not None:
fixture = (row-2) + 8
scaling = event.velocity / 127.0
values = [fixture] + [value*scaling for value in color]
log.debug("Sending lighting /fixture %s", values)
self.network.lights.send_message("/fixture", values)
else:
log.debug("ignoring note on %d, %d", event.note, event.velocity)
elif event.type == 'pitchwheel':
pass
elif event.type == 'aftertouch':
# An Akai MPD218 drum pad in most default modes will emit a
# continuous stream of aftertouch ages for each pad held down. To
# enable: hold Prog Select, tap Pad 1. But in general this stream
# can overwhelm the lighting system so by default it is ignored.
pass
elif event.type == 'polytouch':
# An Akai MPD218 drum pad set to Program 8 will emit a continuous
# stream of polyphonic aftertouch messages for each pad held down.
# To enable: hold Prog Select, tap Pad 8. But in general this
# stream can overwhelm the lighting system so by default it is
# ignored.
pass
else:
log.debug("Ignoring lighting MIDI event %s", event)
#================================================================
[docs] def perform_motion_event(self, event, unit):
if event.type in ['note_off', 'aftertouch', 'polytouch', 'pitchwheel', 'control_change']:
pass
elif event.type == 'note_on':
# translate the MIDI event into a OSC /spline message
row, col, bank = decode_mpd218_key(event.note)
channel = col
path = row
args = [channel] + spline_table[path]
self.network.motion_server(unit).send_message("/spline", args)
log.debug("Sending motion command MIDI (%d, %d) to unit %d as /spline %s", event.note, event.velocity, unit, args)
else:
log.debug("Ignoring motion MIDI event %s", event)
#================================================================