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: TouchkeyReleaseAngleMapping.cpp: per-note mapping for the release angle andrewm@0: mapping, which measures the speed of finger motion along the key at andrewm@0: the time of MIDI note off. andrewm@0: */ andrewm@0: andrewm@0: #include "TouchkeyReleaseAngleMapping.h" andrewm@46: #include "TouchkeyReleaseAngleMappingFactory.h" andrewm@0: #include "../MappingFactory.h" andrewm@0: #include "../../TouchKeys/MidiOutputController.h" andrewm@0: #include "../MappingScheduler.h" andrewm@0: andrewm@46: #define DEBUG_RELEASE_ANGLE_MAPPING andrewm@46: andrewm@0: // Class constants andrewm@0: const int TouchkeyReleaseAngleMapping::kDefaultFilterBufferLength = 30; andrewm@0: const timestamp_diff_type TouchkeyReleaseAngleMapping::kDefaultMaxLookbackTime = milliseconds_to_timestamp(100); andrewm@0: andrewm@46: const float TouchkeyReleaseAngleMapping::kDefaultUpMinimumAngle = 1.0; andrewm@46: const float TouchkeyReleaseAngleMapping::kDefaultDownMinimumAngle = 1.0; andrewm@46: 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: TouchkeyReleaseAngleMapping::TouchkeyReleaseAngleMapping(PianoKeyboard &keyboard, MappingFactory *factory, int noteNumber, Node* touchBuffer, andrewm@0: Node* positionBuffer, KeyPositionTracker* positionTracker) andrewm@0: : TouchkeyBaseMapping(keyboard, factory, noteNumber, touchBuffer, positionBuffer, positionTracker, false), andrewm@46: upEnabled_(true), downEnabled_(true), upMinimumAngle_(kDefaultUpMinimumAngle), downMinimumAngle_(kDefaultDownMinimumAngle), andrewm@0: pastSamples_(kDefaultFilterBufferLength), maxLookbackTime_(kDefaultMaxLookbackTime) andrewm@0: { andrewm@46: for(int i = 0; i < RELEASE_ANGLE_MAX_SEQUENCE_LENGTH; i++) andrewm@46: upNotes_[i] = downNotes_[i] = upVelocities_[i] = downVelocities_[i] = 0; andrewm@0: } andrewm@0: andrewm@0: // Reset state back to defaults andrewm@0: void TouchkeyReleaseAngleMapping::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 TouchkeyReleaseAngleMapping::resend() { andrewm@0: // Message is only sent at release; resend may not apply here. andrewm@0: } andrewm@0: andrewm@46: // Parameters for release angle algorithm andrewm@46: void TouchkeyReleaseAngleMapping::setWindowSize(float windowSize) { andrewm@46: // This was passed in in milliseconds and needs to be converted to a timestamp type andrewm@46: maxLookbackTime_ = milliseconds_to_timestamp(windowSize); andrewm@46: } andrewm@46: andrewm@46: void TouchkeyReleaseAngleMapping::setUpMessagesEnabled(bool enable) { andrewm@46: upEnabled_ = enable; andrewm@46: } andrewm@46: andrewm@46: void TouchkeyReleaseAngleMapping::setDownMessagesEnabled(bool enable) { andrewm@46: downEnabled_ = enable; andrewm@46: } andrewm@46: andrewm@46: void TouchkeyReleaseAngleMapping::setUpMinimumAngle(float minAngle) { andrewm@46: upMinimumAngle_ = fabsf(minAngle); andrewm@46: } andrewm@46: andrewm@46: void TouchkeyReleaseAngleMapping::setUpNote(int sequence, int note) { andrewm@46: if(sequence < 0 || sequence >= RELEASE_ANGLE_MAX_SEQUENCE_LENGTH) andrewm@46: return; andrewm@46: if(note < 0 || note > 127) andrewm@46: upNotes_[sequence] = 0; andrewm@46: else andrewm@46: upNotes_[sequence] = note; andrewm@46: } andrewm@46: andrewm@46: void TouchkeyReleaseAngleMapping::setUpVelocity(int sequence, int velocity) { andrewm@46: if(sequence < 0 || sequence >= RELEASE_ANGLE_MAX_SEQUENCE_LENGTH) andrewm@46: return; andrewm@46: if(velocity < 0 || velocity > 127) andrewm@46: upVelocities_[sequence] = 0; andrewm@46: else andrewm@46: upVelocities_[sequence] = velocity; andrewm@46: } andrewm@46: andrewm@46: void TouchkeyReleaseAngleMapping::setDownMinimumAngle(float minAngle) { andrewm@46: downMinimumAngle_ = fabsf(minAngle); andrewm@46: } andrewm@46: andrewm@46: void TouchkeyReleaseAngleMapping::setDownNote(int sequence, int note) { andrewm@46: if(sequence < 0 || sequence >= RELEASE_ANGLE_MAX_SEQUENCE_LENGTH) andrewm@46: return; andrewm@46: if(note < 0 || note > 127) andrewm@46: downNotes_[sequence] = 0; andrewm@46: else andrewm@46: downNotes_[sequence] = note; andrewm@46: } andrewm@46: andrewm@46: void TouchkeyReleaseAngleMapping::setDownVelocity(int sequence, int velocity) { andrewm@46: if(sequence < 0 || sequence >= RELEASE_ANGLE_MAX_SEQUENCE_LENGTH) andrewm@46: return; andrewm@46: if(velocity < 0 || velocity > 127) andrewm@46: downVelocities_[sequence] = 0; andrewm@46: else andrewm@46: downVelocities_[sequence] = velocity; andrewm@46: } andrewm@46: andrewm@0: // This method receives data from the touch buffer or possibly the continuous key angle (not used here) andrewm@0: void TouchkeyReleaseAngleMapping::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 TouchkeyReleaseAngleMapping::performMapping() { andrewm@0: // Nothing to do here until note is released. andrewm@0: // Register for the next update by returning its timestamp andrewm@0: // TODO: do we even need this? Check Mapping::engage() and Mapping::disengage() andrewm@0: timestamp_type currentTimestamp = keyboard_.schedulerCurrentTimestamp(); andrewm@0: nextScheduledTimestamp_ = currentTimestamp + updateInterval_; andrewm@0: return nextScheduledTimestamp_; andrewm@0: } andrewm@0: andrewm@46: void TouchkeyReleaseAngleMapping::midiNoteOffReceived(int channel) { andrewm@46: processRelease(); andrewm@46: } andrewm@46: andrewm@46: void TouchkeyReleaseAngleMapping::processRelease(/*timestamp_type timestamp*/) { andrewm@0: if(!noteIsOn_) { andrewm@0: return; andrewm@0: } 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: andrewm@0: if(!pastSamples_.empty()) { andrewm@0: Node::size_type index = pastSamples_.endIndex() - 1; andrewm@0: Node::size_type mostRecentTouchPresentIndex = pastSamples_.endIndex() - 1; andrewm@46: timestamp_type lastTimestamp = pastSamples_.timestampAt(index); andrewm@46: andrewm@0: while(index >= pastSamples_.beginIndex()) { andrewm@46: #ifdef DEBUG_RELEASE_ANGLE_MAPPING andrewm@46: std::cout << "examining sample " << index << " with " << pastSamples_[index].count << " touches and time diff " << lastTimestamp - pastSamples_.timestampAt(index) << "\n"; andrewm@46: #endif andrewm@46: if(lastTimestamp - 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: // 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@46: #ifdef DEBUG_RELEASE_ANGLE_MAPPING andrewm@0: std::cout << "Found 0 timestamp difference on key release (indices " << index << " and " << pastSamples_.endIndex() - 1 << "\n"; andrewm@46: #endif andrewm@0: } andrewm@0: } andrewm@46: else { andrewm@46: #ifdef DEBUG_RELEASE_ANGLE_MAPPING andrewm@0: std::cout << "Found empty touch buffer on key release\n"; andrewm@46: #endif andrewm@46: } andrewm@0: andrewm@0: sampleBufferMutex_.exit(); andrewm@0: andrewm@0: if(!missing_value::isMissing(calculatedVelocity)) { andrewm@46: #ifdef DEBUG_RELEASE_ANGLE_MAPPING andrewm@0: std::cout << "Found release velocity " << calculatedVelocity << " on note " << noteNumber_ << std::endl; andrewm@46: #endif andrewm@0: sendReleaseAngleMessage(calculatedVelocity); andrewm@0: } andrewm@0: andrewm@0: andrewm@46: // Check if we're supposed to clean up now andrewm@0: finished_ = true; andrewm@0: if(finishRequested_) andrewm@0: acknowledgeFinish(); andrewm@0: // KLUDGE andrewm@0: } andrewm@0: andrewm@0: void TouchkeyReleaseAngleMapping::sendReleaseAngleMessage(float releaseAngle, bool force) { andrewm@0: if(force || !suspended_) { andrewm@0: keyboard_.sendMessage("/touchkeys/releaseangle", "if", noteNumber_, releaseAngle, LO_ARGS_END); andrewm@0: andrewm@46: if(keyboard_.midiOutputController() == 0) andrewm@46: return; andrewm@46: andrewm@46: int port = static_cast(factory_)->segment().outputPort(); andrewm@46: int ch = keyboard_.key(noteNumber_)->midiChannel(); andrewm@46: andrewm@46: // Check if the release angle exceeds either the up or down threshold andrewm@46: if(releaseAngle > 0 && fabs(releaseAngle) >= upMinimumAngle_ && upEnabled_) { andrewm@46: #ifdef DEBUG_RELEASE_ANGLE_MAPPING andrewm@46: std::cout << "Send up-release messages for note " << noteNumber_ << " on channel " << ch << "\n"; andrewm@46: #endif andrewm@46: // Send key switches: note on and note off in reverse orders andrewm@46: for(int i = 0; i < RELEASE_ANGLE_MAX_SEQUENCE_LENGTH; i++) { andrewm@46: if(upNotes_[i] != 0) andrewm@46: keyboard_.midiOutputController()->sendNoteOn(port, ch, upNotes_[i], upVelocities_[i]); andrewm@46: } andrewm@46: andrewm@46: for(int i = RELEASE_ANGLE_MAX_SEQUENCE_LENGTH - 1; i >= 0; i--) { andrewm@46: if(upNotes_[i] != 0) andrewm@46: keyboard_.midiOutputController()->sendNoteOff(port, ch, upNotes_[i]); andrewm@46: } andrewm@46: } andrewm@46: else if(releaseAngle < 0 && fabs(releaseAngle) >= downMinimumAngle_ && downEnabled_) { andrewm@46: #ifdef DEBUG_RELEASE_ANGLE_MAPPING andrewm@46: std::cout << "Send down-release messages for note " << noteNumber_ << " on channel " << ch << "\n"; andrewm@46: #endif andrewm@46: // Send key switches: note on and note off in reverse orders andrewm@46: for(int i = 0; i < RELEASE_ANGLE_MAX_SEQUENCE_LENGTH; i++) { andrewm@46: if(downNotes_[i] != 0) andrewm@46: keyboard_.midiOutputController()->sendNoteOn(port, ch, downNotes_[i], downVelocities_[i]); andrewm@46: } andrewm@46: andrewm@46: for(int i = RELEASE_ANGLE_MAX_SEQUENCE_LENGTH - 1; i >= 0; i--) { andrewm@46: if(downNotes_[i] != 0) andrewm@46: keyboard_.midiOutputController()->sendNoteOff(port, ch, downNotes_[i]); andrewm@46: } andrewm@46: } andrewm@46: andrewm@46: // TODO: delayed release andrewm@46: andrewm@0: #ifdef TROMBONE andrewm@0: // KLUDGE: figure out how to do this more elegantly andrewm@0: if(keyboard_.midiOutputController() != 0) { andrewm@0: if(releaseAngle > 1.0) { andrewm@0: keyboard_.midiOutputController()->sendNoteOn(0, 0, 36, 64); andrewm@0: keyboard_.midiOutputController()->sendNoteOn(0, 0, 31, 96); andrewm@0: keyboard_.midiOutputController()->sendNoteOff(0, 0, 31); andrewm@0: keyboard_.midiOutputController()->sendNoteOff(0, 0, 36); andrewm@0: } andrewm@0: else if(releaseAngle < -1.5) { andrewm@0: keyboard_.midiOutputController()->sendNoteOn(0, 0, 36, 64); andrewm@0: keyboard_.midiOutputController()->sendNoteOn(0, 0, 33, 80); andrewm@0: keyboard_.midiOutputController()->sendNoteOff(0, 0, 33); andrewm@0: keyboard_.midiOutputController()->sendNoteOff(0, 0, 36); andrewm@0: } andrewm@0: } andrewm@0: #elif defined(TRUMPET) andrewm@0: if(keyboard_.midiOutputController() != 0) { andrewm@0: if(releaseAngle > 1.0) { andrewm@0: keyboard_.midiOutputController()->sendNoteOn(0, 0, 48, 64); andrewm@0: keyboard_.midiOutputController()->sendNoteOn(0, 0, 42, 96); andrewm@0: //keyboard_.midiOutputController()->sendNoteOff(0, 0, 42); andrewm@0: keyboard_.midiOutputController()->sendNoteOff(0, 0, 48); andrewm@0: keyboard_.scheduleEvent(this, boost::bind(&TouchkeyReleaseAngleMapping::releaseKeySwitch, this), andrewm@0: keyboard_.schedulerCurrentTimestamp() + milliseconds_to_timestamp(250)); andrewm@0: } andrewm@0: else if(releaseAngle < -1.5) { andrewm@0: keyboard_.midiOutputController()->sendNoteOn(0, 0, 48, 64); andrewm@0: keyboard_.midiOutputController()->sendNoteOn(0, 0, 46, 96); andrewm@0: //keyboard_.midiOutputController()->sendNoteOff(0, 0, 47); andrewm@0: keyboard_.midiOutputController()->sendNoteOff(0, 0, 48); andrewm@0: keyboard_.scheduleEvent(this, boost::bind(&TouchkeyReleaseAngleMapping::releaseKeySwitch, this), andrewm@0: keyboard_.schedulerCurrentTimestamp() + milliseconds_to_timestamp(250)); andrewm@0: } andrewm@0: else { andrewm@0: // Check if we're suppose to clean up now andrewm@0: finished_ = true; andrewm@0: if(finishRequested_) andrewm@0: acknowledgeFinish(); andrewm@0: } andrewm@0: } andrewm@0: #endif andrewm@0: } andrewm@0: } andrewm@0: andrewm@0: timestamp_type TouchkeyReleaseAngleMapping::releaseKeySwitch() { andrewm@0: keyboard_.midiOutputController()->sendNoteOff(0, 0, 42); andrewm@0: keyboard_.midiOutputController()->sendNoteOff(0, 0, 46); andrewm@0: andrewm@0: // Check if we're suppose to clean up now andrewm@0: /*finished_ = true; andrewm@0: if(finishRequested_) andrewm@0: acknowledgeFinish();*/ andrewm@0: andrewm@0: return 0; andrewm@0: }