Source code for sim1

#!/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()