Mercurial > hg > touchkeys
view Source/Mappings/PitchBend/TouchkeyPitchBendMapping.cpp @ 20:dfff66c07936
Lots of minor changes to support building on Visual Studio. A few MSVC-specific #ifdefs to eliminate things Visual Studio doesn't like. This version now compiles on Windows (provided liblo, Juce and pthread are present) but the TouchKeys device support is not yet enabled. Also, the code now needs to be re-checked on Mac and Linux.
author | Andrew McPherson <andrewm@eecs.qmul.ac.uk> |
---|---|
date | Sun, 09 Feb 2014 18:40:51 +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); }