changeset 53:ff5d65c69e73

Updates to control passthrough, log playback, firmware update ability
author Andrew McPherson <andrewm@eecs.qmul.ac.uk>
date Mon, 02 Jan 2017 22:29:39 +0000
parents 18af05164894
children e468cb91794a 19650b4076ee
files Source/GUI/KeyboardZoneComponent.cpp Source/GUI/KeyboardZoneComponent.h Source/GUI/MainWindow.cpp Source/GUI/MainWindow.h Source/MainApplicationController.cpp Source/MainApplicationController.h Source/Mappings/Vibrato/TouchkeyVibratoMapping.cpp Source/TouchKeys/LogPlayback.cpp Source/TouchKeys/MidiKeyboardSegment.cpp Source/TouchKeys/MidiKeyboardSegment.h Source/TouchKeys/TouchkeyDevice.cpp Source/TouchKeys/TouchkeyDevice.h
diffstat 12 files changed, 330 insertions(+), 54 deletions(-) [+]
line wrap: on
line diff
--- a/Source/GUI/KeyboardZoneComponent.cpp	Tue May 12 18:19:05 2015 +0100
+++ b/Source/GUI/KeyboardZoneComponent.cpp	Mon Jan 02 22:29:39 2017 +0000
@@ -644,6 +644,7 @@
     menu.addItem(MidiKeyboardSegment::kControlPitchWheel, "Pitch Wheel", true, keyboardSegment_->usesKeyboardPitchWheel());
     menu.addItem(MidiKeyboardSegment::kControlChannelAftertouch, "Aftertouch", true, keyboardSegment_->usesKeyboardChannnelPressure());
     menu.addItem(1, "CC 1 (Mod Wheel)", true, keyboardSegment_->usesKeyboardModWheel());
+    menu.addItem(kKeyboardControllerRetransmitPedals, "Pedals", true, keyboardSegment_->usesKeyboardPedals());
     menu.addItem(kKeyboardControllerRetransmitOthers, "Other Controllers", true, keyboardSegment_->usesKeyboardMIDIControllers());
 
     menu.showMenuAsync(PopupMenu::Options().withTargetComponent(keyboardControllersButton),
@@ -682,6 +683,9 @@
     else if(result == 1) { // ModWheel == CC 1
         keyboardSegment_->setUsesKeyboardModWheel(!keyboardSegment_->usesKeyboardModWheel());
     }
+    else if(result == kKeyboardControllerRetransmitPedals) {
+        keyboardSegment_->setUsesKeyboardPedals(!keyboardSegment_->usesKeyboardPedals());
+    }
     else if(result == kKeyboardControllerRetransmitOthers) {
         keyboardSegment_->setUsesKeyboardMIDIControllers(!keyboardSegment_->usesKeyboardMIDIControllers());
     }
--- a/Source/GUI/KeyboardZoneComponent.h	Tue May 12 18:19:05 2015 +0100
+++ b/Source/GUI/KeyboardZoneComponent.h	Mon Jan 02 22:29:39 2017 +0000
@@ -122,7 +122,8 @@
     enum {
         // Special commands for keyboard controller popup button
         kKeyboardControllerRetransmitOthers = 2000,
-        kKeyboardControllerSendPitchWheelRange
+        kKeyboardControllerSendPitchWheelRange,
+        kKeyboardControllerRetransmitPedals
     };
 
     // Update list of MIDI output devices
--- a/Source/GUI/MainWindow.cpp	Tue May 12 18:19:05 2015 +0100
+++ b/Source/GUI/MainWindow.cpp	Mon Jan 02 22:29:39 2017 +0000
@@ -129,6 +129,9 @@
 #ifdef ENABLE_TOUCHKEYS_SENSOR_TEST
         menu.addCommandItem(&commandManager_, kCommandTestTouchkeySensors);
 #endif
+#ifdef ENABLE_TOUCHKEYS_FIRMWARE_UPDATE
+        menu.addCommandItem(&commandManager_, kCommandJumpToBootloader);
+#endif
         menu.addSeparator();
         menu.addCommandItem(&commandManager_, kCommandPreferences);
     }
@@ -174,6 +177,9 @@
 #ifdef ENABLE_TOUCHKEYS_SENSOR_TEST
         kCommandTestTouchkeySensors,
 #endif
+#ifdef ENABLE_TOUCHKEYS_FIRMWARE_UPDATE
+        kCommandJumpToBootloader,
+#endif
         kCommandPreferences,
         
         // Window
@@ -267,12 +273,12 @@
         case kCommandLoggingStartStop:
             result.setInfo("Record Log File", "Records TouchKeys and MIDI data to file", controlCategory, 0);
             result.setTicked(controller_.isLogging());
-            result.setActive(true);
+            result.setActive(!controller_.isPlayingLog());
             break;
         case kCommandLoggingPlay:
             result.setInfo("Play Log...", "Plays TouchKeys and MIDI from file", controlCategory, 0);
-            result.setTicked(false);
-            result.setActive(false);
+            result.setTicked(controller_.isPlayingLog());
+            result.setActive(!controller_.isLogging());
             break;
         case kCommandEnableExperimentalMappings:
             result.setInfo("Enable Experimental Mappings", "Enables mappings which are still experimental", controlCategory, 0);
@@ -285,6 +291,13 @@
             result.setTicked(controller_.touchkeySensorTestIsRunning());
             break;
 #endif
