#!/usr/bin/env python3
"""A simulated winch system with GUI."""
################################################################
# Written in 2018-2019 by Garth Zeglin <garthz@cmu.edu>
# To the extent possible under law, the author has dedicated all copyright
# and related and neighboring rights to this software to the public domain
# worldwide. This software is distributed without any warranty.
# You should have received a copy of the CC0 Public Domain Dedication along with this software.
# If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
################################################################
# standard Python libraries
from __future__ import print_function
import sys, logging
# for documentation on the PyQt5 API, see http://pyqt.sourceforge.net/Docs/PyQt5/index.html
from PyQt5 import QtCore, QtWidgets
# This uses several modules from the course library.
import kf.QtWinch
import kf.QtMPD218
import kf.QtConfig
import kf.QtLog
import kf.QtPlotter
import kf.path
import kf.midi
import kf.osc
import kf.sim
# set up logger for module
log = logging.getLogger('demo1')
################################################################
[docs]class ControlLogic(kf.midi.MIDIProcessor):
"""Core performance logic for processing MIDI input into winch commands."""
def __init__(self):
super(ControlLogic,self).__init__()
self.winches = None
self.display = None
self.all_axes = range(4) # index list for updating all motors
self.frequency = 1.0
self.damping_ratio = 1.0
self.winch_dir = [0,0,0,0] # array to keep track of currently moving winches for aftertouch control
return
[docs] def connect_winches(self, winches):
"""Attach a winch output device to the performance logic, either physical or simulated."""
self.winches = winches
[docs] def connect_display(self, display):
"""Attach a console status output device to the performance logic."""
self.display = display
#---- methods to process MIDI messages -------------------------------------
[docs] def note_on(self, channel, key, velocity):
"""Process a MIDI Note On event."""
log.debug("ControlLogic received note on: %d, %d", key, velocity)
row, col, bank = self.decode_mpd218_key(key)
# Each column maps to a specific winch.
# Rows 0 and 1 move forward, rows 2 and 3 move backward.
# The middle rows make small motions, the outer rows make larger motions.
delta = 5 * velocity if row <= 1 else -5 * velocity
if row == 0 or row == 3:
delta = delta * 8
self.winches.increment_target(col, delta)
self.winch_dir[col] = 1 if delta > 0 else -1 if delta < 0 else 0
return
[docs] def note_off(self, channel, key, velocity):
"""Process a MIDI Note Off event."""
log.debug("ControlLogic received note off: %d, %d", key, velocity)
row, col, bank = self.decode_mpd218_key(key)
self.winch_dir[col] = 0
self.winches.set_velocity(col, 0)
[docs] def control_change(self, channel, cc, value):
"""Process a MIDI Control Change event."""
knob, bank = self.decode_mpd218_cc(cc)
log.debug("Control change %d on knob %d bank %d", cc, knob, bank)
if knob == 1: # Knob #1 on MPD218, use to control resonant frequency
self.frequency = 0.05 + 0.1 * value
elif knob == 2: # Knob #2 on on MPD218, use to control damping ratio
self.damping_ratio = 0.05 + 0.01 * value
self.winches.set_freq_damping([0,1,2,3], self.frequency, self.damping_ratio)
self.display.set_status("Frequency: %f, damping ratio: %f" % (self.frequency, self.damping_ratio))
return
[docs] def channel_pressure(self, channel, pressure):
"""Process a MIDI Channel Pressure event."""
velocities = [direction * 10 * pressure for direction in self.winch_dir]
self.winches.set_velocity(self.all_axes, velocities)
log.debug("aftertouch: %d, velocities: %s", pressure, velocities)
################################################################
[docs]class SimWindow(QtWidgets.QMainWindow):
"""A custom main window which provides all GUI controls. This generally follows
a model-view-controller convention in which this window provides the views,
passing events to the application controller via callbacks.
"""
def __init__(self):
super(SimWindow,self).__init__()
# the graphical state
self.cartoons = list() # WinchCartoon objects
# create the GUI elements
self._setupUi()
# finish initialization
self.show()
return
# ------------------------------------------------------------------------------------------------
def _setupUi(self):
# basic window setup
self.setWindowTitle("KF System Simulator")
self.statusbar = QtWidgets.QStatusBar(self)
self.setStatusBar(self.statusbar)
# set up tabbed page structure
self.tabs = QtWidgets.QTabWidget()
self.setCentralWidget(self.tabs)
self.tabs.currentChanged.connect(self._tab_changed)
# set up the main tab
self.mainTab = QtWidgets.QWidget(self)
self.mainLayout = QtWidgets.QVBoxLayout(self.mainTab)
self.tabs.addTab(self.mainTab, 'Main')
# generate a horizontal array of winch cartoons
self.winchSet = kf.QtWinch.QtWinchSet()
self.mainLayout.addWidget(self.winchSet)
self.cartoons = self.winchSet.winches()
# generate a simulated MPD218 controller
self.MIDI_controller = kf.QtMPD218.QtMPD218()
self.mainLayout.addWidget(self.MIDI_controller)
# set up the configuration tab
self.configForm = kf.QtConfig.QtConfigForm()
self.tabs.addTab(self.configForm, 'Config')
self.oscListenerConfig = kf.QtConfig.QtConfigOSCPort()
self.configForm.addField("OSC message listener address:port", self.oscListenerConfig)
self.midiCombo = kf.QtConfig.QtConfigComboBox()
self.configForm.addField("MIDI input", self.midiCombo)
# set up the logging tab
self.logDisplay = kf.QtLog.QtLog(level=logging.INFO)
self.tabs.addTab(self.logDisplay, 'Log')
# set up a more complex cartoon showing a suspended plotter system
self.plotter = kf.QtPlotter.QtPlotterCartoon()
self.tabs.addTab(self.plotter, 'Plotter')
return
# --- configure callbacks to connect GUI to application controller -----------------------------
[docs] def connect_midi_processor(self, processor):
"""Connect an object to receive synthetic MIDI events; the object is assumed to have MIDIProcessor methods."""
self.MIDI_controller.connect_midi_processor(processor)
[docs] def connect_osc_listener(self, listener):
"""Connect an OSC network listener to the port configuration control."""
self.oscListenerConfig.callback = listener.set_OSC_port
[docs] def connect_midi_listener(self, listener):
"""Connect a MIDI input listener to the port configuration control."""
self.midiCombo.callback = listener.open_MIDI_input
self.midiCombo.set_items(listener.get_midi_port_names())
# --- window and Qt event processing -------------------------------------------------------------
[docs] def set_status(self, string):
"""Update the status bar at the bottom of the display to show the provided string."""
self.statusbar.showMessage(string)
return
def _tab_changed(self, index):
log.debug("Tab changed to %d", index)
return
[docs] def closeEvent(self, event):
"""Qt callback received before windows closes."""
log.info("Received window close event.")
super(SimWindow,self).closeEvent(event)
return
# --------------------------------------------------------------------------------------------------
################################################################
[docs]class MainApp(object):
"""Main application controller object holding any non-GUI related state."""
def __init__(self):
# create the interface window
self.window = SimWindow()
# initialize the winch set simulator
self.sim = kf.sim.SimWinch()
# Initialize the performance logic.
self.control_logic = ControlLogic()
self.control_logic.connect_winches(self.sim)
self.control_logic.connect_display(self.window)
self.window.connect_midi_processor(self.control_logic)
# Initialize the MIDI input system.
self.midi_listener = kf.midi.QtMIDIListener()
self.midi_listener.connect_midi_processor(self.control_logic)
self.window.connect_midi_listener(self.midi_listener)
# Initialize the OSC message dispatch system.
self.osc_listener = kf.osc.QtOSCListener()
self.osc_listener.map_handler("/midi", self._received_remote_midi)
self.osc_listener.open_receiver()
self.window.connect_osc_listener(self.osc_listener)
# start the animation timer
self.frame_timer = QtCore.QTimer()
self.frame_timer.start(40) # units are milliseconds
self.frame_timer.timeout.connect(self._timer_tick)
return
def _timer_tick(self):
# Method called at intervals by the animation timer to update the model and graphics.
self.sim.update_for_interval(0.040)
positions = self.sim.positions()
for pos, cartoon in zip(positions, self.window.cartoons):
cartoon.update_position(pos)
self.window.plotter.update_positions(positions[0:2])
# ---- process OSC network messages ---------------------------------------
# The midi_osc_bridge.py tool can tunnel MIDI messages over the network as
# OSC packets. This routes any bridged MIDI data into the same performance
# logic.
def _received_remote_midi(self, msgaddr, *args):
log.debug("remote midi: %s", " ".join([str(arg) for arg in args]))
self.control_logic.decode_message(args)
return
################################################################
def _main():
# initialize the Qt system itself
app = QtWidgets.QApplication(sys.argv)
# create the main application controller
main = MainApp()
# run the event loop until the user is done
log.info("Starting event loop.")
sys.exit(app.exec_())
################################################################
# Main script follows. This sequence is executed when the script is initiated from the command line.
if __name__ == "__main__":
_main()