We started with a piece of ‘pleated lines’ and a ‘pleated fan’ to work with. After trying out different combinations, we sewed the two pieces together and added cording to the fan. We originally intended to have shirring effects on the fan and have the circle made from the ‘pleated lines’ open and close. However, we struggled with shirring in a non linear direction on capstan and controlling circular motions. So we then decided to just keep vertical movements.

With the aid of software, we were able to explore symmetry and randomness. As shown in the video, we have the two lines controlling the circle move at the same speed, and the line controlling the fan moves with random behaviors. There are two pads with such features(left most column: second and third pad). Due to the randomness, if we just use the single pad to move all three lines up, the fan ends up with different shapes when the lines attached to the circle reach the top. In addition, each line can still be controlled individually with all the other pads.


(Changes are made in note_on and note_off)

################################################################
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

        # set up a metronome timer
        self.tempo = 60  # metronome rate in beats per minute
        self.metronome_timer = QtCore.QTimer()
        self.metronome_timer.start(int(60000/self.tempo))  # units are milliseconds
        self.metronome_timer.timeout.connect(self.metronome_tick)

        # state variables for the metronome process
        self.pulsing = [0,0,0,0]  # array of offsets for metronomic output pulsing
        return

    def connect_winches(self, winches):
        """Attach a winch output device to the performance logic, either physical or simulated."""
        self.winches = winches

    def connect_display(self, display):
        """Attach a console status output device to the performance logic."""
        self.display = display

    #---- methods related to the metronome -------------------------------------
    def set_metronome_tempo(self, tempo):
        """Adjust the metronome timer rate.  N.B. the underlying Qt QTimer accepts
        intervals in milliseconds, so this is fairly precise for beats but will
        be approximate for small subdivisions.  E.g. a 120 BPM timer at 500 ms
        is precise, but the 32nd note subdivision at 62.5 ms would be rounded to
        62 ms and run about 1% slow.

        :param tempo: tempo in BPM
        """
        self.tempo = tempo
        self.metronome_timer.setInterval(int(60000/self.tempo)) # units are milliseconds

    def metronome_tick(self):
        """Callback invoked at regular intervals governed by the metronome timer.  In
        this particular example, the metronome can trigger a regular series of
        alternating forward and back movements to excite the path generator
        oscillators.
        """
        # check if any pulsing is active, i.e., any value is non-zero
        if any(self.pulsing):
            # send out the next pulse movement command
            self.winches.increment_target(self.all_axes, self.pulsing)

            # and reverse the directions for the next iteration
            self.pulsing = [-d for d in self.pulsing]

    #---- methods to process MIDI messages -------------------------------------
    def note_on(self, channel, key, velocity):
        """Process a MIDI Note On event."""
        log.debug("ControlLogic received note on: %d, %d", key, velocity)
        print("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
        randDelta = random.randint(200,900)
        randSign = random.randint(-1,1)
        randSign = 1 if randSign&gt;0 else -1
        if bank == 0 or bank == 1: # bank A or B directly control the winches
            if col==0 and (row==0 or row==2):
                self.winches.increment_target(col, delta)
                self.winches.increment_target(col+1, delta)
                self.winches.increment_target(col+2, delta+randSign*randDelta)
                self.winch_dir[col] = 1 if delta &gt; 0 else -1 if delta < 0 else 0
                self.winch_dir[col+1] = 1 if delta &gt; 0 else -1 if delta < 0 else 0
                self.winch_dir[col+2] = 1 if delta+randSign*randDelta &gt; 0 else -1 if delta+randSign*randDelta < 0 else 0
                self.pulsing[col] = 0 # reset pulsing on this winch
                self.pulsing[col+1] = 0
                self.pulsing[col+2] = 0
            else:
                self.winches.increment_target(col, delta)
                self.winch_dir[col] = 1 if delta &gt; 0 else -1 if delta < 0 else 0
                self.pulsing[col] = 0 # reset pulsing on this winch

        else: # bank C pads instead invoke the metronome oscillation
            self.pulsing[col] = delta//8
            log.debug("Pulsing array now %s", self.pulsing)
            print("Pulsing array now %s", self.pulsing)

    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)
        if col==0 and (row==0 or row==2):
            self.winch_dir[col] = 0
            self.winch_dir[col+1] = 0
            self.winch_dir[col+2] = 0
            self.winches.set_velocity(col, 0)
            self.winches.set_velocity(col+1, 0)
            self.winches.set_velocity(col+2, 0)
        else:
            self.winch_dir[col] = 0
            self.winches.set_velocity(col, 0)

    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)
        print("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(self.all_axes, self.frequency, self.damping_ratio)
        self.display.set_status("Frequency: %f, damping ratio: %f" % (self.frequency, self.damping_ratio))
        return

    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)

################################################################
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 Controller: Ex 5")
        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)
        self.winchSelect = kf.QtConfig.QtConfigComboBox()
        self.configForm.addField("Winch serial port", self.winchSelect)

        # 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 -----------------------------
    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)

    def connect_osc_listener(self, listener):
        """Connect an OSC network listener to the port configuration control."""
        self.oscListenerConfig.callback = listener.set_OSC_port

    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.midi_port_names)

    def connect_winch(self, winch):
        """Connect a serial winch output to the port configuration control."""
        self.winchSelect.callback = winch.set_and_open_port
        self.winchSelect.set_items(winch.available_ports())
        return

    # --- window and Qt event processing -------------------------------------------------------------
    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

    def closeEvent(self, event):
        """Qt callback received before windows closes."""
        log.info("Received window close event.")
        super(SimWindow,self).closeEvent(event)
        return
    # --------------------------------------------------------------------------------------------------