+#ifdef ENABLE_TOUCHKEYS_FIRMWARE_UPDATE
+        case kCommandJumpToBootloader:
+            result.setInfo("Go to Firmware Update Mode", "Puts the TouchKeys in firmware update mode", controlCategory, 0);
+            result.setActive(controller_.availableTouchkeyDevices().size() > 0);
+            result.setTicked(false);
+            break;
+#endif
         case kCommandPreferences:
             result.setInfo("Preferences...", "General application preferences", controlCategory, 0);
             result.setTicked(false);
@@ -337,7 +350,10 @@
                 controller_.startLogging();
             break;
         case kCommandLoggingPlay:
-            // TODO
+            if(controller_.isPlayingLog())
+                controller_.stopPlayingLog();
+            else
+                controller_.playLogWithDialog();
             break;
         case kCommandEnableExperimentalMappings:
             controller_.setExperimentalMappingsEnabled(!controller_.experimentalMappingsEnabled());
@@ -350,6 +366,11 @@
                 controller_.touchkeySensorTestStop();
             break;
 #endif
+#ifdef ENABLE_TOUCHKEYS_FIRMWARE_UPDATE
+        case kCommandJumpToBootloader:
+            controller_.touchkeyJumpToBootloader(mainComponent_.currentTouchkeysSelectedPath().toUTF8());
+            break;
+#endif
         case kCommandPreferences:
             controller_.showPreferencesWindow();
             break;
--- a/Source/GUI/MainWindow.h	Tue May 12 18:19:05 2015 +0100
+++ b/Source/GUI/MainWindow.h	Mon Jan 02 22:29:39 2017 +0000
@@ -54,6 +54,7 @@
         kCommandLoggingPlay,
         kCommandEnableExperimentalMappings,
         kCommandTestTouchkeySensors,
+        kCommandJumpToBootloader,
         kCommandPreferences,
         
         // Window menu
--- a/Source/MainApplicationController.cpp	Tue May 12 18:19:05 2015 +0100
+++ b/Source/MainApplicationController.cpp	Mon Jan 02 22:29:39 2017 +0000
@@ -40,6 +40,7 @@
   oscReceiver_(0, "/touchkeys"),
   touchkeyController_(keyboardController_),
   touchkeyEmulator_(keyboardController_, oscReceiver_),
+  logPlayback_(0),
 #ifdef TOUCHKEY_ENTROPY_GENERATOR_ENABLE
   touchkeyEntropyGenerator_(keyboardController_),
   entropyGeneratorSelected_(false),
@@ -59,7 +60,8 @@
   preferencesWindow_(0),
 #endif
   segmentCounter_(0),
-  loggingActive_(false)
+  loggingActive_(false),
+  isPlayingLog_(false)
 {
     // Set our OSC controller
     setOscController(&keyboardController_);
@@ -105,6 +107,8 @@
     if(touchkeySensorTestIsRunning())
         touchkeySensorTestStop();
 #endif
+    if(logPlayback_ != 0)
+        delete logPlayback_;
     removeAllOscListeners();
     midiInputController_.removeAllSegments();   // Remove segments now to avoid deletion-order problems
     delete mainOscController_;
@@ -456,6 +460,61 @@
     loggingDirectory_ = directory;
 }
 
