andrewm@0: /* andrewm@0: TouchKeys: multi-touch musical keyboard control software andrewm@0: Copyright (c) 2013 Andrew McPherson andrewm@0: andrewm@0: This program is free software: you can redistribute it and/or modify andrewm@0: it under the terms of the GNU General Public License as published by andrewm@0: the Free Software Foundation, either version 3 of the License, or andrewm@0: (at your option) any later version. andrewm@0: andrewm@0: This program is distributed in the hope that it will be useful, andrewm@0: but WITHOUT ANY WARRANTY; without even the implied warranty of andrewm@0: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the andrewm@0: GNU General Public License for more details. andrewm@0: andrewm@0: You should have received a copy of the GNU General Public License andrewm@0: along with this program. If not, see . andrewm@0: andrewm@0: ===================================================================== andrewm@0: andrewm@0: TouchkeyOnsetAngleMapping.cpp: per-note mapping for the onset angle mapping, andrewm@0: which measures the speed of finger motion along the key surface at the andrewm@0: time of MIDI note onset. andrewm@0: */ andrewm@0: andrewm@0: #include "TouchkeyOnsetAngleMapping.h" andrewm@0: #include "../MappingFactory.h" andrewm@0: andrewm@0: #define DEBUG_NOTE_ONSET_MAPPING andrewm@0: andrewm@0: // Class constants andrewm@0: const int TouchkeyOnsetAngleMapping::kDefaultFilterBufferLength = 30; andrewm@0: const timestamp_diff_type TouchkeyOnsetAngleMapping::kDefaultMaxLookbackTime = milliseconds_to_timestamp(100); andrewm@0: const int TouchkeyOnsetAngleMapping::kDefaultMaxLookbackSamples = 3; andrewm@0: andrewm@0: // Main constructor takes references/pointers from objects which keep track andrewm@0: // of touch location, continuous key position and the state detected from that andrewm@0: // position. The PianoKeyboard object is strictly required as it gives access to andrewm@0: // Scheduler and OSC methods. The others are optional since any given system may andrewm@0: // contain only one of continuous key position or touch sensitivity andrewm@0: TouchkeyOnsetAngleMapping::TouchkeyOnsetAngleMapping(PianoKeyboard &keyboard, MappingFactory *factory, int noteNumber, Node* touchBuffer, andrewm@0: Node* positionBuffer, KeyPositionTracker* positionTracker) andrewm@0: : TouchkeyBaseMapping(keyboard, factory, noteNumber, touchBuffer, positionBuffer, positionTracker), andrewm@0: pastSamples_(kDefaultFilterBufferLength), maxLookbackTime_(kDefaultMaxLookbackTime), andrewm@0: startingPitchBendSemitones_(0), lastPitchBendSemitones_(0), andrewm@0: rampBeginTime_(missing_value::missing()), rampLength_(0) andrewm@0: { andrewm@0: } andrewm@0: andrewm@0: // Reset state back to defaults andrewm@0: void TouchkeyOnsetAngleMapping::reset() { andrewm@0: ScopedLock sl(sampleBufferMutex_); andrewm@0: andrewm@0: TouchkeyBaseMapping::reset(); andrewm@0: pastSamples_.clear(); andrewm@0: } andrewm@0: andrewm@0: // Resend all current parameters andrewm@0: void TouchkeyOnsetAngleMapping::resend() { andrewm@0: // Message is only sent at release; resend may not apply here. andrewm@0: } andrewm@0: andrewm@0: // This method receives data from the touch buffer or possibly the continuous key angle (not used here) andrewm@0: void TouchkeyOnsetAngleMapping::triggerReceived(TriggerSource* who, timestamp_type timestamp) { andrewm@0: if(who == touchBuffer_) { andrewm@0: ScopedLock sl(sampleBufferMutex_); andrewm@0: andrewm@0: // Save the latest frame, even if it is an empty touch (we need to know what happened even andrewm@0: // after the touch ends since the MIDI off may come later) andrewm@0: if(!touchBuffer_->empty()) andrewm@0: pastSamples_.insert(touchBuffer_->latest(), touchBuffer_->latestTimestamp()); andrewm@0: } andrewm@0: } andrewm@0: andrewm@0: // Mapping method. This actually does the real work of sending OSC data in response to the andrewm@0: // latest information from the touch sensors or continuous key angle andrewm@0: timestamp_type TouchkeyOnsetAngleMapping::performMapping() { andrewm@0: timestamp_type currentTimestamp = keyboard_.schedulerCurrentTimestamp(); andrewm@0: andrewm@0: if(rampLength_ != 0 && currentTimestamp <= rampBeginTime_ + rampLength_) { andrewm@0: float rampValue = 1.0 - (float)(currentTimestamp - rampBeginTime_)/(float)rampLength_; andrewm@0: andrewm@0: lastPitchBendSemitones_ = startingPitchBendSemitones_ * rampValue; andrewm@0: #ifdef DEBUG_NOTE_ONSET_MAPPING andrewm@0: std::cout << "onset pitch = " << lastPitchBendSemitones_ << endl; andrewm@0: #endif andrewm@0: sendPitchBendMessage(lastPitchBendSemitones_); andrewm@0: } andrewm@0: else if(lastPitchBendSemitones_ != 0) { andrewm@0: lastPitchBendSemitones_ = 0; andrewm@0: #ifdef DEBUG_NOTE_ONSET_MAPPING andrewm@0: std::cout << "onset pitch = " << lastPitchBendSemitones_ << endl; andrewm@0: #endif andrewm@0: sendPitchBendMessage(lastPitchBendSemitones_); andrewm@0: } andrewm@0: andrewm@0: // Register for the next update by returning its timestamp andrewm@0: nextScheduledTimestamp_ = currentTimestamp + updateInterval_; andrewm@0: return nextScheduledTimestamp_; andrewm@0: } andrewm@0: andrewm@0: void TouchkeyOnsetAngleMapping::processOnset(timestamp_type timestamp) { andrewm@0: andrewm@0: sampleBufferMutex_.enter(); andrewm@0: andrewm@0: // Look backwards from the current timestamp to find the velocity andrewm@0: float calculatedVelocity = missing_value::missing(); andrewm@0: bool touchWasOn = false; andrewm@0: int sampleCount = 0; andrewm@0: andrewm@0: #ifdef DEBUG_NOTE_ONSET_MAPPING andrewm@0: std::cout << "processOnset begin = " << pastSamples_.beginIndex() << " end = " << pastSamples_.endIndex() << "\n"; andrewm@0: #endif andrewm@0: andrewm@0: if(!pastSamples_.empty()) { andrewm@0: Node::size_type index = pastSamples_.endIndex() - 1; andrewm@0: Node::size_type mostRecentTouchPresentIndex = pastSamples_.endIndex() - 1; andrewm@0: while(index >= pastSamples_.beginIndex()) { andrewm@0: #ifdef DEBUG_NOTE_ONSET_MAPPING andrewm@0: std::cout << "examining sample " << index << " with " << pastSamples_[index].count << " touches and time diff " << timestamp - pastSamples_.timestampAt(index) << "\n"; andrewm@0: #endif andrewm@0: if(timestamp - pastSamples_.timestampAt(index) >= maxLookbackTime_) andrewm@0: break; andrewm@0: if(pastSamples_[index].count == 0) { andrewm@0: if(touchWasOn) { andrewm@0: // We found a break in the touch; stop here. But don't stop andrewm@0: // if the first frames we consider have no touches. andrewm@0: if(index < pastSamples_.endIndex() - 1) andrewm@0: index++; andrewm@0: break; andrewm@0: } andrewm@0: } andrewm@0: else if(!touchWasOn) { andrewm@0: mostRecentTouchPresentIndex = index; andrewm@0: touchWasOn = true; andrewm@0: } andrewm@0: if(sampleCount++ >= kDefaultMaxLookbackSamples) andrewm@0: break; andrewm@0: andrewm@0: // Can't decrement past 0 in an unsigned type andrewm@0: if(index == 0) andrewm@0: break; andrewm@0: index--; andrewm@0: } andrewm@0: andrewm@0: // If we fell off the beginning of the buffer, back up. andrewm@0: if(index < pastSamples_.beginIndex()) andrewm@0: index = pastSamples_.beginIndex(); andrewm@0: andrewm@0: // Need at least two points for this calculation to work andrewm@0: timestamp_type endingTimestamp = pastSamples_.timestampAt(mostRecentTouchPresentIndex); andrewm@0: timestamp_type startingTimestamp = pastSamples_.timestampAt(index); andrewm@0: if(endingTimestamp - startingTimestamp > 0) { andrewm@0: float endingPosition = pastSamples_[mostRecentTouchPresentIndex].locs[0]; andrewm@0: float startingPosition = pastSamples_[index].locs[0]; andrewm@0: calculatedVelocity = (endingPosition - startingPosition) / (endingTimestamp - startingTimestamp); andrewm@0: } andrewm@0: else { // DEBUG andrewm@0: #ifdef DEBUG_NOTE_ONSET_MAPPING andrewm@0: std::cout << "Found 0 timestamp difference on key onset (indices " << index << " and " << pastSamples_.endIndex() - 1 << "\n"; andrewm@0: #endif andrewm@0: } andrewm@0: } andrewm@0: else { andrewm@0: #ifdef DEBUG_NOTE_ONSET_MAPPING andrewm@0: std::cout << "Found empty touch buffer on key onset\n"; andrewm@0: #endif andrewm@0: } andrewm@0: andrewm@0: sampleBufferMutex_.exit(); andrewm@0: andrewm@0: if(!missing_value::isMissing(calculatedVelocity)) { andrewm@0: #ifdef DEBUG_NOTE_ONSET_MAPPING andrewm@0: std::cout << "Found onset velocity " << calculatedVelocity << " on note " << noteNumber_ << std::endl; andrewm@0: #endif andrewm@0: if(calculatedVelocity > 6.0) andrewm@0: calculatedVelocity = 6.0; andrewm@0: andrewm@0: if(calculatedVelocity > 1.5) { andrewm@0: startingPitchBendSemitones_ = -1.0 * (calculatedVelocity / 5.0); andrewm@0: rampLength_ = milliseconds_to_timestamp((50.0 + calculatedVelocity*25.0)); andrewm@0: rampBeginTime_ = keyboard_.schedulerCurrentTimestamp(); andrewm@0: } andrewm@0: else andrewm@0: rampLength_ = 0; andrewm@0: andrewm@0: sendOnsetAngleMessage(calculatedVelocity); andrewm@0: } andrewm@0: } andrewm@0: andrewm@0: void TouchkeyOnsetAngleMapping::sendOnsetAngleMessage(float onsetAngle, bool force) { andrewm@0: if(force || !suspended_) { andrewm@0: keyboard_.sendMessage("/touchkeys/onsetangle", "if", noteNumber_, onsetAngle, LO_ARGS_END); andrewm@0: } andrewm@0: } andrewm@0: andrewm@0: // Send the pitch bend message of a given number of a semitones. Send by OSC, andrewm@0: // which can be mapped to MIDI CC externally andrewm@0: void TouchkeyOnsetAngleMapping::sendPitchBendMessage(float pitchBendSemitones, bool force) { andrewm@0: if(force || !suspended_) andrewm@0: keyboard_.sendMessage("/touchkeys/scoop", "if", noteNumber_, pitchBendSemitones, LO_ARGS_END); andrewm@0: }