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: TouchkeyMultiFingerTriggerMapping.cpp: per-note mapping for the multiple- andrewm@0: finger trigger mapping, which performs actions when two or more fingers andrewm@0: are added or removed from the key. andrewm@0: */ andrewm@0: andrewm@0: #include "TouchkeyMultiFingerTriggerMapping.h" andrewm@42: #include "TouchkeyMultiFingerTriggerMappingFactory.h" andrewm@0: #include "../../TouchKeys/MidiOutputController.h" andrewm@0: andrewm@0: // Class constants andrewm@0: const int TouchkeyMultiFingerTriggerMapping::kDefaultFilterBufferLength = 30; andrewm@0: const int TouchkeyMultiFingerTriggerMapping::kDefaultNumTouchesForTrigger = 2; andrewm@0: const int TouchkeyMultiFingerTriggerMapping::kDefaultNumFramesForTrigger = 2; andrewm@0: const int TouchkeyMultiFingerTriggerMapping::kDefaultNumConsecutiveTapsForTrigger = 1; andrewm@42: const timestamp_diff_type TouchkeyMultiFingerTriggerMapping::kDefaultMaxTapSpacing = milliseconds_to_timestamp(300.0); andrewm@42: const int TouchkeyMultiFingerTriggerMapping::kDefaultTriggerOnAction = TouchkeyMultiFingerTriggerMapping::kActionNoteOn; andrewm@42: const int TouchkeyMultiFingerTriggerMapping::kDefaultTriggerOffAction = TouchkeyMultiFingerTriggerMapping::kActionNone; andrewm@42: const int TouchkeyMultiFingerTriggerMapping::kDefaultTriggerOnNoteNum = -1; andrewm@42: const int TouchkeyMultiFingerTriggerMapping::kDefaultTriggerOffNoteNum = -1; andrewm@42: const int TouchkeyMultiFingerTriggerMapping::kDefaultTriggerOnNoteVel = -1; andrewm@42: const int TouchkeyMultiFingerTriggerMapping::kDefaultTriggerOffNoteVel = -1; 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: TouchkeyMultiFingerTriggerMapping::TouchkeyMultiFingerTriggerMapping(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: numTouchesForTrigger_(kDefaultNumTouchesForTrigger), numFramesForTrigger_(kDefaultNumFramesForTrigger), andrewm@0: numConsecutiveTapsForTrigger_(kDefaultNumConsecutiveTapsForTrigger), maxTapSpacing_(kDefaultMaxTapSpacing), andrewm@42: needsMidiNoteOn_(true), triggerOnAction_(kDefaultTriggerOnAction), triggerOffAction_(kDefaultTriggerOffAction), andrewm@42: triggerOnNoteNum_(kDefaultTriggerOnNoteNum), triggerOffNoteNum_(kDefaultTriggerOffNoteNum), andrewm@42: triggerOnNoteVel_(kDefaultTriggerOnNoteVel), triggerOffNoteVel_(kDefaultTriggerOffNoteVel), andrewm@42: pastSamples_(kDefaultFilterBufferLength) andrewm@0: { andrewm@0: reset(); andrewm@0: } andrewm@0: andrewm@42: // Turn off mapping of data. andrewm@42: void TouchkeyMultiFingerTriggerMapping::disengage(bool shouldDelete) { andrewm@42: // Send note off messages for anything currently on andrewm@42: std::set >::iterator it; andrewm@42: int port = static_cast(factory_)->segment().outputPort(); andrewm@42: andrewm@42: for(it = otherNotesOn_.begin(); it != otherNotesOn_.end(); ++it) { andrewm@42: int ch = it->first; andrewm@42: int note = it->second; andrewm@42: andrewm@42: keyboard_.midiOutputController()->sendNoteOn(port, ch, note, 0); andrewm@42: } andrewm@42: andrewm@42: otherNotesOn_.clear(); andrewm@42: TouchkeyBaseMapping::disengage(shouldDelete); andrewm@42: } andrewm@42: andrewm@0: // Reset state back to defaults andrewm@0: void TouchkeyMultiFingerTriggerMapping::reset() { andrewm@0: ScopedLock sl(sampleBufferMutex_); andrewm@0: andrewm@0: TouchkeyBaseMapping::reset(); andrewm@0: pastSamples_.clear(); andrewm@0: andrewm@0: lastNumActiveTouches_ = 0; andrewm@0: lastActiveTouchLocations_[0] = lastActiveTouchLocations_[1] = lastActiveTouchLocations_[2] = 0; andrewm@0: framesCount_ = 0; andrewm@0: tapsCount_ = 0; andrewm@0: hasGeneratedTap_ = false; andrewm@0: lastTapStartTimestamp_ = missing_value::missing(); andrewm@0: hasTriggered_ = false; andrewm@0: } andrewm@0: andrewm@0: // Resend all current parameters andrewm@0: void TouchkeyMultiFingerTriggerMapping::resend() { andrewm@0: // Message is only sent at release; resend may not apply here. andrewm@0: } andrewm@0: andrewm@42: void TouchkeyMultiFingerTriggerMapping::setTouchesForTrigger(int touches) { andrewm@42: numTouchesForTrigger_ = touches; andrewm@42: } andrewm@42: andrewm@42: void TouchkeyMultiFingerTriggerMapping::setFramesForTrigger(int frames) { andrewm@42: numFramesForTrigger_ = frames; andrewm@42: } andrewm@42: andrewm@42: void TouchkeyMultiFingerTriggerMapping::setConsecutiveTapsForTrigger(int taps) { andrewm@42: numConsecutiveTapsForTrigger_ = taps; andrewm@42: } andrewm@42: andrewm@42: void TouchkeyMultiFingerTriggerMapping::setMaxTimeBetweenTapsForTrigger(timestamp_diff_type timeDiff) { andrewm@42: maxTapSpacing_ = timeDiff; andrewm@42: } andrewm@42: andrewm@42: void TouchkeyMultiFingerTriggerMapping::setNeedsMidiNoteOn(bool needsMidi) { andrewm@42: needsMidiNoteOn_ = needsMidi; andrewm@42: } andrewm@42: andrewm@42: void TouchkeyMultiFingerTriggerMapping::setTriggerOnAction(int action) { andrewm@42: triggerOnAction_ = action; andrewm@42: } andrewm@42: andrewm@42: void TouchkeyMultiFingerTriggerMapping::setTriggerOffAction(int action) { andrewm@42: triggerOffAction_ = action; andrewm@42: } andrewm@42: andrewm@42: void TouchkeyMultiFingerTriggerMapping::setTriggerOnNoteNumber(int note) { andrewm@42: triggerOnNoteNum_ = note; andrewm@42: } andrewm@42: andrewm@42: void TouchkeyMultiFingerTriggerMapping::setTriggerOffNoteNumber(int note) { andrewm@42: triggerOffNoteNum_ = note; andrewm@42: } andrewm@42: andrewm@42: void TouchkeyMultiFingerTriggerMapping::setTriggerOnNoteVelocity(int velocity) { andrewm@42: triggerOnNoteVel_ = velocity; andrewm@42: } andrewm@42: andrewm@42: void TouchkeyMultiFingerTriggerMapping::setTriggerOffNoteVelocity(int velocity) { andrewm@42: triggerOffNoteVel_ = velocity; andrewm@42: } andrewm@42: andrewm@0: // This method receives data from the touch buffer or possibly the continuous key angle (not used here) andrewm@0: void TouchkeyMultiFingerTriggerMapping::triggerReceived(TriggerSource* who, timestamp_type timestamp) { andrewm@0: if(needsMidiNoteOn_ && !noteIsOn_) { andrewm@0: framesCount_ = 0; andrewm@0: hasGeneratedTap_ = false; andrewm@0: return; andrewm@0: } andrewm@0: andrewm@0: if(who == touchBuffer_) { andrewm@0: if(!touchBuffer_->empty()) { andrewm@0: // Find the current number of touches andrewm@0: KeyTouchFrame frame = touchBuffer_->latest(); andrewm@0: int count = frame.count; andrewm@0: andrewm@0: if(count < numTouchesForTrigger_) { andrewm@0: framesCount_ = 0; andrewm@0: hasGeneratedTap_ = false; andrewm@0: if(hasTriggered_) { andrewm@0: generateTriggerOff(timestamp); andrewm@0: hasTriggered_ = false; andrewm@0: } andrewm@0: } andrewm@0: else if(count == numTouchesForTrigger_) { andrewm@0: framesCount_++; andrewm@0: if(framesCount_ >= numFramesForTrigger_ && !hasGeneratedTap_) { andrewm@0: // Enough frames have elapsed to consider this a tap andrewm@0: // Figure out if it is a multiple consecutive tap or the first andrewm@0: // of a set. andrewm@0: if(!missing_value::isMissing(lastTapStartTimestamp_)) { andrewm@0: if(timestamp - lastTapStartTimestamp_ < maxTapSpacing_) { andrewm@0: tapsCount_++; andrewm@0: } andrewm@0: else andrewm@0: tapsCount_ = 1; andrewm@0: } andrewm@0: else andrewm@0: tapsCount_ = 1; andrewm@0: andrewm@0: // Check if the right number of taps has elapsed andrewm@0: if(tapsCount_ >= numConsecutiveTapsForTrigger_ && !hasTriggered_) { andrewm@0: hasTriggered_ = true; andrewm@0: andrewm@0: // Find the ID of the newest touch and compare its location andrewm@0: // to the immediately preceding touch(es) to find the distance andrewm@0: int newest = 0, oldest = 0, newestId = -1, oldestId = 1000000; andrewm@0: for(int i = 0; i < count; i++) { andrewm@0: if(frame.ids[i] > newestId) { andrewm@0: newest = i; andrewm@0: newestId = frame.ids[i]; andrewm@0: } andrewm@0: if(frame.ids[i] < oldestId) { andrewm@0: oldest = i; andrewm@0: oldestId = frame.ids[i]; andrewm@0: } andrewm@0: } andrewm@0: andrewm@0: // Find the distance between the point before this tap and the andrewm@0: // point that was added to create the tap. If this is a 3-touch andrewm@0: // tap, find the distance between the farthest two points, with andrewm@0: // the direction determined by which end is older. andrewm@0: float distance = frame.locs[newest] - frame.locs[oldest]; andrewm@0: if(count == 3) { andrewm@0: if(fabsf(frame.locs[2] - frame.locs[0]) > fabsf(distance)) { andrewm@0: if(frame.ids[2] > frame.ids[0]) andrewm@0: distance = frame.locs[2] - frame.locs[0]; andrewm@0: else andrewm@0: distance = frame.locs[0] - frame.locs[2]; andrewm@0: } andrewm@0: } andrewm@0: andrewm@0: // Generate the trigger. If a multi-tap gesture, also indicate the timing andrewm@0: if(numConsecutiveTapsForTrigger_ <= 1) andrewm@0: generateTriggerOn(timestamp, 0, distance); andrewm@0: else andrewm@0: generateTriggerOn(timestamp, timestamp - lastTapStartTimestamp_, distance); andrewm@0: } andrewm@0: andrewm@0: hasGeneratedTap_ = true; andrewm@0: lastTapStartTimestamp_ = timestamp; andrewm@0: } andrewm@0: } andrewm@0: andrewm@0: // Save the count locations for next time andrewm@0: lastNumActiveTouches_ = frame.count; andrewm@0: for(int i = 0; i < count; i++) { andrewm@0: lastActiveTouchLocations_[i] = frame.locs[i]; andrewm@0: } andrewm@0: } 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 TouchkeyMultiFingerTriggerMapping::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@0: void TouchkeyMultiFingerTriggerMapping::generateTriggerOn(timestamp_type timestamp, timestamp_diff_type timeBetweenTaps, float distanceBetweenPoints) { andrewm@0: if(!suspended_) { andrewm@42: if(triggerOnAction_ == kActionNoteOn || andrewm@42: triggerOnAction_ == kActionNoteOff) { andrewm@42: // Send a MIDI note on message with given note number and velocity andrewm@42: int port = static_cast(factory_)->segment().outputPort(); andrewm@0: int ch = keyboard_.key(noteNumber_)->midiChannel(); andrewm@42: int vel = triggerOnNoteVel_; andrewm@42: int note = triggerOnNoteNum_; andrewm@42: if(note < 0) // note < 0 means current note andrewm@42: note = noteNumber_; andrewm@42: if(note < 128) { andrewm@42: if(triggerOnAction_ == kActionNoteOn) { andrewm@42: // Can't send notes above 127... andrewm@42: if(vel < 0) // vel < 0 means same as current andrewm@42: vel = keyboard_.key(noteNumber_)->midiVelocity(); andrewm@42: if(vel > 127) andrewm@42: vel = 127; andrewm@42: andrewm@42: // Register that this note has been turned on andrewm@42: if(note != noteNumber_) andrewm@42: otherNotesOn_.insert(std::pair(ch, note)); andrewm@42: } andrewm@42: else { andrewm@42: // Note off andrewm@42: vel = 0; andrewm@42: if(note != noteNumber_) { andrewm@42: // Unregister this note if we are turning it off andrewm@42: if(otherNotesOn_.count(std::pair(ch, note)) > 0) { andrewm@42: otherNotesOn_.erase(std::pair(ch, note)); andrewm@42: } andrewm@42: } andrewm@42: } andrewm@42: andrewm@42: keyboard_.midiOutputController()->sendNoteOn(port, ch, note, vel); andrewm@42: } andrewm@0: } andrewm@0: } andrewm@0: } andrewm@0: andrewm@0: void TouchkeyMultiFingerTriggerMapping::generateTriggerOff(timestamp_type timestamp) { andrewm@0: if(!suspended_) { andrewm@42: if(triggerOffAction_ == kActionNoteOn || andrewm@42: triggerOffAction_ == kActionNoteOff) { andrewm@42: // Send a MIDI note on message with given note number and velocity andrewm@42: int port = static_cast(factory_)->segment().outputPort(); andrewm@42: int ch = keyboard_.key(noteNumber_)->midiChannel(); andrewm@42: int vel = triggerOffNoteVel_; andrewm@42: int note = triggerOffNoteNum_; andrewm@42: if(note < 0) // note < 0 means current note andrewm@42: note = noteNumber_; andrewm@42: if(note < 128) { andrewm@42: if(triggerOffAction_ == kActionNoteOn) { andrewm@42: // Can't send notes above 127... andrewm@42: if(vel < 0) // vel < 0 means same as current andrewm@42: vel = keyboard_.key(noteNumber_)->midiVelocity(); andrewm@42: if(vel > 127) andrewm@42: vel = 127; andrewm@42: andrewm@42: // Register that this note has been turned on andrewm@42: if(note != noteNumber_) andrewm@42: otherNotesOn_.insert(std::pair(ch, note)); andrewm@42: } andrewm@42: else { andrewm@42: // Note off andrewm@42: vel = 0; andrewm@42: if(note != noteNumber_) { andrewm@42: // Unregister this note if we are turning it off andrewm@42: if(otherNotesOn_.count(std::pair(ch, note)) > 0) { andrewm@42: otherNotesOn_.erase(std::pair(ch, note)); andrewm@42: } andrewm@42: } andrewm@42: } andrewm@42: andrewm@42: keyboard_.midiOutputController()->sendNoteOn(port, ch, note, vel); andrewm@42: } andrewm@42: } andrewm@0: } andrewm@0: } andrewm@0: andrewm@0: // MIDI note-off message received andrewm@0: void TouchkeyMultiFingerTriggerMapping::midiNoteOffReceived(int channel) { andrewm@42: // int ch = keyboard_.key(noteNumber_)->midiChannel(); andrewm@42: // keyboard_.midiOutputController()->sendControlChange(0, ch, 73, 0); andrewm@0: }