+void MainApplicationController::playLogWithDialog() {
+    if(isPlayingLog_)
+        return;
+    
+    FileChooser tkChooser ("Select TouchKeys log...",
+                           File::nonexistent, // File::getSpecialLocation (File::userHomeDirectory),
+                           "*.bin");
+    if(tkChooser.browseForFileToOpen()) {
+        FileChooser midiChooser ("Select MIDI log...",
+                               File::nonexistent, // File::getSpecialLocation (File::userHomeDirectory),
+                               "*.bin");
+        if(midiChooser.browseForFileToOpen()) {
+            logPlayback_ = new LogPlayback(keyboardController_, midiInputController_);
+            if(logPlayback_ == 0)
+                return;
+            
+            if(logPlayback_->openLogFiles(tkChooser.getResult().getFullPathName().toRawUTF8(), midiChooser.getResult().getFullPathName().toRawUTF8())) {
+                logPlayback_->startPlayback();
+                isPlayingLog_ = true;
+#ifndef TOUCHKEYS_NO_GUI
+                // Always show 88 keys for log playback since we won't know which keys were actually recorded
+                keyboardDisplay_.setKeyboardRange(21, 108);
+                if(keyboardDisplayWindow_ != 0) {
+                    keyboardDisplayWindow_->getConstrainer()->setFixedAspectRatio(keyboardDisplay_.keyboardAspectRatio());
+                    
+                    Rectangle<int> bounds = keyboardDisplayWindow_->getBounds();
+                    if(bounds.getY() < 44)
+                        bounds.setY(44);
+                    keyboardDisplayWindow_->setBoundsConstrained(bounds);
+                }
+                showKeyboardDisplayWindow();
+#endif
+            }
+        }
+    }
+}
+
+void MainApplicationController::stopPlayingLog() {
+    if(!isPlayingLog_)
+        return;
+    
+    if(logPlayback_ != 0) {
+        logPlayback_->stopPlayback();
+        logPlayback_->closeLogFiles();
+        delete logPlayback_;
+        logPlayback_ = 0;
+    }
+    
+#ifndef TOUCHKEYS_NO_GUI
+    keyboardDisplay_.clearAllTouches();
+#endif
+    midiInputController_.allNotesOff();
+    isPlayingLog_ = false;
+}
+
 // Add a new MIDI keyboard segment. This method also handles numbering of the segments
 MidiKeyboardSegment* MainApplicationController::midiSegmentAdd() {
     // For now, the segment counter increments with each new segment. Eventually, we could
@@ -1202,6 +1261,31 @@
 
 #endif // ENABLE_TOUCHKEYS_SENSOR_TEST
 
+#ifdef ENABLE_TOUCHKEYS_FIRMWARE_UPDATE
+// Put TouchKeys controller board into bootloader mode, for receiving firmware updates
+// (supplied by a different utility)
+bool MainApplicationController::touchkeyJumpToBootloader(const char *path) {
+    // First, close the existing device which stops the data autogathering
+    closeTouchkeyDevice();
+    
+    // Now reopen the TouchKeys device
+    if(!touchkeyController_.openDevice(path)) {
+        touchkeyErrorMessage_ = "Failed to open";
+        touchkeyErrorOccurred_ = true;
+        return false;
+    }
+    
+    touchkeyController_.jumpToBootloader();
+    
+    // Set an "error" condition to display this message, and because
+    // after jumping to bootloader mode, the device will not open properly
+    // until it has been reset.
+    touchkeyErrorMessage_ = "Firmware update mode";
+    touchkeyErrorOccurred_ = true;
+    return true;
+}
+#endif // ENABLE_TOUCHKEYS_FIRMWARE_UPDATE
+
 // Return the name of a MIDI note given its number
 std::string MainApplicationController::midiNoteName(int noteNumber) {
     if(noteNumber < 0 || noteNumber > 127)
--- a/Source/MainApplicationController.h	Tue May 12 18:19:05 2015 +0100
+++ b/Source/MainApplicationController.h	Mon Jan 02 22:29:39 2017 +0000
@@ -259,6 +259,12 @@
     bool isLogging() { return loggingActive_; }
     void setLoggingDirectory(const char *directory);
     
+    // Playback methods for log files
+
+    void playLogWithDialog();
+    void stopPlayingLog();
+    bool isPlayingLog() { return isPlayingLog_; }
+    
     // *** OSC handler method (different from OSC device selection) ***
     
 	bool oscHandlerMethod(const char *path, const char *types, int numValues, lo_arg **values, void *data);
@@ -333,6 +339,11 @@
     void touchkeySensorTestResetState();
 #endif
     
+#ifdef ENABLE_TOUCHKEYS_FIRMWARE_UPDATE
+    // Put TouchKeys controller board into bootloader mode
+    bool touchkeyJumpToBootloader(const char *path);
+#endif
+    
     // *** Static utility methods ***
     static std::string midiNoteName(int noteNumber);
     static int midiNoteNumberForName(std::string const& name);
@@ -353,6 +364,7 @@
     OscReceiver oscReceiver_;
     TouchkeyDevice touchkeyController_;
     TouchkeyOscEmulator touchkeyEmulator_;
+    LogPlayback *logPlayback_;
 #ifdef TOUCHKEY_ENTROPY_GENERATOR_ENABLE
     TouchkeyEntropyGenerator touchkeyEntropyGenerator_;
     bool entropyGeneratorSelected_;
@@ -385,7 +397,7 @@
     int segmentCounter_;
     
     // Logging info
-    bool loggingActive_;
+    bool loggingActive_, isPlayingLog_;
     std::string loggingDirectory_;
 };
 
--- a/Source/Mappings/Vibrato/TouchkeyVibratoMapping.cpp	Tue May 12 18:19:05 2015 +0100
+++ b/Source/Mappings/Vibrato/TouchkeyVibratoMapping.cpp	Mon Jan 02 22:29:39 2017 +0000
@@ -253,6 +253,7 @@
                             
                             //distance = fabsf(lastY_ - onsetLocationY_);
                             distance = lastY_ - onsetLocationY_;
+                            //distance = 0; // TESTING
                         }
                         else {
                             // Euclidean distance between points
--- a/Source/TouchKeys/LogPlayback.cpp	Tue May 12 18:19:05 2015 +0100
+++ b/Source/TouchKeys/LogPlayback.cpp	Mon Jan 02 22:29:39 2017 +0000
@@ -101,6 +101,8 @@
             timestampOffset_ = playbackScheduler_.currentTimestamp() - firstMidiTimestamp;
     }
     
+    cout << "Touch " << firstTouchTimestamp << " MIDI " << firstMidiTimestamp << " offset " << timestampOffset_ << endl;
+    
     playing_ = true;
     paused_ = false;
     
@@ -277,7 +279,7 @@
             }
         }
         
