Mercurial > hg > touchkeys
diff Source/Mappings/KeyDivision/TouchkeyKeyDivisionMapping.cpp @ 0:3580ffe87dc8
First commit of TouchKeys public pre-release.
author | Andrew McPherson <andrewm@eecs.qmul.ac.uk> |
---|---|
date | Mon, 11 Nov 2013 18:19:35 +0000 |
parents | |
children | 78b9808a2c65 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Source/Mappings/KeyDivision/TouchkeyKeyDivisionMapping.cpp Mon Nov 11 18:19:35 2013 +0000 @@ -0,0 +1,266 @@ +/* + 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/>. + + ===================================================================== + + TouchkeyKeyDivisionMapping.cpp: per-note mapping for the split-key mapping + which triggers different actions or pitches depending on where the key + was struck. +*/ + +#include "TouchkeyKeyDivisionMapping.h" +#include "TouchkeyKeyDivisionMappingFactory.h" + +#define DEBUG_KEY_DIVISION_MAPPING + +const int TouchkeyKeyDivisionMapping::kDefaultNumberOfSegments = 2; +const timestamp_diff_type TouchkeyKeyDivisionMapping::kDefaultDetectionTimeout = milliseconds_to_timestamp(25.0); +const int TouchkeyKeyDivisionMapping::kDefaultDetectionParameter = kDetectionParameterYPosition; +const int TouchkeyKeyDivisionMapping::kDefaultRetriggerNumFrames = 2; + +// 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 +TouchkeyKeyDivisionMapping::TouchkeyKeyDivisionMapping(PianoKeyboard &keyboard, MappingFactory *factory, int noteNumber, Node<KeyTouchFrame>* touchBuffer, + Node<key_position>* positionBuffer, KeyPositionTracker* positionTracker) +: TouchkeyBaseMapping(keyboard, factory, noteNumber, touchBuffer, positionBuffer, positionTracker), +numberOfSegments_(kDefaultNumberOfSegments), candidateSegment_(-1), detectedSegment_(-1), defaultSegment_(0), +detectionParameter_(kDefaultDetectionParameter), retriggerable_(false), retriggerNumFrames_(kDefaultRetriggerNumFrames), +retriggerKeepsVelocity_(true), +midiNoteOnTimestamp_(missing_value<timestamp_type>::missing()), timeout_(kDefaultDetectionTimeout), +lastNumActiveTouches_(-1) +{ +} + +// Reset state back to defaults +void TouchkeyKeyDivisionMapping::reset() { + TouchkeyBaseMapping::reset(); + + candidateSegment_ = detectedSegment_ = -1; + midiNoteOnTimestamp_ = missing_value<timestamp_type>::missing(); +} + +// Resend all current parameters +void TouchkeyKeyDivisionMapping::resend() { + if(detectedSegment_ >= 0) + sendSegmentMessage(detectedSegment_, true); +} + +// Set the pitch bend values (in semitones) for each segment. These +// values are in relation to the pitch of this note +void TouchkeyKeyDivisionMapping::setSegmentPitchBends(const float *bendsInSemitones, int numBends) { + // Clear old values and refill the vector + segmentBends_.clear(); + for(int i = 0; i < numBends; i++) + segmentBends_.push_back(bendsInSemitones[i]); +} + +// This method receives data from the touch buffer or possibly the continuous key angle (not used here) +void TouchkeyKeyDivisionMapping::triggerReceived(TriggerSource* who, timestamp_type timestamp) { + if(who == touchBuffer_) { + // If we get here, a new touch frame has been received and there is no segment detected + // yet. We should come up with a candidate segment. If the MIDI note is on, activate this + // segment right away. Otherwise, save it for later so when the MIDI note begins, we have + // it ready to go. + if(!touchBuffer_->empty()) { + const KeyTouchFrame& frame = touchBuffer_->latest(); + + if(detectedSegment_ < 0) { + int candidateBasedOnYPosition = -1, candidateBasedOnNumberOfTouches = -1; + + // Find the first touch. TODO: eventually look for the largest touch + float yPosition = frame.locs[0]; + + // Calculate two possible segments based on touch location and based on + // number of touches. + candidateBasedOnYPosition = segmentForLocation(yPosition); + candidateBasedOnNumberOfTouches = segmentForNumTouches(frame.count); + + if(detectionParameter_ == kDetectionParameterYPosition) + candidateSegment_ = candidateBasedOnYPosition; + else if(detectionParameter_ == kDetectionParameterNumberOfTouches) + candidateSegment_ = candidateBasedOnNumberOfTouches; + else if(detectionParameter_ == kDetectionParameterYPositionAndNumberOfTouches) { + // Choose the maximum segment specified by the other two methods + candidateSegment_ = candidateBasedOnNumberOfTouches > candidateBasedOnYPosition ? candidateBasedOnNumberOfTouches : candidateBasedOnYPosition; + } + else // Shouldn't happen + candidateSegment_ = -1; + + if(noteIsOn_) { + detectedSegment_ = candidateSegment_; +#ifdef DEBUG_KEY_DIVISION_MAPPING + cout << "TouchkeyKeyDivisionMapping::triggerReceived(): detectedSegment_ = " << detectedSegment_ << endl; +#endif + sendSegmentMessage(detectedSegment_); + } + } + else if(retriggerable_ && + (lastNumActiveTouches_ == 1 && + frame.count >= 2) && noteIsOn_) { + // Here, there was one touch active before and now there are two. Look for the + // location of the most recently added touch, and determine whether it matches a + // segment different from the one we're in. If so, retrigger the MIDI note + // with a different pitch bend + + int newCandidate = segmentForLocation(locationOfNewestTouch(frame)); + +#ifdef DEBUG_KEY_DIVIOSION_MAPPING + cout << "TouchkeyKeyDivisionMapping: touch added with candidate segment " << newCandidate << " (current is " << detectedSegment_ << ")\n"; +#endif + if(newCandidate != detectedSegment_) { + // Set up a new segment to retrigger and tell the scheduler to insert the mapping + detectedSegment_ = newCandidate; + + // Find the keyboard segment, which gives us the output port + int outputPort = static_cast<TouchkeyKeyDivisionMappingFactory*>(factory_)->segment().outputPort(); + + // Send MIDI note-on on the same channel as previously + int ch = keyboard_.key(noteNumber_)->midiChannel(); + int vel = 64; + if(retriggerKeepsVelocity_) + vel = keyboard_.key(noteNumber_)->midiVelocity(); + keyboard_.midiOutputController()->sendNoteOn(outputPort, ch, noteNumber_, vel); + sendSegmentMessage(detectedSegment_); + } + } + + // Save the number of active touches for next time + lastNumActiveTouches_ = frame.count; + } + } +} + +// 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 TouchkeyKeyDivisionMapping::performMapping() { + timestamp_type currentTimestamp = keyboard_.schedulerCurrentTimestamp(); + + if(detectedSegment_ >= 0) { + // Found segment; no need to keep sending mapping callbacks + nextScheduledTimestamp_ = 0; + return 0; + } + + if(currentTimestamp - midiNoteOnTimestamp_ > timeout_) { + // Timeout occurred. Activate default segment +#ifdef DEBUG_KEY_DIVISION_MAPPING + cout << "TouchkeyKeyDivisionMapping: timeout\n"; +#endif + detectedSegment_ = defaultSegment_; + sendSegmentMessage(detectedSegment_); + nextScheduledTimestamp_ = 0; + return 0; + } + + // Register for the next update by returning its timestamp + nextScheduledTimestamp_ = currentTimestamp + updateInterval_; + return nextScheduledTimestamp_; +} + +// MIDI note-on received. If we have a candidate segment, activate it as the actual segment +void TouchkeyKeyDivisionMapping::midiNoteOnReceived(int channel, int velocity) { + midiNoteOnTimestamp_ = keyboard_.schedulerCurrentTimestamp(); + + if(detectedSegment_ < 0) { +#ifdef DEBUG_KEY_DIVISION_MAPPING + cout << "TouchkeyKeyDivisionMapping::midiNoteOnReceived(): candidateSegment_ = " << candidateSegment_ << endl; +#endif + detectedSegment_ = candidateSegment_; + if(detectedSegment_ >= 0) { + sendSegmentMessage(detectedSegment_); + } + } +} + +// MIDI note-off received. Reset back to the detecting state so we can assign the next note to a segment +void TouchkeyKeyDivisionMapping::midiNoteOffReceived(int channel) { + detectedSegment_ = candidateSegment_ = -1; +} + +void TouchkeyKeyDivisionMapping::sendSegmentMessage(int segment, bool force) { + if(force || !suspended_) { + keyboard_.sendMessage("/touchkeys/keysegment", "ii", noteNumber_, segment, LO_ARGS_END); + if(segment < segmentBends_.size() && segment >= 0) { +#ifdef DEBUG_KEY_DIVISION_MAPPING + cout << "TouchkeyKeyDivisionMapping::sendSegmentMessage(): pitch bend = " << segmentBends_[segment] << endl; +#endif + sendPitchBendMessage(segmentBends_[segment], force); + } + else { +#ifdef DEBUG_KEY_DIVISION_MAPPING + cout << "TouchkeyKeyDivisionMapping::sendSegmentMessage(): no bend for segment " << segment << endl; +#endif + } + } + else { +#ifdef DEBUG_KEY_DIVISION_MAPPING + cout << "TouchkeyKeyDivisionMapping::sendSegmentMessage(): suspended, not sending segment " << segment << endl; +#endif + } +} + +// Send the pitch bend message of a given number of a semitones. Send by OSC, +// which can be mapped to MIDI CC externally +void TouchkeyKeyDivisionMapping::sendPitchBendMessage(float pitchBendSemitones, bool force) { + if(force || !suspended_) + keyboard_.sendMessage(controlName_.c_str(), "if", noteNumber_, pitchBendSemitones, LO_ARGS_END); +} + +// Find the segment corresponding to a (Y) touch location +int TouchkeyKeyDivisionMapping::segmentForLocation(float location) { + // Divide the key into evenly-spaced regions, and identify and candidate segment. + // Since the location can go up to 1.0, make sure the top value doesn't overflow + // the number of segments + int segment = floorf(location * (float)numberOfSegments_); + if(segment >= numberOfSegments_) + segment = numberOfSegments_ - 1; + return segment; +} + +// Find the segment corresponding to a number of touches +int TouchkeyKeyDivisionMapping::segmentForNumTouches(int numTouches) { + // Check the number of touches, which could divide the note into as many + // as three segments. + if(numTouches <= 0) + return -1; + + int segment = numTouches - 1; + if(segment >= numberOfSegments_) + segment = numberOfSegments_ - 1; + return segment; +} + +// Return the location of the most recently added touch (indicated by the highest ID) +float TouchkeyKeyDivisionMapping::locationOfNewestTouch(KeyTouchFrame const& frame) { + if(frame.count == 0) + return -1.0; + + // Go through the active touches and find the one with the highest id + int highestId = -1; + float location = -1.0; + for(int i = 0; i < frame.count; i++) { + if(frame.ids[i] > highestId) { + highestId = frame.ids[i]; + location = frame.locs[i]; + } + } + + return location; +} \ No newline at end of file