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: }