Mercurial > hg > touchkeys
view Source/Mappings/ReleaseAngle/TouchkeyReleaseAngleMapping.cpp @ 56:b4a2d2ae43cf tip
merge
author | Andrew McPherson <andrewm@eecs.qmul.ac.uk> |
---|---|
date | Fri, 23 Nov 2018 15:48:14 +0000 |
parents | 78b9808a2c65 |
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/>. ===================================================================== TouchkeyReleaseAngleMapping.cpp: per-note mapping for the release angle mapping, which measures the speed of finger motion along the key at the time of MIDI note off. */ #include "TouchkeyReleaseAngleMapping.h" #include "TouchkeyReleaseAngleMappingFactory.h" #include "../MappingFactory.h" #include "../../TouchKeys/MidiOutputController.h" #include "../MappingScheduler.h" #define DEBUG_RELEASE_ANGLE_MAPPING // Class constants const int TouchkeyReleaseAngleMapping::kDefaultFilterBufferLength = 30; const timestamp_diff_type TouchkeyReleaseAngleMapping::kDefaultMaxLookbackTime = milliseconds_to_timestamp(100); const float TouchkeyReleaseAngleMapping::kDefaultUpMinimumAngle = 1.0; const float TouchkeyReleaseAngleMapping::kDefaultDownMinimumAngle = 1.0; // 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 TouchkeyReleaseAngleMapping::TouchkeyReleaseAngleMapping(PianoKeyboard &keyboard, MappingFactory *factory, int noteNumber, Node<KeyTouchFrame>* touchBuffer, Node<key_position>* positionBuffer, KeyPositionTracker* positionTracker) : TouchkeyBaseMapping(keyboard, factory, noteNumber, touchBuffer, positionBuffer, positionTracker, false), upEnabled_(true), downEnabled_(true), upMinimumAngle_(kDefaultUpMinimumAngle), downMinimumAngle_(kDefaultDownMinimumAngle), pastSamples_(kDefaultFilterBufferLength), maxLookbackTime_(kDefaultMaxLookbackTime) { for(int i = 0; i < RELEASE_ANGLE_MAX_SEQUENCE_LENGTH; i++) upNotes_[i] = downNotes_[i] = upVelocities_[i] = downVelocities_[i] = 0; } // Reset state back to defaults void TouchkeyReleaseAngleMapping::reset() { ScopedLock sl(sampleBufferMutex_); TouchkeyBaseMapping::reset(); pastSamples_.clear(); } // Resend all current parameters void TouchkeyReleaseAngleMapping::resend() { // Message is only sent at release; resend may not apply here. } // Parameters for release angle algorithm void TouchkeyReleaseAngleMapping::setWindowSize(float windowSize) { // This was passed in in milliseconds and needs to be converted to a timestamp type maxLookbackTime_ = milliseconds_to_timestamp(windowSize); } void TouchkeyReleaseAngleMapping::setUpMessagesEnabled(bool enable) { upEnabled_ = enable; } void TouchkeyReleaseAngleMapping::setDownMessagesEnabled(bool enable) { downEnabled_ = enable; } void TouchkeyReleaseAngleMapping::setUpMinimumAngle(float minAngle) { upMinimumAngle_ = fabsf(minAngle); } void TouchkeyReleaseAngleMapping::setUpNote(int sequence, int note) { if(sequence < 0 || sequence >= RELEASE_ANGLE_MAX_SEQUENCE_LENGTH) return; if(note < 0 || note > 127) upNotes_[sequence] = 0; else upNotes_[sequence] = note; } void TouchkeyReleaseAngleMapping::setUpVelocity(int sequence, int velocity) { if(sequence < 0 || sequence >= RELEASE_ANGLE_MAX_SEQUENCE_LENGTH) return; if(velocity < 0 || velocity > 127) upVelocities_[sequence] = 0; else upVelocities_[sequence] = velocity; } void TouchkeyReleaseAngleMapping::setDownMinimumAngle(float minAngle) { downMinimumAngle_ = fabsf(minAngle); } void TouchkeyReleaseAngleMapping::setDownNote(int sequence, int note) { if(sequence < 0 || sequence >= RELEASE_ANGLE_MAX_SEQUENCE_LENGTH) return; if(note < 0 || note > 127) downNotes_[sequence] = 0; else downNotes_[sequence] = note; } void TouchkeyReleaseAngleMapping::setDownVelocity(int sequence, int velocity) { if(sequence < 0 || sequence >= RELEASE_ANGLE_MAX_SEQUENCE_LENGTH) return; if(velocity < 0 || velocity > 127) downVelocities_[sequence] = 0; else downVelocities_[sequence] = velocity; } // This method receives data from the touch buffer or possibly the continuous key angle (not used here) void TouchkeyReleaseAngleMapping::triggerReceived(TriggerSource* who, timestamp_type timestamp) { if(who == touchBuffer_) { ScopedLock sl(sampleBufferMutex_); // Save the latest frame, even if it is an empty touch (we need to know what happened even // after the touch ends since the MIDI off may come later) if(!touchBuffer_->empty()) pastSamples_.insert(touchBuffer_->latest(), touchBuffer_->latestTimestamp()); } } // 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 TouchkeyReleaseAngleMapping::performMapping() { // Nothing to do here until note is released. // Register for the next update by returning its timestamp // TODO: do we even need this? Check Mapping::engage() and Mapping::disengage() timestamp_type currentTimestamp = keyboard_.schedulerCurrentTimestamp(); nextScheduledTimestamp_ = currentTimestamp + updateInterval_; return nextScheduledTimestamp_; } void TouchkeyReleaseAngleMapping::midiNoteOffReceived(int channel) { processRelease(); } void TouchkeyReleaseAngleMapping::processRelease(/*timestamp_type timestamp*/) { if(!noteIsOn_) { return; } sampleBufferMutex_.enter(); // Look backwards from the current timestamp to find the velocity float calculatedVelocity = missing_value<float>::missing(); bool touchWasOn = false; if(!pastSamples_.empty()) { Node<KeyTouchFrame>::size_type index = pastSamples_.endIndex() - 1; Node<KeyTouchFrame>::size_type mostRecentTouchPresentIndex = pastSamples_.endIndex() - 1; timestamp_type lastTimestamp = pastSamples_.timestampAt(index); while(index >= pastSamples_.beginIndex()) { #ifdef DEBUG_RELEASE_ANGLE_MAPPING std::cout << "examining sample " << index << " with " << pastSamples_[index].count << " touches and time diff " << lastTimestamp - pastSamples_.timestampAt(index) << "\n"; #endif if(lastTimestamp - pastSamples_.timestampAt(index) >= maxLookbackTime_) break; if(pastSamples_[index].count == 0) { if(touchWasOn) { // We found a break in the touch; stop here. But don't stop // if the first frames we consider have no touches. if(index < pastSamples_.endIndex() - 1) index++; break; } } else if(!touchWasOn) { mostRecentTouchPresentIndex = index; touchWasOn = true; } // Can't decrement past 0 in an unsigned type if(index == 0) break; index--; } // If we fell off the beginning of the buffer, back up. if(index < pastSamples_.beginIndex()) index = pastSamples_.beginIndex(); // Need at least two points for this calculation to work timestamp_type endingTimestamp = pastSamples_.timestampAt(mostRecentTouchPresentIndex); timestamp_type startingTimestamp = pastSamples_.timestampAt(index); if(endingTimestamp - startingTimestamp > 0) { float endingPosition = pastSamples_[mostRecentTouchPresentIndex].locs[0]; float startingPosition = pastSamples_[index].locs[0]; calculatedVelocity = (endingPosition - startingPosition) / (endingTimestamp - startingTimestamp); } else { // DEBUG #ifdef DEBUG_RELEASE_ANGLE_MAPPING std::cout << "Found 0 timestamp difference on key release (indices " << index << " and " << pastSamples_.endIndex() - 1 << "\n"; #endif } } else { #ifdef DEBUG_RELEASE_ANGLE_MAPPING std::cout << "Found empty touch buffer on key release\n"; #endif } sampleBufferMutex_.exit(); if(!missing_value<float>::isMissing(calculatedVelocity)) { #ifdef DEBUG_RELEASE_ANGLE_MAPPING std::cout << "Found release velocity " << calculatedVelocity << " on note " << noteNumber_ << std::endl; #endif sendReleaseAngleMessage(calculatedVelocity); } // Check if we're supposed to clean up now finished_ = true; if(finishRequested_) acknowledgeFinish(); // KLUDGE } void TouchkeyReleaseAngleMapping::sendReleaseAngleMessage(float releaseAngle, bool force) { if(force || !suspended_) { keyboard_.sendMessage("/touchkeys/releaseangle", "if", noteNumber_, releaseAngle, LO_ARGS_END); if(keyboard_.midiOutputController() == 0) return; int port = static_cast<TouchkeyReleaseAngleMappingFactory*>(factory_)->segment().outputPort(); int ch = keyboard_.key(noteNumber_)->midiChannel(); // Check if the release angle exceeds either the up or down threshold if(releaseAngle > 0 && fabs(releaseAngle) >= upMinimumAngle_ && upEnabled_) { #ifdef DEBUG_RELEASE_ANGLE_MAPPING std::cout << "Send up-release messages for note " << noteNumber_ << " on channel " << ch << "\n"; #endif // Send key switches: note on and note off in reverse orders for(int i = 0; i < RELEASE_ANGLE_MAX_SEQUENCE_LENGTH; i++) { if(upNotes_[i] != 0) keyboard_.midiOutputController()->sendNoteOn(port, ch, upNotes_[i], upVelocities_[i]); } for(int i = RELEASE_ANGLE_MAX_SEQUENCE_LENGTH - 1; i >= 0; i--) { if(upNotes_[i] != 0) keyboard_.midiOutputController()->sendNoteOff(port, ch, upNotes_[i]); } } else if(releaseAngle < 0 && fabs(releaseAngle) >= downMinimumAngle_ && downEnabled_) { #ifdef DEBUG_RELEASE_ANGLE_MAPPING std::cout << "Send down-release messages for note " << noteNumber_ << " on channel " << ch << "\n"; #endif // Send key switches: note on and note off in reverse orders for(int i = 0; i < RELEASE_ANGLE_MAX_SEQUENCE_LENGTH; i++) { if(downNotes_[i] != 0) keyboard_.midiOutputController()->sendNoteOn(port, ch, downNotes_[i], downVelocities_[i]); } for(int i = RELEASE_ANGLE_MAX_SEQUENCE_LENGTH - 1; i >= 0; i--) { if(downNotes_[i] != 0) keyboard_.midiOutputController()->sendNoteOff(port, ch, downNotes_[i]); } } // TODO: delayed release #ifdef TROMBONE // KLUDGE: figure out how to do this more elegantly if(keyboard_.midiOutputController() != 0) { if(releaseAngle > 1.0) { keyboard_.midiOutputController()->sendNoteOn(0, 0, 36, 64); keyboard_.midiOutputController()->sendNoteOn(0, 0, 31, 96); keyboard_.midiOutputController()->sendNoteOff(0, 0, 31); keyboard_.midiOutputController()->sendNoteOff(0, 0, 36); } else if(releaseAngle < -1.5) { keyboard_.midiOutputController()->sendNoteOn(0, 0, 36, 64); keyboard_.midiOutputController()->sendNoteOn(0, 0, 33, 80); keyboard_.midiOutputController()->sendNoteOff(0, 0, 33); keyboard_.midiOutputController()->sendNoteOff(0, 0, 36); } } #elif defined(TRUMPET) if(keyboard_.midiOutputController() != 0) { if(releaseAngle > 1.0) { keyboard_.midiOutputController()->sendNoteOn(0, 0, 48, 64); keyboard_.midiOutputController()->sendNoteOn(0, 0, 42, 96); //keyboard_.midiOutputController()->sendNoteOff(0, 0, 42); keyboard_.midiOutputController()->sendNoteOff(0, 0, 48); keyboard_.scheduleEvent(this, boost::bind(&TouchkeyReleaseAngleMapping::releaseKeySwitch, this), keyboard_.schedulerCurrentTimestamp() + milliseconds_to_timestamp(250)); } else if(releaseAngle < -1.5) { keyboard_.midiOutputController()->sendNoteOn(0, 0, 48, 64); keyboard_.midiOutputController()->sendNoteOn(0, 0, 46, 96); //keyboard_.midiOutputController()->sendNoteOff(0, 0, 47); keyboard_.midiOutputController()->sendNoteOff(0, 0, 48); keyboard_.scheduleEvent(this, boost::bind(&TouchkeyReleaseAngleMapping::releaseKeySwitch, this), keyboard_.schedulerCurrentTimestamp() + milliseconds_to_timestamp(250)); } else { // Check if we're suppose to clean up now finished_ = true; if(finishRequested_) acknowledgeFinish(); } } #endif } } timestamp_type TouchkeyReleaseAngleMapping::releaseKeySwitch() { keyboard_.midiOutputController()->sendNoteOff(0, 0, 42); keyboard_.midiOutputController()->sendNoteOff(0, 0, 46); // Check if we're suppose to clean up now /*finished_ = true; if(finishRequested_) acknowledgeFinish();*/ return 0; }