Mercurial > hg > touchkeys
diff Source/Mappings/Control/TouchkeyControlMapping.cpp @ 0:3580ffe87dc8
First commit of TouchKeys public pre-release.
author | Andrew McPherson <andrewm@eecs.qmul.ac.uk> |
---|---|
date | Mon, 11 Nov 2013 18:19:35 +0000 |
parents | |
children | c6f30c1e2bda |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Source/Mappings/Control/TouchkeyControlMapping.cpp Mon Nov 11 18:19:35 2013 +0000 @@ -0,0 +1,634 @@ +/* + TouchKeys: multi-touch musical keyboard control software + Copyright (c) 2013 Andrew McPherson + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + + ===================================================================== + + TouchkeyControlMapping.cpp: per-note mapping for the TouchKeys control + mapping, which converts an arbitrary touch parameter into a MIDI or + OSC control message. +*/ + +#include "TouchkeyControlMapping.h" +#include <vector> +#include <climits> +#include <cmath> +#include "../MappingScheduler.h" + +#undef DEBUG_CONTROL_MAPPING + +// Class constants +const int TouchkeyControlMapping::kDefaultMIDIChannel = 0; +const int TouchkeyControlMapping::kDefaultFilterBufferLength = 300; + +const bool TouchkeyControlMapping::kDefaultIgnoresTwoFingers = false; +const bool TouchkeyControlMapping::kDefaultIgnoresThreeFingers = false; +const int TouchkeyControlMapping::kDefaultDirection = TouchkeyControlMapping::kDirectionPositive; + +// Main constructor takes references/pointers from objects which keep track +// of touch location, continuous key position and the state detected from that +// position. The PianoKeyboard object is strictly required as it gives access to +// Scheduler and OSC methods. The others are optional since any given system may +// contain only one of continuous key position or touch sensitivity +TouchkeyControlMapping::TouchkeyControlMapping(PianoKeyboard &keyboard, MappingFactory *factory, int noteNumber, Node<KeyTouchFrame>* touchBuffer, + Node<key_position>* positionBuffer, KeyPositionTracker* positionTracker) +: TouchkeyBaseMapping(keyboard, factory, noteNumber, touchBuffer, positionBuffer, positionTracker), +controlIsEngaged_(false), +inputMin_(0.0), inputMax_(1.0), outputMin_(0.0), outputMax_(1.0), outputDefault_(0.0), +inputParameter_(kInputParameterYPosition), inputType_(kTypeAbsolute), +threshold_(0.0), ignoresTwoFingers_(kDefaultIgnoresTwoFingers), +ignoresThreeFingers_(kDefaultIgnoresThreeFingers), direction_(kDefaultDirection), +touchOnsetValue_(missing_value<float>::missing()), +midiOnsetValue_(missing_value<float>::missing()), +lastValue_(missing_value<float>::missing()), +lastTimestamp_(missing_value<timestamp_type>::missing()), lastProcessedIndex_(0), +controlEngageLocation_(missing_value<float>::missing()), +controlScalerPositive_(missing_value<float>::missing()), +controlScalerNegative_(missing_value<float>::missing()), +lastControlValue_(outputDefault_), +rawValues_(kDefaultFilterBufferLength) +{ + resetDetectionState(); +} + +TouchkeyControlMapping::~TouchkeyControlMapping() { +#if 0 +#ifndef NEW_MAPPING_SCHEDULER + try { + disengage(); + } + catch(...) { + std::cerr << "~TouchkeyControlMapping(): exception during disengage()\n"; + } +#endif +#endif +} + +// Turn on mapping of data. +/*void TouchkeyControlMapping::engage() { + Mapping::engage(); + + // Register for OSC callbacks on MIDI note on/off + addOscListener("/midi/noteon"); + addOscListener("/midi/noteoff"); +} + +// Turn off mapping of data. Remove our callback from the scheduler +void TouchkeyControlMapping::disengage(bool shouldDelete) { + // Remove OSC listeners first + removeOscListener("/midi/noteon"); + removeOscListener("/midi/noteoff"); + + // Don't send any separate message here, leave it where it was + + Mapping::disengage(shouldDelete); + + if(noteIsOn_) { + // TODO + } + noteIsOn_ = false; +}*/ + +// Reset state back to defaults +void TouchkeyControlMapping::reset() { + TouchkeyBaseMapping::reset(); + sendControlMessage(outputDefault_); + resetDetectionState(); + //noteIsOn_ = false; +} + +// Resend all current parameters +void TouchkeyControlMapping::resend() { + sendControlMessage(lastControlValue_, true); +} + +// Name for this control, used in the OSC path +/*void TouchkeyControlMapping::setName(const std::string& name) { + controlName_ = name; +}*/ + +// Parameters for the controller handling +// Input parameter to use for this control mapping and whether it is absolute or relative +void TouchkeyControlMapping::setInputParameter(int parameter, int type) { + if(inputParameter_ >= 0 && inputParameter_ < kInputParameterMaxValue) + inputParameter_ = parameter; + if(type >= 0 && type < kTypeMaxValue) + inputType_ = type; +} + +// Input/output range for this parameter +void TouchkeyControlMapping::setRange(float inputMin, float inputMax, float outputMin, float outputMax, float outputDefault) { + inputMin_ = inputMin; + inputMax_ = inputMax; + outputMin_ = outputMin; + outputMax_ = outputMax; + outputDefault_ = outputDefault; +} + +// Threshold which must be exceeded for the control to engage (for relative position), or 0 if not used +void TouchkeyControlMapping::setThreshold(float threshold) { + threshold_ = threshold; +} + +void TouchkeyControlMapping::setIgnoresMultipleFingers(bool ignoresTwo, bool ignoresThree) { + ignoresTwoFingers_ = ignoresTwo; + ignoresThreeFingers_ = ignoresThree; +} + +void TouchkeyControlMapping::setDirection(int direction) { + if(direction >= 0 && direction < kDirectionMaxValue) + direction_ = direction; +} + +// OSC handler method. Called from PianoKeyboard when MIDI data comes in. +/*bool TouchkeyControlMapping::oscHandlerMethod(const char *path, const char *types, int numValues, lo_arg **values, void *data) { + if(!strcmp(path, "/midi/noteon") && !noteIsOn_ && numValues >= 1) { + if(types[0] == 'i' && values[0]->i == noteNumber_) { + // MIDI note has gone on. Set the starting location to be most recent + // location. It's possible there has been no touch data before this, + // in which case lastX and lastY will hold missing values. + midiOnsetValue_ = lastValue_; + if(!missing_value<float>::isMissing(midiOnsetValue_)) { + if(inputType_ == kTypeNoteOnsetRelative) { + // Already have touch data. Clear the buffer here. + // Clear buffer and start with default value for this point + clearBuffers(); + +#ifdef DEBUG_CONTROL_MAPPING + std::cout << "MIDI on: starting at (" << midiOnsetValue_ << ")\n"; +#endif + } + } + else { +#ifdef DEBUG_CONTROL_MAPPING + std::cout << "MIDI on but no touch\n"; +#endif + } + + noteIsOn_ = true; + return false; + } + } + else if(!strcmp(path, "/midi/noteoff") && noteIsOn_ && numValues >= 1) { + if(types[0] == 'i' && values[0]->i == noteNumber_) { + // MIDI note goes off + noteIsOn_ = false; + if(controlIsEngaged_) { + // TODO: should anything happen here? + } +#ifdef DEBUG_CONTROL_MAPPING + std::cout << "MIDI off\n"; +#endif + return false; + } + } + + return false; +}*/ + +// Trigger method. This receives updates from the TouchKey data or from state changes in +// the continuous key position (KeyPositionTracker). It will potentially change the scheduled +// behavior of future mapping calls, but the actual OSC messages should be transmitted in a different +// thread. +void TouchkeyControlMapping::triggerReceived(TriggerSource* who, timestamp_type timestamp) { + if(who == 0) + return; + + if(who == touchBuffer_) { + if(!touchBuffer_->empty()) { + // New touch data is available. Find the distance from the onset location. + KeyTouchFrame frame = touchBuffer_->latest(); + lastTimestamp_ = timestamp; + + if(frame.count == 0) { + // No touches. Last values are "missing", and we're not tracking any + // particular touch ID + lastValue_ = missing_value<float>::missing(); + idsOfCurrentTouches_[0] = idsOfCurrentTouches_[1] = idsOfCurrentTouches_[2] = -1; + +#ifdef DEBUG_CONTROL_MAPPING + std::cout << "Touch off\n"; +#endif + } + else { + //ScopedLock sl(rawValueAccessMutex_); + + // At least one touch. Check if we are already tracking an ID and, if so, + // use its coordinates. Otherwise grab the lowest current ID. + lastValue_ = getValue(frame); + + // Check that the value actually exists + if(!missing_value<float>::isMissing(lastValue_)) { + // If we have no onset value, this is it + if(missing_value<float>::isMissing(touchOnsetValue_)) { + touchOnsetValue_ = lastValue_; + if(inputType_ == kTypeFirstTouchRelative) { + clearBuffers(); +#ifdef DEBUG_CONTROL_MAPPING + std::cout << "Starting at " << lastValue_ << std::endl; +#endif + } + } + + // If MIDI note is on and we don't previously have a value, this is it + if(noteIsOn_ && missing_value<float>::isMissing(midiOnsetValue_)) { + midiOnsetValue_ = lastValue_; + if(inputType_ == kTypeNoteOnsetRelative) { + clearBuffers(); +#ifdef DEBUG_CONTROL_MAPPING + std::cout << "Starting at " << lastValue_ << std::endl; +#endif + } + } + + if(noteIsOn_) { + // Insert the latest sample into the buffer depending on how the data should be processed + if(inputType_ == kTypeAbsolute) { + rawValues_.insert(lastValue_, timestamp); + } + else if(inputType_ == kTypeFirstTouchRelative) { + rawValues_.insert(lastValue_ - touchOnsetValue_, timestamp); + } + else if(inputType_ == kTypeNoteOnsetRelative) { + rawValues_.insert(lastValue_ - midiOnsetValue_, timestamp); + } + + // Move the current scheduled event up to the present time. + // FIXME: this may be more inefficient than just doing everything in the current thread! +#ifdef NEW_MAPPING_SCHEDULER + keyboard_.mappingScheduler().scheduleNow(this); +#else + keyboard_.unscheduleEvent(this); + keyboard_.scheduleEvent(this, mappingAction_, keyboard_.schedulerCurrentTimestamp()); +#endif + } + } + } + } + } +} + +// Mapping method. This actually does the real work of sending OSC data in response to the +// latest information from the touch sensors or continuous key angle +timestamp_type TouchkeyControlMapping::performMapping() { + //ScopedLock sl(rawValueAccessMutex_); + + timestamp_type currentTimestamp = keyboard_.schedulerCurrentTimestamp(); + bool newSamplePresent = false; + + // Go through the filtered distance samples that are remaining to process. + if(lastProcessedIndex_ < rawValues_.beginIndex() + 1) { + // Fell off the beginning of the position buffer. Skip to the samples we have + // (shouldn't happen except in cases of exceptional system load, and not too + // consequential if it does happen). + lastProcessedIndex_ = rawValues_.beginIndex() + 1; + } + + while(lastProcessedIndex_ < rawValues_.endIndex()) { + float value = rawValues_[lastProcessedIndex_]; + //timestamp_type timestamp = rawValues_.timestampAt(lastProcessedIndex_); + newSamplePresent = true; + + if(inputType_ == kTypeAbsolute) { + controlIsEngaged_ = true; + } + else if(!controlIsEngaged_) { + // Compare value against threshold to see if the control should engage + if(fabsf(value) > threshold_) { + float startingValue; + + controlIsEngaged_ = true; + controlEngageLocation_ = (value > 0 ? threshold_ : -threshold_); + +#ifdef DEBUG_CONTROL_MAPPING + std::cout << "engaging control at distance " << controlEngageLocation_ << std::endl; +#endif + + if(inputType_ == kTypeFirstTouchRelative) + startingValue = touchOnsetValue_; + else + startingValue = midiOnsetValue_; + + // This is how much range we would have had without the threshold + float distanceToPositiveEdgeWithoutThreshold = 1.0 - startingValue; + float distanceToNegativeEdgeWithoutThreshold = 0.0 + startingValue; + + // This is how much range we actually have with the threshold + float actualDistanceToPositiveEdge = 1.0 - (startingValue + controlEngageLocation_); + float actualDistanceToNegativeEdge = 0.0 + startingValue + controlEngageLocation_; + + // Make it so moving toward edge of key gets as far as it would have without + // the distance lost by the threshold + if(actualDistanceToPositiveEdge > 0.0) + controlScalerPositive_ = (outputMax_ - outputDefault_) * distanceToPositiveEdgeWithoutThreshold / actualDistanceToPositiveEdge; + else + controlScalerPositive_ = (outputMax_ - outputDefault_); // Sanity check + if(actualDistanceToNegativeEdge > 0.0) + controlScalerNegative_ = (outputDefault_ - outputMin_) * distanceToNegativeEdgeWithoutThreshold / actualDistanceToNegativeEdge; + else + controlScalerNegative_ = (outputDefault_ - outputMin_); // Sanity check + } + } + + lastProcessedIndex_++; + } + + if(controlIsEngaged_) { + // Having processed every sample individually for any detection/filtering, now send + // the most recent output as an OSC message + if(newSamplePresent) { + float latestValue = rawValues_.latest(); + + // In cases of relative values, the place the control engages will actually be where it crosses + // the threshold, not the onset location itself. Need to update the value accordingly. + if(inputType_ == kTypeFirstTouchRelative || + inputType_ == kTypeNoteOnsetRelative) { + if(latestValue > 0) { + latestValue -= threshold_; + if(latestValue < 0) + latestValue = 0; + } + else if(latestValue < 0) { + latestValue += threshold_; + if(latestValue > 0) + latestValue = 0; + } + } + + if(direction_ == kDirectionNegative) + latestValue = -latestValue; + else if((direction_ == kDirectionBoth) && latestValue < 0) + latestValue = -latestValue; + + sendControlMessage(latestValue); + lastControlValue_ = latestValue; + } + } + + // Register for the next update by returning its timestamp + nextScheduledTimestamp_ = currentTimestamp + updateInterval_; + return nextScheduledTimestamp_; +} + +// MIDI note-on message received +void TouchkeyControlMapping::midiNoteOnReceived(int channel, int velocity) { + // MIDI note has gone on. Set the starting location to be most recent + // location. It's possible there has been no touch data before this, + // in which case lastX and lastY will hold missing values. + midiOnsetValue_ = lastValue_; + if(!missing_value<float>::isMissing(midiOnsetValue_)) { + if(inputType_ == kTypeNoteOnsetRelative) { + // Already have touch data. Clear the buffer here. + // Clear buffer and start with default value for this point + clearBuffers(); + +#ifdef DEBUG_CONTROL_MAPPING + std::cout << "MIDI on: starting at (" << midiOnsetValue_ << ")\n"; +#endif + } + } + else { +#ifdef DEBUG_CONTROL_MAPPING + std::cout << "MIDI on but no touch\n"; +#endif + } +} + +// MIDI note-off message received +void TouchkeyControlMapping::midiNoteOffReceived(int channel) { + if(controlIsEngaged_) { + // TODO: should anything happen here? + } +} + +// Reset variables involved in detecting a pitch bend gesture +void TouchkeyControlMapping::resetDetectionState() { + controlIsEngaged_ = false; + controlEngageLocation_ = missing_value<float>::missing(); + idsOfCurrentTouches_[0] = idsOfCurrentTouches_[1] = idsOfCurrentTouches_[2] = -1; +} + +// Clear the buffers that hold distance measurements +void TouchkeyControlMapping::clearBuffers() { + rawValues_.clear(); + rawValues_.insert(0.0, lastTimestamp_); + lastProcessedIndex_ = 0; +} + +// Return the current parameter value depending on which one we are listening to +float TouchkeyControlMapping::getValue(const KeyTouchFrame& frame) { + if(inputParameter_ == kInputParameterXPosition) + return frame.locH; + /*else if(inputParameter_ == kInputParameter2FingerMean || + inputParameter_ == kInputParameter2FingerDistance) { + if(frame.count < 2) + return missing_value<float>::missing(); + if(frame.count == 3 && ignoresThreeFingers_) + return missing_value<float>::missing(); + + bool foundCurrentTouch = false; + float currentValue; + + // Look for the touches we were tracking last frame + if(idsOfCurrentTouches_[0] >= 0) { + for(int i = 0; i < frame.count; i++) { + if(frame.ids[i] == idsOfCurrentTouches_[0]) { + if(inputParameter_ == kInputParameterYPosition) + currentValue = frame.locs[i]; + else // kInputParameterTouchSize + currentValue = frame.sizes[i]; + foundCurrentTouch = true; + break; + } + } + } + + if(!foundCurrentTouch) { + // Assign a new touch to be tracked + int lowestRemainingId = INT_MAX; + int lowestIndex = 0; + + for(int i = 0; i < frame.count; i++) { + if(frame.ids[i] < lowestRemainingId) { + lowestRemainingId = frame.ids[i]; + lowestIndex = i; + } + } + + idsOfCurrentTouches_[0] = lowestRemainingId; + if(inputParameter_ == kInputParameterYPosition) + currentValue = frame.locs[lowestIndex]; + else if(inputParameter_ == kInputParameterTouchSize) + currentValue = frame.sizes[lowestIndex]; + else // Shouldn't happen + currentValue = missing_value<float>::missing(); + +#ifdef DEBUG_CONTROL_MAPPING + std::cout << "Previous touch stopped; now ID " << idsOfCurrentTouches_[0] << " at (" << currentValue << ")\n"; +#endif + } + + }*/ + else { + if(frame.count == 0) + return missing_value<float>::missing(); + if((inputParameter_ == kInputParameter2FingerMean || + inputParameter_ == kInputParameter2FingerDistance) && + frame.count < 2) + return missing_value<float>::missing(); + if(frame.count == 2 && ignoresTwoFingers_) + return missing_value<float>::missing(); + if(frame.count == 3 && ignoresThreeFingers_) + return missing_value<float>::missing(); + /* + // The other values are dependent on individual touches + bool foundCurrentTouch = false; + float currentValue; + + // Look for the touch we were tracking last frame + if(idsOfCurrentTouches_[0] >= 0) { + for(int i = 0; i < frame.count; i++) { + if(frame.ids[i] == idsOfCurrentTouches_[0]) { + if(inputParameter_ == kInputParameterYPosition) + currentValue = frame.locs[i]; + else // kInputParameterTouchSize + currentValue = frame.sizes[i]; + foundCurrentTouch = true; + break; + } + } + } + + if(!foundCurrentTouch) { + // Assign a new touch to be tracked + int lowestRemainingId = INT_MAX; + int lowestIndex = 0; + + for(int i = 0; i < frame.count; i++) { + if(frame.ids[i] < lowestRemainingId) { + lowestRemainingId = frame.ids[i]; + lowestIndex = i; + } + } + + idsOfCurrentTouches_[0] = lowestRemainingId; + if(inputParameter_ == kInputParameterYPosition) + currentValue = frame.locs[lowestIndex]; + else if(inputParameter_ == kInputParameterTouchSize) + currentValue = frame.sizes[lowestIndex]; + else // Shouldn't happen + currentValue = missing_value<float>::missing(); + +#ifdef DEBUG_CONTROL_MAPPING + std::cout << "Previous touch stopped; now ID " << idsOfCurrentTouches_[0] << " at (" << currentValue << ")\n"; +#endif + }*/ + + float currentValue = 0; + + int idWithinFrame0 = locateTouchId(frame, 0); + if(idWithinFrame0 < 0) { + // Touch ID not found, start a new value + idsOfCurrentTouches_[0] = lowestUnassignedTouch(frame, &idWithinFrame0); +#ifdef DEBUG_CONTROL_MAPPING + std::cout << "Previous touch stopped (0); now ID " << idsOfCurrentTouches_[0] << endl; +#endif + if(idsOfCurrentTouches_[0] < 0) { + cout << "BUG: didn't find any unassigned touch!\n"; + } + } + + if(inputParameter_ == kInputParameterYPosition) + currentValue = frame.locs[idWithinFrame0]; + else if(inputParameter_ == kInputParameterTouchSize) // kInputParameterTouchSize + currentValue = frame.sizes[idWithinFrame0]; + else if(inputParameter_ == kInputParameter2FingerMean || + inputParameter_ == kInputParameter2FingerDistance) { + int idWithinFrame1 = locateTouchId(frame, 1); + if(idWithinFrame1 < 0) { + // Touch ID not found, start a new value + idsOfCurrentTouches_[1] = lowestUnassignedTouch(frame, &idWithinFrame1); +#ifdef DEBUG_CONTROL_MAPPING + std::cout << "Previous touch stopped (1); now ID " << idsOfCurrentTouches_[1] << endl; +#endif + if(idsOfCurrentTouches_[1] < 0) { + cout << "BUG: didn't find any unassigned touch for second finger!\n"; + } + } + + if(inputParameter_ == kInputParameter2FingerMean) + currentValue = (frame.locs[idWithinFrame0] + frame.locs[idWithinFrame1]) * 0.5; + else + currentValue = fabsf(frame.locs[idWithinFrame1] - frame.locs[idWithinFrame0]); + } + + return currentValue; + } +} + +// Look for a touch index in the frame matching the given value of idsOfCurrentTouches[index] +// Returns -1 if not found +int TouchkeyControlMapping::locateTouchId(KeyTouchFrame const& frame, int index) { + if(idsOfCurrentTouches_[index] < 0) + return -1; + + for(int i = 0; i < frame.count; i++) { + if(frame.ids[i] == idsOfCurrentTouches_[index]) { + return i; + } + } + + return -1; +} + +// Locates the lowest touch ID that is not assigned to a current touch +// Returns -1 if no unassigned touches were found +int TouchkeyControlMapping::lowestUnassignedTouch(KeyTouchFrame const& frame, int *indexWithinFrame) { + int lowestRemainingId = INT_MAX; + int lowestIndex = -1; + + for(int i = 0; i < frame.count; i++) { + if(frame.ids[i] < lowestRemainingId) { + bool alreadyAssigned = false; + for(int j = 0; j < 3; j++) { + if(idsOfCurrentTouches_[j] == frame.ids[i]) + alreadyAssigned = true; + } + + if(!alreadyAssigned) { + lowestRemainingId = frame.ids[i]; + lowestIndex = i; + + } + } + } + + if(indexWithinFrame != 0) + *indexWithinFrame = lowestIndex; + return lowestRemainingId; +} + +// Send the pitch bend message of a given number of a semitones. Send by OSC, +// which can be mapped to MIDI CC externally +void TouchkeyControlMapping::sendControlMessage(float value, bool force) { + if(force || !suspended_) { +#ifdef DEBUG_CONTROL_MAPPING + std::cout << "TouchkeyControlMapping: sending " << value << " for note " << noteNumber_ << std::endl; +#endif + keyboard_.sendMessage(controlName_.c_str(), "if", noteNumber_, value, LO_ARGS_END); + } +} +