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