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 close(self): self.network.close()
#---------------------------------------------------------------
[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)
#================================================================