-        readNextTouchFrame();
+        newTouchFound = readNextTouchFrame();
     }
     
     if(!newTouchFound) { // EOF or error
@@ -297,8 +299,12 @@
     // TODO: handle playback rate
     
     // Play the most recent stored touch frame
-    if(nextMidi_.size() >= 3)
-        midiInputController_.handleIncomingMidiMessage(0, MidiMessage(nextMidi_[0], nextMidi_[1], nextMidi_[2]));
+    if(nextMidi_.size() >= 3) {
+        if((nextMidi_[0] & 0xF0) == 0xD0) // channel aftertouch has 2 bytes
+                midiInputController_.handleIncomingMidiMessage(0, MidiMessage(nextMidi_[0], nextMidi_[1]));
+        else
+            midiInputController_.handleIncomingMidiMessage(0, MidiMessage(nextMidi_[0], nextMidi_[1], nextMidi_[2]));
+    }
     //midiInputController_.rtMidiCallback(nextMidiTimestamp_ - lastMidiTimestamp_, &nextMidi_, 0);
     lastMidiTimestamp_ = nextMidiTimestamp_;
     
@@ -306,8 +312,12 @@
     
     // Go through next touch frames and send them as long as the timestamp is not in the future
     while(newMidiEventFound && (nextMidiTimestamp_ + timestampOffset_) <= playbackScheduler_.currentTimestamp()) {
-        if(nextMidi_.size() >= 3)
-            midiInputController_.handleIncomingMidiMessage(0, MidiMessage(nextMidi_[0], nextMidi_[1], nextMidi_[2]));
+        if(nextMidi_.size() >= 3) {
+            if((nextMidi_[0] & 0xF0) == 0xD0) // channel aftertouch has 2 bytes
+                midiInputController_.handleIncomingMidiMessage(0, MidiMessage(nextMidi_[0], nextMidi_[1]));
+            else
+                midiInputController_.handleIncomingMidiMessage(0, MidiMessage(nextMidi_[0], nextMidi_[1], nextMidi_[2]));
+        }
         //midiInputController_.rtMidiCallback(nextMidiTimestamp_ - lastMidiTimestamp_, &nextMidi_, 0);
         lastMidiTimestamp_ = nextMidiTimestamp_;
         
--- a/Source/TouchKeys/MidiKeyboardSegment.cpp	Tue May 12 18:19:05 2015 +0100
+++ b/Source/TouchKeys/MidiKeyboardSegment.cpp	Mon Jan 02 22:29:39 2017 +0000
@@ -42,6 +42,7 @@
 #undef DEBUG_MIDI_KEYBOARD_SEGMENT
 
 const int MidiKeyboardSegment::kMidiControllerDamperPedal = 64;
+const int MidiKeyboardSegment::kMidiControllerSostenutoPedal = 66;
 const int MidiKeyboardSegment::kPedalActiveValue = 64;
 
 // Factores to use
@@ -57,7 +58,8 @@
   noteMin_(0), noteMax_(127), outputChannelLowest_(0), outputTransposition_(0),
   damperPedalEnabled_(true), touchkeyStandaloneMode_(false),
   usesKeyboardChannelPressure_(false), usesKeyboardPitchWheel_(false),
-  usesKeyboardModWheel_(false), usesKeyboardMidiControllers_(false),
+  usesKeyboardModWheel_(false), usesKeyboardPedals_(true),
+  usesKeyboardMidiControllers_(false),
   pitchWheelRange_(2.0), useVoiceStealing_(false)
 {
 	// Register for OSC messages from the internal keyboard source
@@ -163,6 +165,7 @@
 // If in polyphonic mode, send to all channels; otherwise send only
 // to the channel in question.
 void MidiKeyboardSegment::sendMidiPitchWheelRange() {
+    // MPE-TODO
     if(mode_ == ModePolyphonic) {
         for(int i = outputChannelLowest_; i < outputChannelLowest_ + retransmitMaxPolyphony_; i++)
             sendMidiPitchWheelRangeHelper(i);
@@ -216,6 +219,8 @@
         setModeMonophonic();
     else if(mode == ModePolyphonic)
         setModePolyphonic();
+    else if(mode == ModeMPE)
+        setModeMPE();
     else
         setModeOff();
 }
@@ -260,6 +265,28 @@
     modePolyphonicSetupHelper();
 }
 
+void MidiKeyboardSegment::setModeMPE() {
+    // First turn off any notes in the current mode
+    allNotesOff();
+    
+    // MPE-TODO some things need to be set to master-zone retransmit
+    // also reset pitch wheel value to 0 since it's sent separately
+    setAllControllerActionsTo(kControlActionBroadcast);
+    
+    // Register a callback for touchkey data.  When we get a note-on message,
+    // we request this callback occur once touch data is available.  In this mode,
+    // we know the eventual channel before any touch data ever occurs: thus, we
+    // only listen to the MIDI onset itself, which happens after all the touch
+    // data is sent out.
+    addOscListener("/midi/noteon");
+    
+    mode_ = ModeMPE;
+    
+    // MPE-TODO
+    // Set RPN 6 to enable MPE with the appropriate zone
+    
+}
+
 // Set the maximum polyphony, affecting polyphonic mode only
 void MidiKeyboardSegment::setPolyphony(int polyphony) {
     // First turn off any notes if this affects current polyphonic mode
@@ -273,7 +300,13 @@
         retransmitMaxPolyphony_ = 16;
     else
         retransmitMaxPolyphony_ = polyphony;
-    modePolyphonicSetupHelper();
+    
+    // MPE-TODO
+    // Send RPN 6 to change the zone configuration
+    // -- maybe in modePolyphonicSetupHelper()
+    
+    if(mode_ == ModePolyphonic)
+        modePolyphonicSetupHelper();
 }
 
 // Set whether the damper pedal is enabled or not
@@ -287,6 +320,13 @@
     damperPedalEnabled_ = enable;
 }
 
+// Set the lowest output channel
+void MidiKeyboardSegment::setOutputChannelLowest(int ch) {
+    // FIXME this is probably broken for polyphonic mode!
+    // MPE-TODO: send new RPN 6 for disabling old zone and creating new one
+    outputChannelLowest_ = ch;
+}
+
 // Handle an incoming MIDI message
 void MidiKeyboardSegment::midiHandlerMethod(MidiInput* source, const MidiMessage& message) {
     // Log the timestamps of note onsets and releases, regardless of the mode
@@ -300,7 +340,8 @@
         // (damper pedal enabled) && (pedal is down) && (polyphonic mode)
         // In this condition, onsets will be removed when note goes off
         if(message.getNoteNumber() >= 0 && message.getNoteNumber() < 128) {
-            if(!damperPedalEnabled_ || controllerValues_[kMidiControllerDamperPedal] < kPedalActiveValue || mode_ != ModePolyphonic) {
+            if(!damperPedalEnabled_ || controllerValues_[kMidiControllerDamperPedal] < kPedalActiveValue ||
+               (mode_ != ModePolyphonic && mode_ != ModeMPE)) {
                 noteOnsetTimestamps_[message.getNoteNumber()] = 0;
             }
         }
@@ -321,8 +362,17 @@
         }
         
         if(message.getControllerNumber() >= 0 && message.getControllerNumber() < 128) {
-            if((message.getControllerNumber() == 1 && usesKeyboardModWheel_) ||
-               (message.getControllerNumber() != 1 && usesKeyboardMidiControllers_)) {
+            if(message.getControllerNumber() == 1 && usesKeyboardModWheel_) {
+                controllerValues_[message.getControllerNumber()] = message.getControllerValue();
+                handleControlChangeRetransit(message.getControllerNumber(), message);
+            }
+            else if(message.getControllerNumber() >= 64 && message.getControllerNumber() <= 69
+                     && usesKeyboardPedals_) {
+                // MPE-TODO send this on master zone
+                controllerValues_[message.getControllerNumber()] = message.getControllerValue();
+                handleControlChangeRetransit(message.getControllerNumber(), message);
+            }
+            else if(usesKeyboardMidiControllers_) {
                 controllerValues_[message.getControllerNumber()] = message.getControllerValue();
                 handleControlChangeRetransit(message.getControllerNumber(), message);
             }
@@ -336,8 +386,13 @@
     }
     else if(message.isPitchWheel()) {
         if(usesKeyboardPitchWheel_) {
-            controllerValues_[kControlPitchWheel] = message.getPitchWheelValue();
-            handleControlChangeRetransit(kControlPitchWheel, message);
+            if(mode_ == ModeMPE) {
+                // MPE-TODO send this on master zone instead of putting it into the calculations
+            }
+            else {
+                controllerValues_[kControlPitchWheel] = message.getPitchWheelValue();
+                handleControlChangeRetransit(kControlPitchWheel, message);
+            }
         }
     }
     else {
@@ -352,6 +407,9 @@
             case ModePolyphonic:
                 modePolyphonicHandler(source, message);
                 break;
+            case ModeMPE:
+                modeMPEHandler(source, message);
+                break;
             case ModeOff:
             default:
                 // Ignore message
@@ -389,8 +447,8 @@
         }
     }
     
-	if(mode_ == ModePolyphonic) {
-		modePolyphonicNoteOnCallback(path, types, numValues, values);
+	if(mode_ == ModePolyphonic || mode_ == ModeMPE) {
+		modePolyphonicMPENoteOnCallback(path, types, numValues, values);
 	}
 	
 	return true;
@@ -541,14 +599,15 @@
     }
     else if(!strcmp(path, "/set-controller-pass")) {
         // Set which controllers to pass through
-        // Arguments: (channel pressure), (pitch wheel), (mod wheel), (other CCs)
+        // Arguments: (channel pressure), (pitch wheel), (mod wheel), (pedals), (other CCs)
         
-        if(numValues >= 4) {
-            if(types[0] == 'i' && types[1] == 'i' && types[2] == 'i' && types[3] == 'i') {
+        if(numValues >= 5) {
+            if(types[0] == 'i' && types[1] == 'i' && types[2] == 'i' && types[3] == 'i' && types[4] == 'i') {
                 setUsesKeyboardChannelPressure(values[0]->i != 0);
                 setUsesKeyboardPitchWheel(values[1]->i != 0);
                 setUsesKeyboardModWheel(values[2]->i != 0);
-                setUsesKeyboardMIDIControllers(values[3]->i != 0);
+                setUsesKeyboardPedals(values[3]->i != 0);
+                setUsesKeyboardMIDIControllers(values[4]->i != 0);
                 
                 return OscTransmitter::createSuccessMessage();
             }
@@ -590,6 +649,8 @@
                     setModeMonophonic();
                 else if(!strncmp(mode, "poly", 4))
                     setModePolyphonic();
+                else if(!strncmp(mode, "mpe", 3))
+                    setModeMPE();
                 else
                     return OscTransmitter::createFailureMessage();
 
@@ -861,6 +922,7 @@
     properties.setValue("usesKeyboardChannelPressure", usesKeyboardChannelPressure_);
     properties.setValue("usesKeyboardPitchWheel", usesKeyboardPitchWheel_);
     properties.setValue("usesKeyboardModWheel", usesKeyboardModWheel_);
+    properties.setValue("usesKeyboardPedals", usesKeyboardPedals_);
     properties.setValue("usesKeyboardMidiControllers", usesKeyboardMidiControllers_);
     properties.setValue("pitchWheelRange", pitchWheelRange_);
     properties.setValue("retransmitMaxPolyphony", retransmitMaxPolyphony_);
@@ -904,6 +966,8 @@
         setModeMonophonic();
     else if(mode == ModePolyphonic)
         setModePolyphonic();
+    else if(mode == ModeMPE)
+        setModeMPE();
     else // Off or unknown
         setModeOff();
     if(!properties.containsKey("channelMask"))
@@ -933,6 +997,10 @@
     if(!properties.containsKey("usesKeyboardModWheel"))
         return false;
     usesKeyboardModWheel_ = properties.getBoolValue("usesKeyboardModWheel");
+    if(properties.containsKey("usesKeyboardPedals"))
+        usesKeyboardPedals_ = properties.getBoolValue("usesKeyboardPedals");
+    else
+        usesKeyboardPedals_ = false;    // For backwards compatibility with older versions
     if(!properties.containsKey("usesKeyboardMidiControllers"))
         return false;
     usesKeyboardMidiControllers_ = properties.getBoolValue("usesKeyboardMidiControllers");
@@ -941,7 +1009,7 @@
     pitchWheelRange_ = properties.getDoubleValue("pitchWheelRange");
     if(!properties.containsKey("retransmitMaxPolyphony"))
         return false;
-    retransmitMaxPolyphony_ = properties.getIntValue("retransmitMaxPolyphony");
+    setPolyphony(properties.getIntValue("retransmitMaxPolyphony"));
     if(!properties.containsKey("useVoiceStealing"))
         return false;
     useVoiceStealing_ = properties.getBoolValue("useVoiceStealing");
@@ -1141,6 +1209,22 @@
 void MidiKeyboardSegment::modePolyphonicNoteOn(unsigned char note, unsigned char velocity) {
     int newChannel = -1;
     
+#ifdef DEBUG_MIDI_KEYBOARD_SEGMENT
+    cout << "Channels available: ";
+    for(set<int>::iterator it = retransmitChannelsAvailable_.begin();
+        it != retransmitChannelsAvailable_.end(); ++it) {
+        cout << *it << " ";
+    }
+    cout << endl;
+    
+    cout << "Channels allocated: ";
+    for(map<int, int>::iterator it = retransmitChannelForNote_.begin();
+        it != retransmitChannelForNote_.end(); ++it) {
+        cout << it->second << "(" << it->first << ") ";
+    }
+    cout << endl;
+#endif
+    
     if(retransmitNotesHeldInPedal_.count(note) > 0) {
         // For notes that are still sounding in the pedal, reuse the same MIDI channel
         // they had before.
@@ -1173,13 +1257,6 @@
                     cout << "Stealing note " << oldNote << " from pedal for note " << (int)note << endl;
 #endif
                     modePolyphonicNoteOff(oldNote, true);
-                    if(oldChannel >= 0) {
-                        //midiOutputController_->sendControlChange(outputPortNumber_, oldChannel, kMidiControllerDamperPedal, 0);
-                        //midiOutputController_->sendControlChange(outputPortNumber_, oldChannel, kMidiControlAllNotesOff, 0);
-                        //midiOutputController_->sendControlChange(outputPortNumber_, oldChannel, kMidiControlAllSoundOff, 0);
-                        //midiOutputController_->sendControlChange(outputPortNumber_, oldChannel, kMidiControllerDamperPedal,
-                        //                                         controllerValues_[kMidiControllerDamperPedal]);
-                    }
                 }
             }
             
@@ -1202,13 +1279,6 @@
                     cout << "Stealing note " << oldNote << " for note " << (int)note << endl;
 #endif
                     modePolyphonicNoteOff(oldNote, true);
-                    if(oldChannel >= 0) {
-                        //midiOutputController_->sendControlChange(outputPortNumber_, oldChannel, kMidiControllerDamperPedal, 0);
-                        //midiOutputController_->sendControlChange(outputPortNumber_, oldChannel, kMidiControlAllNotesOff, 0);
-                        //midiOutputController_->sendControlChange(outputPortNumber_, oldChannel, kMidiControlAllSoundOff, 0);
-                        //midiOutputController_->sendControlChange(outputPortNumber_, oldChannel, kMidiControllerDamperPedal,
-                        //                                         controllerValues_[kMidiControllerDamperPedal]);
-                    }
                 }
                 else {
                     // No channels available.  Print a warning and finish
@@ -1247,12 +1317,31 @@
 	if(keyboard_.key(note) != 0) {
 		keyboard_.key(note)->midiNoteOff(this, keyboard_.schedulerCurrentTimestamp());
 	}
-	
-	// Send a Note Off message to the appropriate channel
-	if(midiOutputController_ != 0) {
-		midiOutputController_->sendNoteOff(outputPortNumber_, retransmitChannelForNote_[note], note + outputTransposition_);
-	}
-	
+
+    int oldNoteChannel = retransmitChannelForNote_[note];
+    
+    if(midiOutputController_ != 0) {
+        if(forceOff) {
+            // To silence a note, we need to clear any pedals that might be holding it
+            if(controllerValues_[kMidiControllerDamperPedal] >= kPedalActiveValue) {
+                midiOutputController_->sendControlChange(outputPortNumber_, oldNoteChannel,
+                                                         kMidiControllerDamperPedal, 0);
+            }
+            if(controllerValues_[kMidiControllerSostenutoPedal] >= kPedalActiveValue) {
+                midiOutputController_->sendControlChange(outputPortNumber_, oldNoteChannel,
+                                                         kMidiControllerSostenutoPedal, 0);
+            }
+            
+            // Send All Notes Off and All Sound Off
+            midiOutputController_->sendControlChange(outputPortNumber_, oldNoteChannel, kMidiControlAllNotesOff, 0);
+            midiOutputController_->sendControlChange(outputPortNumber_, oldNoteChannel, kMidiControlAllSoundOff, 0);
+        }
+        else {
+            // Send a Note Off message to the appropriate channel
+            midiOutputController_->sendNoteOff(outputPortNumber_, oldNoteChannel, note + outputTransposition_);
+        }
+    }
+    
     // If the pedal is enabled and currently active, don't re-enable this channel
     // just yet. Instead, let the note continue ringing until we have to steal it later.
     if(damperPedalEnabled_ && controllerValues_[kMidiControllerDamperPedal] >= kPedalActiveValue && !forceOff) {
@@ -1267,6 +1356,20 @@
         if(note >= 0 && note < 128)
             noteOnsetTimestamps_[note] = 0;
     }
+    
+    if(forceOff) {
+        // Now re-enable any pedals that we might have temporarily lifted on this channel
+        if(controllerValues_[kMidiControllerDamperPedal] >= kPedalActiveValue) {
+            midiOutputController_->sendControlChange(outputPortNumber_, oldNoteChannel,
+                                                     kMidiControllerDamperPedal,
+                                                     controllerValues_[kMidiControllerDamperPedal]);
+        }
+        if(controllerValues_[kMidiControllerSostenutoPedal] >= kPedalActiveValue) {
+            midiOutputController_->sendControlChange(outputPortNumber_, oldNoteChannel,
+                                                     kMidiControllerSostenutoPedal,
+                                                     controllerValues_[kMidiControllerSostenutoPedal]);
+        }
+    }
 }
 
 // Callback function after we request a note on.  PianoKey class will respond
@@ -1274,7 +1377,7 @@
 // indicating an absence of touch data.  Once we receive this, we can send the
 // MIDI note on message.
 
-void MidiKeyboardSegment::modePolyphonicNoteOnCallback(const char *path, const char *types, int numValues, lo_arg **values) {
+void MidiKeyboardSegment::modePolyphonicMPENoteOnCallback(const char *path, const char *types, int numValues, lo_arg **values) {
 	if(numValues < 3)	// Sanity check: first 3 values hold MIDI information
 		return;
 	if(types[0] != 'i' || types[1] != 'i' || types[2] != 'i')
@@ -1298,10 +1401,29 @@
 	}
 }
 
+// MPE (Multidimensional Polyphonic Expression): Each incoming note gets its own unique MIDI channel.
+// Like polyphonic mode but implementing the details of the MPE specification which differ subtly
+// from a straightforward polyphonic allocation
+void MidiKeyboardSegment::modeMPEHandler(MidiInput* source, const MidiMessage& message) {
+    // MPE-TODO
+}
+
+// Handle note on message in MPE mode.  Allocate a new channel
+// for this note and rebroadcast it.
+void MidiKeyboardSegment::modeMPENoteOn(unsigned char note, unsigned char velocity) {
+    // MPE-TODO
+    // allocate notes to channels like polyphonic mode, with certain changes:
+    // -- round-robin as default rather than first available
+    // -- different stealing behaviour:
+    // ---- when no channels are available, add to an existing one with the fewest sounding notes
+    // ---- old note doesn't need to be turned off, but it could(?) have its mappings disabled
+}
+
 // Private helper method to handle changes in polyphony
 void MidiKeyboardSegment::modePolyphonicSetupHelper() {
-    if(retransmitMaxPolyphony_ > 16)
-		retransmitMaxPolyphony_ = 16;	// Limit polyphony to 16 (number of MIDI channels
+    // Limit polyphony to 16 (number of MIDI channels) or fewer if starting above channel 1
+    if(retransmitMaxPolyphony_ + outputChannelLowest_ > 16)
+		retransmitMaxPolyphony_ = 16 - outputChannelLowest_;
     retransmitChannelsAvailable_.clear();
 	for(int i = outputChannelLowest_; i < outputChannelLowest_ + retransmitMaxPolyphony_; i++)
 		retransmitChannelsAvailable_.insert(i);
@@ -1394,6 +1516,7 @@
 // retransit or not to outgoing MIDI channels depending on the current behaviour defined in
 // controllerActions_.
 void MidiKeyboardSegment::handleControlChangeRetransit(int controllerNumber, const MidiMessage& message) {
+    // MPE-TODO need a new mode for sending on master zone, e.g. for pitch wheel
     if(midiOutputController_ == 0)
         return;
     if(controllerActions_[controllerNumber] == kControlActionPassthrough) {
--- a/Source/TouchKeys/MidiKeyboardSegment.h	Tue May 12 18:19:05 2015 +0100
+++ b/Source/TouchKeys/MidiKeyboardSegment.h	Mon Jan 02 22:29:39 2017 +0000
@@ -48,6 +48,7 @@
 class MidiKeyboardSegment : public OscHandler {
 private:
     static const int kMidiControllerDamperPedal;
+    static const int kMidiControllerSostenutoPedal;
     static const int kPedalActiveValue;
     
 public:
@@ -56,7 +57,8 @@
 		ModeOff = 0,
 		ModePassThrough,
 		ModeMonophonic,
-		ModePolyphonic
+		ModePolyphonic,
+        ModeMPE
 	};
 	
     // The MIDI Pitch Wheel is not handled by control change like the others,
@@ -132,6 +134,16 @@
         }
     }
     
+    bool usesKeyboardPedals() { return usesKeyboardPedals_; }
+    void setUsesKeyboardPedals(bool use) {
+        usesKeyboardPedals_ = use;
+        // Reset to default if not using
+        if(!use) {
+            // MIDI CCs 64 to 69 are for pedals
+            for(int i = 64; i <= 69; i++)
+                controllerValues_[i] = 0;
+        }
+    }
     
     bool usesKeyboardMIDIControllers() { return usesKeyboardMidiControllers_; }
     void setUsesKeyboardMIDIControllers(bool use) {
@@ -175,6 +187,7 @@
 	void setModePassThrough();
     void setModeMonophonic();
 	void setModePolyphonic();
+    void setModeMPE();
     
     // Get/set polyphony and voice stealing for polyphonic mode
     int polyphony() { return retransmitMaxPolyphony_; }
@@ -188,7 +201,7 @@
     
     // Set the minimum MIDI channel that should be used for output (0-15)
     int outputChannelLowest() { return outputChannelLowest_; }
-    void setOutputChannelLowest(int ch) { outputChannelLowest_ = ch; }
+    void setOutputChannelLowest(int ch);
     
     // Get set the output transposition in semitones, relative to input MIDI notes
     int outputTransposition() { return outputTransposition_; }
@@ -266,7 +279,10 @@
 	void modePolyphonicHandler(MidiInput* source, const MidiMessage& message);
 	void modePolyphonicNoteOn(unsigned char note, unsigned char velocity);
 	void modePolyphonicNoteOff(unsigned char note, bool forceOff = false);
-	void modePolyphonicNoteOnCallback(const char *path, const char *types, int numValues, lo_arg **values);
+	void modePolyphonicMPENoteOnCallback(const char *path, const char *types, int numValues, lo_arg **values);
+    
+    void modeMPEHandler(MidiInput* source, const MidiMessage& message);
+    void modeMPENoteOn(unsigned char note, unsigned char velocity);
 
     // Helper functions for polyphonic mode
     void modePolyphonicSetupHelper();
@@ -303,6 +319,7 @@
     bool usesKeyboardChannelPressure_;              // Whether this segment passes aftertouch from the keyboard
     bool usesKeyboardPitchWheel_;                   // Whether this segment passes pitchwheel from the keyboard
     bool usesKeyboardModWheel_;                     // Whether this segment passes CC 1 (mod wheel) from keyboard
+    bool usesKeyboardPedals_;                       // Whether this segment passes CCs 64-69 (pedals) from the keyboard
     bool usesKeyboardMidiControllers_;              // Whether this segment passes other controllers
     float pitchWheelRange_;                         // Range of MIDI pitch wheel (in semitones)
     
--- a/Source/TouchKeys/TouchkeyDevice.cpp	Tue May 12 18:19:05 2015 +0100
+++ b/Source/TouchKeys/TouchkeyDevice.cpp	Mon Jan 02 22:29:39 2017 +0000
@@ -771,11 +771,12 @@
 
 // Jump to the built-in bootloader of the TouchKeys device
 void TouchkeyDevice::jumpToBootloader() {
+    // The command includes a 4-byte magic number to avoid a corrupt packet accidentally triggering the jump
 	unsigned char command[] = {ESCAPE_CHARACTER, kControlCharacterFrameBegin, kFrameTypeEnterSelfProgramMode,
-		ESCAPE_CHARACTER, kControlCharacterFrameEnd};
+		0xA1, 0xB2, 0xC3, 0xD4, ESCAPE_CHARACTER, kControlCharacterFrameEnd};
 	
 	// Send command
-	if(deviceWrite((char*)command, 5) < 0) {
+	if(deviceWrite((char*)command, 9) < 0) {
         if(verbose_ >= 1)
             cout << "ERROR: unable to write jumpToBootloader command.  errno = " << errno << endl;
 	}
--- a/Source/TouchKeys/TouchkeyDevice.h	Tue May 12 18:19:05 2015 +0100
+++ b/Source/TouchKeys/TouchkeyDevice.h	Mon Jan 02 22:29:39 2017 +0000
@@ -111,6 +111,7 @@
 	kFrameTypeMonitorRawFromKey = 138,
 	kFrameTypeUpdateBaselines = 139,	// Reinitialize baseline values
 	kFrameTypeRescanKeyboard = 140,	// Rescan what keys are connected
+    kFrameTypeEncapsulatedMIDI = 167, // MIDI messages to pass to MIDI standalone firmware
     kFrameTypeRGBLEDSetColors = 168, // Set RGBLEDs of given index to specific values
 	kFrameTypeRGBLEDAllOff = 169,    // All LEDs off
 	kFrameTypeEnterISPMode = 192,