Mercurial > hg > touchkeys
view Source/Mappings/PitchBend/TouchkeyPitchBendMapping.cpp @ 56:b4a2d2ae43cf tip
merge
author | Andrew McPherson <andrewm@eecs.qmul.ac.uk> |
---|---|
date | Fri, 23 Nov 2018 15:48:14 +0000 |
parents | c6f30c1e2bda |
children |
line wrap: on
line source
/* 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/>. ===================================================================== TouchkeyPitchBendMapping.cpp: per-note mapping for the pitch-bend mapping, which handles changing pitch based on relative finger motion. */ #include "TouchkeyPitchBendMapping.h" #include "../../TouchKeys/MidiOutputController.h" #include <vector> #include <climits> #include <cmath> #include "../MappingScheduler.h" #undef DEBUG_PITCHBEND_MAPPING // Class constants const int TouchkeyPitchBendMapping::kDefaultMIDIChannel = 0; const int TouchkeyPitchBendMapping::kDefaultFilterBufferLength = 30; const float TouchkeyPitchBendMapping::kDefaultBendRangeSemitones = 2.0; const float TouchkeyPitchBendMapping::kDefaultBendThresholdSemitones = 0.2; const float TouchkeyPitchBendMapping::kDefaultBendThresholdKeyLength = 0.1; const float TouchkeyPitchBendMapping::kDefaultSnapZoneSemitones = 0.5; const int TouchkeyPitchBendMapping::kDefaultPitchBendMode = TouchkeyPitchBendMapping::kPitchBendModeVariableEndpoints; const float TouchkeyPitchBendMapping::kDefaultFixedModeEnableDistance = 0.1; const float TouchkeyPitchBendMapping::kDefaultFixedModeBufferDistance = 0; const bool TouchkeyPitchBendMapping::kDefaultIgnoresTwoFingers = false; const bool TouchkeyPitchBendMapping::kDefaultIgnoresThreeFingers = false; // 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 TouchkeyPitchBendMapping::TouchkeyPitchBendMapping(PianoKeyboard &keyboard, MappingFactory *factory, int noteNumber, Node<KeyTouchFrame>* touchBuffer, Node<key_position>* positionBuffer, KeyPositionTracker* positionTracker) : TouchkeyBaseMapping(keyboard, factory, noteNumber, touchBuffer, positionBuffer, positionTracker), bendIsEngaged_(false), snapIsEngaged_(false), thresholdSemitones_(kDefaultBendThresholdSemitones), thresholdKeyLength_(kDefaultBendThresholdKeyLength), snapZoneSemitones_(kDefaultSnapZoneSemitones), bendMode_(kDefaultPitchBendMode), fixedModeMinEnableDistance_(kDefaultFixedModeEnableDistance), fixedModeBufferDistance_(kDefaultFixedModeBufferDistance), ignoresTwoFingers_(kDefaultIgnoresTwoFingers), ignoresThreeFingers_(kDefaultIgnoresThreeFingers), onsetLocationX_(missing_value<float>::missing()), onsetLocationY_(missing_value<float>::missing()), lastX_(missing_value<float>::missing()), lastY_(missing_value<float>::missing()), idOfCurrentTouch_(-1), lastTimestamp_(missing_value<timestamp_type>::missing()), lastProcessedIndex_(0), bendScalerPositive_(missing_value<float>::missing()), bendScalerNegative_(missing_value<float>::missing()), currentSnapDestinationSemitones_(missing_value<float>::missing()), bendRangeSemitones_(kDefaultBendRangeSemitones), lastPitchBendSemitones_(0), rawDistance_(kDefaultFilterBufferLength) { resetDetectionState(); updateCombinedThreshold(); } TouchkeyPitchBendMapping::~TouchkeyPitchBendMapping() { } // Reset state back to defaults void TouchkeyPitchBendMapping::reset() { TouchkeyBaseMapping::reset(); sendPitchBendMessage(0.0); resetDetectionState(); } // Resend all current parameters void TouchkeyPitchBendMapping::resend() { sendPitchBendMessage(lastPitchBendSemitones_, true); } // Set the range of vibrato void TouchkeyPitchBendMapping::setRange(float rangeSemitones) { bendRangeSemitones_ = rangeSemitones; } // Set the vibrato detection thresholds void TouchkeyPitchBendMapping::setThresholds(float thresholdSemitones, float thresholdKeyLength) { thresholdSemitones_ = thresholdSemitones; thresholdKeyLength_ = thresholdKeyLength; updateCombinedThreshold(); } // Set the mode to bend a fixed amount up and down the key, regardless of where // the touch starts. minimumDistanceToEnable sets a floor below which the bend isn't // possible (for starting very close to an edge) and bufferAtEnd sets the amount // of key length beyond which no further bend takes place. void TouchkeyPitchBendMapping::setFixedEndpoints(float minimumDistanceToEnable, float bufferAtEnd) { bendMode_ = kPitchBendModeFixedEndpoints; fixedModeMinEnableDistance_ = minimumDistanceToEnable; fixedModeBufferDistance_ = bufferAtEnd; } // Set the mode to bend an amount proportional to distance, which means // that the total range of bend will depend on where the finger started. void TouchkeyPitchBendMapping::setVariableEndpoints() { bendMode_ = kPitchBendModeVariableEndpoints; } void TouchkeyPitchBendMapping::setIgnoresMultipleFingers(bool ignoresTwo, bool ignoresThree) { ignoresTwoFingers_ = ignoresTwo; ignoresThreeFingers_ = ignoresThree; } // 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 TouchkeyPitchBendMapping::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 lastX_ = lastY_ = missing_value<float>::missing(); idOfCurrentTouch_ = -1; #ifdef DEBUG_PITCHBEND_MAPPING std::cout << "Touch off\n"; #endif } else if((frame.count == 2 && ignoresTwoFingers_) || (frame.count == 3 && ignoresThreeFingers_)) { // Multiple touches that we have chosen to ignore. Do nothing for now... } else { // At least one touch. Check if we are already tracking an ID and, if so, // use its coordinates. Otherwise grab the lowest current ID. bool foundCurrentTouch = false; if(idOfCurrentTouch_ >= 0) { for(int i = 0; i < frame.count; i++) { if(frame.ids[i] == idOfCurrentTouch_) { lastY_ = frame.locs[i]; if(frame.locH < 0) lastX_ = missing_value<float>::missing(); else lastX_ = frame.locH; 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; } } if(!bendIsEngaged_) onsetLocationX_ = onsetLocationY_ = missing_value<float>::missing(); idOfCurrentTouch_ = lowestRemainingId; lastY_ = frame.locs[lowestIndex]; if(frame.locH < 0) lastX_ = missing_value<float>::missing(); else lastX_ = frame.locH; #ifdef DEBUG_PITCHBEND_MAPPING std::cout << "Previous touch stopped; now ID " << idOfCurrentTouch_ << " at (" << lastX_ << ", " << lastY_ << ")\n"; #endif } // Now we have an X and (maybe) a Y coordinate for the most recent touch. // Check whether we have an initial location (if the note is active). if(noteIsOn_) { //ScopedLock sl(distanceAccessMutex_); if(missing_value<float>::isMissing(onsetLocationY_) || (!foundCurrentTouch && !bendIsEngaged_)) { // Note is on but touch hasn't yet arrived --> this touch becomes // our onset location. Alternatively, the current touch is a different // ID from the previous one. onsetLocationY_ = lastY_; onsetLocationX_ = lastX_; // Clear buffer and start with 0 distance for this point clearBuffers(); #ifdef DEBUG_PITCHBEND_MAPPING std::cout << "Starting at (" << onsetLocationX_ << ", " << onsetLocationY_ << ")\n"; #endif } else { float distance = 0.0; // Note is on and a start location exists. Calculate distance between // start location and the current point. if(missing_value<float>::isMissing(onsetLocationX_) && !missing_value<float>::isMissing(lastX_)) { // No X location indicated for onset but we have one now. // Update the onset X location. onsetLocationX_ = lastX_; #ifdef DEBUG_PITCHBEND_MAPPING std::cout << "Found first X location at " << onsetLocationX_ << std::endl; #endif } // Distance is based on Y location. TODO: do we need all the X location stuff?? distance = lastY_ - onsetLocationY_; // Insert raw distance into the buffer. The rest of the processing takes place // in the dedicated thread so as not to slow down commmunication with the hardware. rawDistance_.insert(distance, 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 //std::cout << "Raw distance " << distance << " filtered " << filteredDistance_.latest() << std::endl; } } } } } } // 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 TouchkeyPitchBendMapping::performMapping() { //ScopedLock sl(distanceAccessMutex_); timestamp_type currentTimestamp = keyboard_.schedulerCurrentTimestamp(); bool newSamplePresent = false; float lastProcessedDistance = missing_value<float>::missing(); // Go through the filtered distance samples that are remaining to process. if(lastProcessedIndex_ < rawDistance_.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_ = rawDistance_.beginIndex() + 1; } while(lastProcessedIndex_ < rawDistance_.endIndex()) { float distance = lastProcessedDistance = rawDistance_[lastProcessedIndex_]; //timestamp_type timestamp = rawDistance_.timestampAt(lastProcessedIndex_); newSamplePresent = true; if(bendIsEngaged_) { /* // TODO: look for snapping // Raw distance is the distance from note onset. Adjusted distance takes into account // that the bend actually started on the cross of a threshold. float adjustedDistance = rawDistance_.latest() - bendEngageLocation_; float pitchBendSemitones = 0.0; // Calculate pitch bend based on most recent distance if(adjustedDistance > 0.0) pitchBendSemitones = adjustedDistance * bendScalerPositive_; else pitchBendSemitones = adjustedDistance * bendScalerNegative_; // Find the nearest semitone to the current value by rounding currentSnapDestinationSemitones_ = roundf(pitchBendSemitones); if(snapIsEngaged_) { // TODO: check velocity conditions; if above minimum velocity, disengage } else { if(fabsf(pitchBendSemitones - currentSnapDestinationSemitones_) < snapZoneSemitones_) { // TODO: check velocity conditions; if below minimum velocity, engage //engageSnapping(); } } */ } else { // Check if bend should engage, using two thresholds: one as fraction of // key length, one as distance in semitones if(fabsf(distance) > thresholdCombinedMax_) { bendIsEngaged_ = true; #ifdef DEBUG_PITCHBEND_MAPPING std::cout << "engaging bend at distance " << distance << std::endl; #endif // Set up dynamic scaling based on fixed distances to edge of key. // TODO: make this more flexible, to always nail the nearest semitone (optionally) // This is how far we would have had from the onset point to the edge of key. float distanceToPositiveEdgeWithoutThreshold = 1.0 - onsetLocationY_; float distanceToNegativeEdgeWithoutThreshold = onsetLocationY_; // This is how far we actually have to go to the edge of the key float actualDistanceToPositiveEdge = 1.0 - (onsetLocationY_ + thresholdCombinedMax_); float actualDistanceToNegativeEdge = onsetLocationY_ - thresholdCombinedMax_; // Make it so moving toward edge of key gets as far as it would have without // the distance lost by the threshold if(bendMode_ == kPitchBendModeVariableEndpoints) { if(actualDistanceToPositiveEdge > 0.0) bendScalerPositive_ = bendRangeSemitones_ * distanceToPositiveEdgeWithoutThreshold / actualDistanceToPositiveEdge; else bendScalerPositive_ = bendRangeSemitones_; // Sanity check if(actualDistanceToNegativeEdge > 0.0) bendScalerNegative_ = bendRangeSemitones_ * distanceToNegativeEdgeWithoutThreshold / actualDistanceToNegativeEdge; else bendScalerNegative_ = bendRangeSemitones_; // Sanity check } else if(bendMode_ == kPitchBendModeFixedEndpoints) { // TODO: buffer distance at end if(actualDistanceToPositiveEdge > fixedModeMinEnableDistance_) bendScalerPositive_ = bendRangeSemitones_ / actualDistanceToPositiveEdge; else bendScalerPositive_ = 0.0; if(actualDistanceToNegativeEdge > fixedModeMinEnableDistance_) bendScalerNegative_ = bendRangeSemitones_ / actualDistanceToNegativeEdge; else bendScalerNegative_ = 0.0; } else // unknown mode bendScalerPositive_ = bendScalerNegative_ = 0.0; } } lastProcessedIndex_++; } if(bendIsEngaged_ && !missing_value<float>::isMissing(lastProcessedDistance)) { // Having processed every sample individually for detection, send a pitch bend message based on the most // recent one (no sense in sending multiple pitch bend messages simultaneously). if(newSamplePresent) { // Raw distance is the distance from note onset. Adjusted distance takes into account // that the bend actually started on the cross of a threshold. float pitchBendSemitones; if(lastProcessedDistance > thresholdCombinedMax_) pitchBendSemitones = (lastProcessedDistance - thresholdCombinedMax_) * bendScalerPositive_; else if(lastProcessedDistance < -thresholdCombinedMax_) pitchBendSemitones = (lastProcessedDistance + thresholdCombinedMax_) * bendScalerNegative_; else pitchBendSemitones = 0.0; sendPitchBendMessage(pitchBendSemitones); lastPitchBendSemitones_ = pitchBendSemitones; } else if(snapIsEngaged_) { // We may have arrived here without a new touch, just based on timing. Even so, if pitch snapping // is engaged we need to continue to update the pitch // TODO: calculate the next filtered pitch based on snapping } } // Register for the next update by returning its timestamp nextScheduledTimestamp_ = currentTimestamp + updateInterval_; return nextScheduledTimestamp_; } // MIDI note-on message received void TouchkeyPitchBendMapping::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. onsetLocationX_ = lastX_; onsetLocationY_ = lastY_; bendIsEngaged_ = false; if(!missing_value<float>::isMissing(onsetLocationY_)) { // Already have touch data. Clear the buffer here. // Clear buffer and start with 0 distance for this point clearBuffers(); #ifdef DEBUG_PITCHBEND_MAPPING std::cout << "MIDI on: starting at (" << onsetLocationX_ << ", " << onsetLocationY_ << ")\n"; #endif } else { #ifdef DEBUG_PITCHBEND_MAPPING std::cout << "MIDI on but no touch\n"; #endif } } // MIDI note-off message received void TouchkeyPitchBendMapping::midiNoteOffReceived(int channel) { if(bendIsEngaged_) { // TODO: should anything happen here? No new samples processed anyway, // but we may want the snapping algorithm to still continue its work. } } // Reset variables involved in detecting a pitch bend gesture void TouchkeyPitchBendMapping::resetDetectionState() { bendIsEngaged_ = false; snapIsEngaged_ = false; } // Clear the buffers that hold distance measurements void TouchkeyPitchBendMapping::clearBuffers() { rawDistance_.clear(); rawDistance_.insert(0.0, lastTimestamp_); lastProcessedIndex_ = 0; } // Engage the snapping algorithm to pull the pitch into the nearest semitone void TouchkeyPitchBendMapping::engageSnapping() { snapIsEngaged_ = true; } // Disengage the snapping algorithm void TouchkeyPitchBendMapping::disengageSnapping() { snapIsEngaged_ = false; } // Set the combined threshold based on the two independent parameters // relating to semitones and key length void TouchkeyPitchBendMapping::updateCombinedThreshold() { if(thresholdKeyLength_ > thresholdSemitones_ / bendRangeSemitones_) thresholdCombinedMax_ = thresholdKeyLength_; else thresholdCombinedMax_ = thresholdSemitones_ / bendRangeSemitones_; } // Send the pitch bend message of a given number of a semitones. Send by OSC, // which can be mapped to MIDI CC externally void TouchkeyPitchBendMapping::sendPitchBendMessage(float pitchBendSemitones, bool force) { if(force || !suspended_) keyboard_.sendMessage(controlName_.c_str(), "if", noteNumber_, pitchBendSemitones, LO_ARGS_END); }