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: TouchkeyVibratoMapping.cpp: per-note mapping for the vibrato mapping class, andrewm@0: which creates vibrato through side-to-side motion of the finger on the andrewm@0: key surface. andrewm@0: */ andrewm@0: andrewm@0: #include "TouchkeyVibratoMapping.h" andrewm@0: #include "../MappingScheduler.h" andrewm@0: #include andrewm@0: #include andrewm@0: #include andrewm@0: andrewm@0: #undef DEBUG_TOUCHKEY_VIBRATO_MAPPING andrewm@0: andrewm@0: // Class constants andrewm@0: const int TouchkeyVibratoMapping::kDefaultMIDIChannel = 0; andrewm@0: const int TouchkeyVibratoMapping::kDefaultFilterBufferLength = 30; andrewm@0: andrewm@0: const float TouchkeyVibratoMapping::kDefaultVibratoThresholdX = 0.05; andrewm@0: const float TouchkeyVibratoMapping::kDefaultVibratoRatioX = 0.3; andrewm@0: const float TouchkeyVibratoMapping::kDefaultVibratoThresholdY = 0.02; andrewm@0: const float TouchkeyVibratoMapping::kDefaultVibratoRatioY = 0.8; andrewm@0: const timestamp_diff_type TouchkeyVibratoMapping::kDefaultVibratoTimeout = microseconds_to_timestamp(400000); // 0.4s andrewm@0: const float TouchkeyVibratoMapping::kDefaultVibratoPrescaler = 2.0; andrewm@0: const float TouchkeyVibratoMapping::kDefaultVibratoRangeSemitones = 1.25; andrewm@0: andrewm@0: const timestamp_diff_type TouchkeyVibratoMapping::kZeroCrossingMinimumTime = microseconds_to_timestamp(50000); // 50ms andrewm@0: const timestamp_diff_type TouchkeyVibratoMapping::kMinimumOnsetTime = microseconds_to_timestamp(30000); // 30ms andrewm@0: const timestamp_diff_type TouchkeyVibratoMapping::kMaximumOnsetTime = microseconds_to_timestamp(300000); // 300ms andrewm@0: const timestamp_diff_type TouchkeyVibratoMapping::kMinimumReleaseTime = microseconds_to_timestamp(30000); // 30ms andrewm@0: const timestamp_diff_type TouchkeyVibratoMapping::kMaximumReleaseTime = microseconds_to_timestamp(300000); // 300ms andrewm@0: andrewm@0: const float TouchkeyVibratoMapping::kWhiteKeySingleAxisThreshold = (7.0 / 19.0); 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: andrewm@0: TouchkeyVibratoMapping::TouchkeyVibratoMapping(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: vibratoState_(kStateInactive), andrewm@0: rampBeginTime_(missing_value::missing()), andrewm@0: rampScaleValue_(0), andrewm@0: rampLength_(0), andrewm@0: lastCalculatedRampValue_(0), andrewm@0: onsetThresholdX_(kDefaultVibratoThresholdX), onsetThresholdY_(kDefaultVibratoThresholdY), andrewm@0: onsetRatioX_(kDefaultVibratoRatioX), onsetRatioY_(kDefaultVibratoRatioY), andrewm@0: onsetTimeout_(kDefaultVibratoTimeout), andrewm@0: onsetLocationX_(missing_value::missing()), andrewm@0: onsetLocationY_(missing_value::missing()), andrewm@0: lastX_(missing_value::missing()), lastY_(missing_value::missing()), andrewm@0: idOfCurrentTouch_(-1), andrewm@0: lastTimestamp_(missing_value::missing()), andrewm@0: lastProcessedIndex_(0), andrewm@0: lastZeroCrossingTimestamp_(missing_value::missing()), andrewm@0: lastZeroCrossingInterval_(0), andrewm@0: lastSampleWasPositive_(false), andrewm@0: foundFirstExtremum_(false), andrewm@0: firstExtremumX_(0), firstExtremumY_(0), andrewm@0: firstExtremumTimestamp_(missing_value::missing()), andrewm@0: lastExtremumTimestamp_(missing_value::missing()), andrewm@0: //vibratoType_(kDefaultVibratoType), andrewm@0: vibratoPrescaler_(kDefaultVibratoPrescaler), andrewm@0: vibratoRangeSemitones_(kDefaultVibratoRangeSemitones), andrewm@0: lastPitchBendSemitones_(0), andrewm@0: rawDistance_(kDefaultFilterBufferLength), andrewm@0: filteredDistance_(kDefaultFilterBufferLength, rawDistance_) andrewm@0: { andrewm@0: // Initialize the filter coefficients for filtered key velocity (used for vibrato detection) andrewm@0: std::vector bCoeffs, aCoeffs; andrewm@0: designSecondOrderBandpass(bCoeffs, aCoeffs, 9.0, 0.707, 200.0); andrewm@0: std::vector bCf(bCoeffs.begin(), bCoeffs.end()), aCf(aCoeffs.begin(), aCoeffs.end()); andrewm@0: filteredDistance_.setCoefficients(bCf, aCf); andrewm@0: filteredDistance_.setAutoCalculate(true); andrewm@0: andrewm@0: //setOscController(&keyboard_); andrewm@0: resetDetectionState(); andrewm@0: } andrewm@0: andrewm@0: TouchkeyVibratoMapping::~TouchkeyVibratoMapping() { andrewm@0: } andrewm@0: andrewm@0: // Turn off mapping of data. Remove our callback from the scheduler andrewm@0: void TouchkeyVibratoMapping::disengage(bool shouldDelete) { andrewm@0: sendVibratoMessage(0.0); andrewm@0: TouchkeyBaseMapping::disengage(shouldDelete); andrewm@0: } andrewm@0: andrewm@0: // Reset state back to defaults andrewm@0: void TouchkeyVibratoMapping::reset() { andrewm@0: TouchkeyBaseMapping::reset(); andrewm@0: sendVibratoMessage(0.0); andrewm@0: resetDetectionState(); andrewm@0: } andrewm@0: andrewm@0: // Resend all current parameters andrewm@0: void TouchkeyVibratoMapping::resend() { andrewm@0: sendVibratoMessage(lastPitchBendSemitones_, true); andrewm@0: } andrewm@0: andrewm@0: // Set the range of vibrato andrewm@0: void TouchkeyVibratoMapping::setRange(float rangeSemitones) { andrewm@0: vibratoRangeSemitones_ = rangeSemitones; andrewm@0: } andrewm@0: andrewm@0: // Set the vibrato prescaler andrewm@0: void TouchkeyVibratoMapping::setPrescaler(float prescaler) { andrewm@0: vibratoPrescaler_ = prescaler; andrewm@0: } andrewm@0: andrewm@0: // Set the vibrato detection thresholds andrewm@0: void TouchkeyVibratoMapping::setThresholds(float thresholdX, float thresholdY, float ratioX, float ratioY) { andrewm@0: onsetThresholdX_ = thresholdX; andrewm@0: onsetThresholdY_ = thresholdY; andrewm@0: onsetRatioX_ = ratioX; andrewm@0: onsetRatioY_ = ratioY; andrewm@0: } andrewm@0: andrewm@0: // Set the timeout for vibrato detection andrewm@0: void TouchkeyVibratoMapping::setTimeout(timestamp_diff_type timeout) { andrewm@0: onsetTimeout_ = timeout; andrewm@0: } andrewm@0: andrewm@0: // Trigger method. This receives updates from the TouchKey data or from state changes in andrewm@0: // the continuous key position (KeyPositionTracker). It will potentially change the scheduled andrewm@0: // behavior of future mapping calls, but the actual OSC messages should be transmitted in a different andrewm@0: // thread. andrewm@0: void TouchkeyVibratoMapping::triggerReceived(TriggerSource* who, timestamp_type timestamp) { andrewm@0: if(who == 0) andrewm@0: return; andrewm@0: andrewm@0: if(who == touchBuffer_) { andrewm@0: if(!touchBuffer_->empty()) { andrewm@0: // New touch data is available. Find the distance from the onset location. andrewm@0: KeyTouchFrame frame = touchBuffer_->latest(); andrewm@0: lastTimestamp_ = timestamp; andrewm@0: andrewm@0: if(frame.count == 0) { andrewm@0: // No touches. Last values are "missing", and we're not tracking any andrewm@0: // particular touch ID andrewm@0: lastX_ = lastY_ = missing_value::missing(); andrewm@0: idOfCurrentTouch_ = -1; andrewm@0: #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING andrewm@0: std::cout << "Touch off\n"; andrewm@0: #endif andrewm@0: } andrewm@0: else { andrewm@0: // At least one touch. Check if we are already tracking an ID and, if so, andrewm@0: // use its coordinates. Otherwise grab the lowest current ID. andrewm@0: andrewm@0: bool foundCurrentTouch = false; andrewm@0: andrewm@0: if(idOfCurrentTouch_ >= 0) { andrewm@0: for(int i = 0; i < frame.count; i++) { andrewm@0: if(frame.ids[i] == idOfCurrentTouch_) { andrewm@0: lastY_ = frame.locs[i]; andrewm@0: if(frame.locH < 0 || (keyIsWhite() && lastY_ > kWhiteKeySingleAxisThreshold)) andrewm@0: lastX_ = missing_value::missing(); andrewm@0: else andrewm@0: lastX_ = frame.locH; andrewm@0: foundCurrentTouch = true; andrewm@0: break; andrewm@0: } andrewm@0: } andrewm@0: } andrewm@0: andrewm@0: if(!foundCurrentTouch) { andrewm@0: // Assign a new touch to be tracked andrewm@0: int lowestRemainingId = INT_MAX; andrewm@0: int lowestIndex = 0; andrewm@0: andrewm@0: for(int i = 0; i < frame.count; i++) { andrewm@0: if(frame.ids[i] < lowestRemainingId) { andrewm@0: lowestRemainingId = frame.ids[i]; andrewm@0: lowestIndex = i; andrewm@0: } andrewm@0: } andrewm@0: andrewm@0: idOfCurrentTouch_ = lowestRemainingId; andrewm@0: lastY_ = frame.locs[lowestIndex]; andrewm@0: if(frame.locH < 0 || (keyIsWhite() && lastY_ > kWhiteKeySingleAxisThreshold)) andrewm@0: lastX_ = missing_value::missing(); andrewm@0: else andrewm@0: lastX_ = frame.locH; andrewm@0: #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING andrewm@0: std::cout << "Previous touch stopped; now ID " << idOfCurrentTouch_ << " at (" << lastX_ << ", " << lastY_ << ")\n"; andrewm@0: #endif andrewm@0: } andrewm@0: andrewm@0: // Now we have an X and (maybe) a Y coordinate for the most recent touch. andrewm@0: // Check whether we have an initial location (if the note is active). andrewm@0: if(noteIsOn_) { andrewm@0: //ScopedLock sl(distanceAccessMutex_); andrewm@0: andrewm@0: if(missing_value::isMissing(onsetLocationY_) || andrewm@0: !foundCurrentTouch) { andrewm@0: // Note is on but touch hasn't yet arrived --> this touch becomes andrewm@0: // our onset location. Alternatively, the current touch is a different andrewm@0: // ID from the previous one. andrewm@0: onsetLocationY_ = lastY_; andrewm@0: onsetLocationX_ = lastX_; andrewm@0: andrewm@0: // Clear buffer and start with 0 distance for this point andrewm@0: clearBuffers(); andrewm@0: andrewm@0: #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING andrewm@0: std::cout << "Starting at (" << onsetLocationX_ << ", " << onsetLocationY_ << ")\n"; andrewm@0: #endif andrewm@0: } andrewm@0: else { andrewm@0: float distance = 0.0; andrewm@0: andrewm@0: // Note is on and a start location exists. Calculate distance between andrewm@0: // start location and the current point. andrewm@0: andrewm@0: if(missing_value::isMissing(onsetLocationX_) && andrewm@0: !missing_value::isMissing(lastX_)) { andrewm@0: // No X location indicated for onset but we have one now. andrewm@0: // Update the onset X location. andrewm@0: onsetLocationX_ = lastX_; andrewm@0: #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING andrewm@0: std::cout << "Found first X location at " << onsetLocationX_ << std::endl; andrewm@0: #endif andrewm@0: } andrewm@0: andrewm@0: andrewm@0: if(missing_value::isMissing(lastX_) || andrewm@0: missing_value::isMissing(onsetLocationX_)) { andrewm@0: // If no X value is available on the current touch, calculate the distance andrewm@0: // based on Y only. TODO: check whether we should do this by keeping the andrewm@0: // last X value we recorded. andrewm@0: andrewm@0: //distance = fabsf(lastY_ - onsetLocationY_); andrewm@0: distance = lastY_ - onsetLocationY_; andrewm@53: //distance = 0; // TESTING andrewm@0: } andrewm@0: else { andrewm@0: // Euclidean distance between points andrewm@0: //distance = sqrtf((lastY_ - onsetLocationY_) * (lastY_ - onsetLocationY_) + andrewm@0: // (lastX_ - onsetLocationX_) * (lastX_ - onsetLocationX_)); andrewm@0: distance = lastX_ - onsetLocationX_; andrewm@0: } andrewm@0: andrewm@0: // Insert raw distance into the buffer. Bandpass filter calculates the next andrewm@0: // sample automatically. The rest of the processing takes place in the dedicated andrewm@0: // thread so as not to slow down commmunication with the hardware. andrewm@0: rawDistance_.insert(distance, timestamp); andrewm@0: andrewm@0: // Move the current scheduled event up to the present time. andrewm@0: // FIXME: this may be more inefficient than just doing everything in the current thread! andrewm@0: #ifdef NEW_MAPPING_SCHEDULER andrewm@0: keyboard_.mappingScheduler().scheduleNow(this); andrewm@0: #else andrewm@0: keyboard_.unscheduleEvent(this); andrewm@0: keyboard_.scheduleEvent(this, mappingAction_, keyboard_.schedulerCurrentTimestamp()); andrewm@0: #endif andrewm@0: andrewm@0: //std::cout << "Raw distance " << distance << " filtered " << filteredDistance_.latest() << std::endl; andrewm@0: } andrewm@0: } 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 TouchkeyVibratoMapping::performMapping() { andrewm@0: //ScopedLock sl(distanceAccessMutex_); andrewm@0: andrewm@0: timestamp_type currentTimestamp = keyboard_.schedulerCurrentTimestamp(); andrewm@0: bool newSamplePresent = false; andrewm@0: andrewm@0: // Go through the filtered distance samples that are remaining to process. andrewm@0: if(lastProcessedIndex_ < filteredDistance_.beginIndex() + 1) { andrewm@0: // Fell off the beginning of the position buffer. Skip to the samples we have andrewm@0: // (shouldn't happen except in cases of exceptional system load, and not too andrewm@0: // consequential if it does happen). andrewm@0: lastProcessedIndex_ = filteredDistance_.beginIndex() + 1; andrewm@0: } andrewm@0: andrewm@0: while(lastProcessedIndex_ < filteredDistance_.endIndex()) { andrewm@0: float distance = filteredDistance_[lastProcessedIndex_]; andrewm@0: timestamp_type timestamp = filteredDistance_.timestampAt(lastProcessedIndex_); andrewm@0: newSamplePresent = true; andrewm@0: andrewm@0: if((distance > 0 && !lastSampleWasPositive_) || andrewm@0: (distance < 0 && lastSampleWasPositive_)) { andrewm@0: // Found a zero crossing: save it if we're active or have at least found the andrewm@0: // first extremum andrewm@0: if(!missing_value::isMissing(lastZeroCrossingTimestamp_) && andrewm@0: (timestamp - lastZeroCrossingTimestamp_ > kZeroCrossingMinimumTime)) { andrewm@0: if(vibratoState_ == kStateActive || vibratoState_ == kStateSwitchingOn || andrewm@0: foundFirstExtremum_) { andrewm@0: lastZeroCrossingInterval_ = timestamp - lastZeroCrossingTimestamp_; andrewm@0: #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING andrewm@0: std::cout << "Zero crossing interval " << lastZeroCrossingInterval_ << std::endl; andrewm@0: #endif andrewm@0: } andrewm@0: } andrewm@0: lastZeroCrossingTimestamp_ = timestamp; andrewm@0: } andrewm@0: lastSampleWasPositive_ = (distance > 0); andrewm@0: andrewm@0: // If not currently engaged, check for the pattern of side-to-side motion that andrewm@0: // begins a vibrato gesture. andrewm@0: if(vibratoState_ == kStateInactive || vibratoState_ == kStateSwitchingOff) { andrewm@0: if(foundFirstExtremum_) { andrewm@0: // Already found first extremum. Look for second extremum in the opposite andrewm@0: // direction of the given ratio from the original. andrewm@0: if((firstExtremumX_ > 0 && distance < 0) || andrewm@0: (firstExtremumX_ < 0 && distance > 0)) { andrewm@0: if(fabsf(distance) >= fabsf(firstExtremumX_) * onsetRatioX_) { andrewm@0: #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING andrewm@0: std::cout << "Found second extremum at " << distance << ", TS " << timestamp << std::endl; andrewm@0: #endif andrewm@0: changeStateSwitchingOn(timestamp); andrewm@0: } andrewm@0: } andrewm@0: else if(timestamp - lastExtremumTimestamp_ > onsetTimeout_) { andrewm@0: #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING andrewm@0: std::cout << "Onset timeout at " << timestamp << endl; andrewm@0: #endif andrewm@0: resetDetectionState(); andrewm@0: } andrewm@0: } andrewm@0: else { andrewm@0: if(fabsf(distance) >= onsetThresholdX_) { andrewm@0: // TODO: differentiate X/Y here andrewm@0: if(missing_value::isMissing(firstExtremumX_) || andrewm@0: fabsf(distance) > fabsf(firstExtremumX_)) { andrewm@0: firstExtremumX_ = distance; andrewm@0: lastExtremumTimestamp_ = timestamp; andrewm@0: #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING andrewm@0: std::cout << "First extremum candidate at " << firstExtremumX_ << ", TS " << lastExtremumTimestamp_ << std::endl; andrewm@0: #endif andrewm@0: } andrewm@0: } andrewm@0: else if(!missing_value::isMissing(firstExtremumX_) && andrewm@0: fabsf(firstExtremumX_) > onsetThresholdX_) { andrewm@0: // We must have found the first extremum since its maximum value is andrewm@0: // above the threshold, and we must have moved away from it since we are andrewm@0: // now below the threshold. Next step will be to look for extremum in andrewm@0: // opposite direction. Save the timestamp of this location in case andrewm@0: // another extremum is found later. andrewm@0: firstExtremumTimestamp_ = lastExtremumTimestamp_; andrewm@0: foundFirstExtremum_ = true; andrewm@0: #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING andrewm@0: std::cout << "Found first extremum at " << firstExtremumX_ << ", TS " << lastExtremumTimestamp_ << std::endl; andrewm@0: #endif andrewm@0: } andrewm@0: } andrewm@0: } andrewm@0: else { andrewm@0: // Currently engaged. Look for timeout, defined as the finger staying below the lower (ratio-adjusted) threshold. andrewm@0: if(fabsf(distance) >= onsetThresholdX_ * onsetRatioX_) andrewm@0: lastExtremumTimestamp_ = timestamp; andrewm@0: if(timestamp - lastExtremumTimestamp_ > onsetTimeout_) { andrewm@0: #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING andrewm@0: std::cout << "Vibrato timeout at " << timestamp << " (last was " << lastExtremumTimestamp_ << ")" << endl; andrewm@0: #endif andrewm@0: changeStateSwitchingOff(timestamp); andrewm@0: } andrewm@0: } andrewm@0: andrewm@0: lastProcessedIndex_++; andrewm@0: } andrewm@0: andrewm@0: // Having processed every sample individually for detection, send a pitch bend message based on the most andrewm@0: // recent one (no sense in sending multiple pitch bend messages simultaneously). andrewm@0: if(newSamplePresent && vibratoState_ != kStateInactive) { andrewm@0: float distance = filteredDistance_.latest(); andrewm@0: float scale = 1.0; andrewm@0: andrewm@0: if(vibratoState_ == kStateSwitchingOn) { andrewm@0: // Switching on state gradually scales vibrato depth from 0 to andrewm@0: // its final value over a specified switch-on time. andrewm@0: if(rampLength_ <= 0 || (currentTimestamp - rampBeginTime_ >= rampLength_)) { andrewm@0: scale = 1.0; andrewm@0: changeStateActive(currentTimestamp); andrewm@0: #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING andrewm@0: std::cout << "Vibrato switch on finished, going to Active\n"; andrewm@0: #endif andrewm@0: } andrewm@0: else { andrewm@0: lastCalculatedRampValue_ = rampScaleValue_ * (float)(currentTimestamp - rampBeginTime_)/(float)rampLength_; andrewm@0: scale = lastCalculatedRampValue_; andrewm@0: //std::cout << "Vibrato scale " << scale << ", TS " << currentTimestamp - rampBeginTime_ << std::endl; andrewm@0: } andrewm@0: } andrewm@0: else if(vibratoState_ == kStateSwitchingOff) { andrewm@0: // Switching off state gradually scales vibrato depth from full andrewm@0: // value to 0 over a specified switch-off time. andrewm@0: if(rampLength_ <= 0 || (currentTimestamp - rampBeginTime_ >= rampLength_)) { andrewm@0: scale = 0.0; andrewm@0: changeStateInactive(currentTimestamp); andrewm@0: #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING andrewm@0: std::cout << "Vibrato switch off finished, going to Inactive\n"; andrewm@0: #endif andrewm@0: } andrewm@0: else { andrewm@0: lastCalculatedRampValue_ = rampScaleValue_ * (1.0 - (float)(currentTimestamp - rampBeginTime_)/(float)rampLength_); andrewm@0: scale = lastCalculatedRampValue_; andrewm@0: //std::cout << "Vibrato scale " << scale << ", TS " << currentTimestamp - rampBeginTime_ << std::endl; andrewm@0: } andrewm@0: } andrewm@0: andrewm@0: // Calculate pitch bend based on current distance, with a non-linear scaling to accentuate andrewm@0: // smaller motions. andrewm@0: float pitchBendSemitones = vibratoRangeSemitones_ * tanhf(vibratoPrescaler_ * scale * distance); andrewm@0: andrewm@0: sendVibratoMessage(pitchBendSemitones); andrewm@0: lastPitchBendSemitones_ = pitchBendSemitones; andrewm@0: } andrewm@0: andrewm@0: // We may have arrived here without a new touch, just based on timing. Check for timeouts and process andrewm@0: // any release in progress. andrewm@0: if(!newSamplePresent) { andrewm@0: if(vibratoState_ == kStateSwitchingOff) { andrewm@0: // No new information in the distance buffer, but we do need to gradually reduce the pitch bend to zero andrewm@0: if(rampLength_ <= 0 || (currentTimestamp - rampBeginTime_ >= rampLength_)) { andrewm@0: sendVibratoMessage(0.0); andrewm@0: lastPitchBendSemitones_ = 0; andrewm@0: changeStateInactive(currentTimestamp); andrewm@0: #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING andrewm@0: std::cout << "Vibrato switch off finished, going to Inactive\n"; andrewm@0: #endif andrewm@0: } andrewm@0: else { andrewm@0: // Still in the middle of the ramp. Calculate its current value based on the last one andrewm@0: // that actually had a touch data point (lastPitchBendSemitones_). andrewm@0: andrewm@0: lastCalculatedRampValue_ = rampScaleValue_ * (1.0 - (float)(currentTimestamp - rampBeginTime_)/(float)rampLength_); andrewm@0: float pitchBendSemitones = lastPitchBendSemitones_ * lastCalculatedRampValue_; andrewm@0: andrewm@0: sendVibratoMessage(pitchBendSemitones); andrewm@0: } andrewm@0: } andrewm@0: else if(vibratoState_ != kStateInactive) { andrewm@0: // Might still be active but with no data coming in. We need to look for a timeout here too. andrewm@0: if(currentTimestamp - lastExtremumTimestamp_ > onsetTimeout_) { andrewm@0: #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING andrewm@0: std::cout << "Vibrato timeout at " << currentTimestamp << " (2; last was " << lastExtremumTimestamp_ << ")" << endl; andrewm@0: #endif andrewm@0: changeStateSwitchingOff(currentTimestamp); andrewm@0: } andrewm@0: } andrewm@0: } andrewm@0: andrewm@0: // Register for the next update by returning its timestamp andrewm@0: nextScheduledTimestamp_ = currentTimestamp + updateInterval_; andrewm@0: return nextScheduledTimestamp_; andrewm@0: } andrewm@0: andrewm@0: // MIDI note-on message received andrewm@0: void TouchkeyVibratoMapping::midiNoteOnReceived(int channel, int velocity) { andrewm@0: // MIDI note has gone on. Set the starting location to be most recent andrewm@0: // location. It's possible there has been no touch data before this, andrewm@0: // in which case lastX and lastY will hold missing values. andrewm@0: onsetLocationX_ = lastX_; andrewm@0: onsetLocationY_ = lastY_; andrewm@0: if(!missing_value::isMissing(onsetLocationY_)) { andrewm@0: // Already have touch data. Clear the buffer here. andrewm@0: // Clear buffer and start with 0 distance for this point andrewm@0: clearBuffers(); andrewm@0: andrewm@0: #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING andrewm@0: std::cout << "MIDI on: starting at (" << onsetLocationX_ << ", " << onsetLocationY_ << ")\n"; andrewm@0: #endif andrewm@0: } andrewm@0: else { andrewm@0: #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING andrewm@0: std::cout << "MIDI on but no touch\n"; andrewm@0: #endif andrewm@0: } andrewm@0: } andrewm@0: andrewm@0: // MIDI note-off message received andrewm@0: void TouchkeyVibratoMapping::midiNoteOffReceived(int channel) { andrewm@0: if(vibratoState_ == kStateActive || vibratoState_ == kStateSwitchingOn) { andrewm@0: changeStateSwitchingOff(keyboard_.schedulerCurrentTimestamp()); andrewm@0: } andrewm@0: } andrewm@0: andrewm@0: // Internal state-change methods, which keep the state variables in sync andrewm@0: void TouchkeyVibratoMapping::changeStateSwitchingOn(timestamp_type timestamp) { andrewm@0: // Go to SwitchingOn state, which brings the vibrato value gradually up to full amplitude andrewm@0: andrewm@0: // TODO: need to start from a non-zero value if SwitchingOff andrewm@0: rampScaleValue_ = 1.0; andrewm@0: rampBeginTime_ = timestamp; andrewm@0: rampLength_ = 0.0; andrewm@0: // Interval between peak and zero crossing will be a quarter of a cycle. andrewm@0: // From this, figure out how much longer we have to go to get to the next andrewm@0: // peak if the rate remains the same. andrewm@0: if(!missing_value::isMissing(lastZeroCrossingTimestamp_) && andrewm@0: !missing_value::isMissing(firstExtremumTimestamp_)) { andrewm@0: timestamp_type estimatedPeakTimestamp = lastZeroCrossingTimestamp_ + (lastZeroCrossingTimestamp_ - firstExtremumTimestamp_); andrewm@0: rampLength_ = estimatedPeakTimestamp - timestamp; andrewm@0: if(rampLength_ < kMinimumOnsetTime) andrewm@0: rampLength_ = kMinimumOnsetTime; andrewm@0: if(rampLength_ > kMaximumOnsetTime) andrewm@0: rampLength_ = kMaximumOnsetTime; andrewm@0: #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING andrewm@0: std::cout << "Switching on with ramp length " << rampLength_ << " (peak " << firstExtremumTimestamp_ << ", zero " << lastZeroCrossingTimestamp_ << ")" << std::endl; andrewm@0: #endif andrewm@0: } andrewm@0: andrewm@0: vibratoState_ = kStateSwitchingOn; andrewm@0: } andrewm@0: andrewm@0: void TouchkeyVibratoMapping::changeStateSwitchingOff(timestamp_type timestamp) { andrewm@0: // Go to SwitchingOff state, which brings the vibrato value gradually down to 0 andrewm@0: andrewm@0: if(vibratoState_ == kStateSwitchingOn) { andrewm@0: // Might already be in the midst of a ramp up. Start from its current value andrewm@0: rampScaleValue_ = lastCalculatedRampValue_; andrewm@0: } andrewm@0: else andrewm@0: rampScaleValue_ = 1.0; andrewm@0: andrewm@0: rampBeginTime_ = timestamp; andrewm@0: rampLength_ = lastZeroCrossingInterval_; andrewm@0: if(rampLength_ < kMinimumReleaseTime) andrewm@0: rampLength_ = kMinimumReleaseTime; andrewm@0: if(rampLength_ > kMaximumReleaseTime) andrewm@0: rampLength_ = kMaximumReleaseTime; andrewm@0: andrewm@0: #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING andrewm@0: std::cout << "Switching off with ramp length " << rampLength_ << std::endl; andrewm@0: #endif andrewm@0: andrewm@0: resetDetectionState(); andrewm@0: vibratoState_ = kStateSwitchingOff; andrewm@0: } andrewm@0: andrewm@0: void TouchkeyVibratoMapping::changeStateActive(timestamp_type timestamp) { andrewm@0: vibratoState_ = kStateActive; andrewm@0: } andrewm@0: andrewm@0: void TouchkeyVibratoMapping::changeStateInactive(timestamp_type timestamp) { andrewm@0: vibratoState_ = kStateInactive; andrewm@0: } andrewm@0: andrewm@0: // Reset variables involved in detecting a vibrato gesture andrewm@0: void TouchkeyVibratoMapping::resetDetectionState() { andrewm@0: foundFirstExtremum_ = false; andrewm@0: firstExtremumX_ = firstExtremumY_ = 0.0; andrewm@0: lastExtremumTimestamp_ = firstExtremumTimestamp_ = lastZeroCrossingTimestamp_ = missing_value::missing(); andrewm@0: } andrewm@0: andrewm@0: // Clear the buffers that hold distance measurements andrewm@0: void TouchkeyVibratoMapping::clearBuffers() { andrewm@0: rawDistance_.clear(); andrewm@0: filteredDistance_.clear(); andrewm@0: rawDistance_.insert(0.0, lastTimestamp_); andrewm@0: lastProcessedIndex_ = 0; andrewm@0: } andrewm@0: andrewm@0: bool TouchkeyVibratoMapping::keyIsWhite() { andrewm@0: int modNoteNumber = noteNumber_ % 12; andrewm@0: if(modNoteNumber == 1 || andrewm@0: modNoteNumber == 3 || andrewm@0: modNoteNumber == 6 || andrewm@0: modNoteNumber == 8 || andrewm@0: modNoteNumber == 10) andrewm@0: return false; andrewm@0: return true; andrewm@0: } andrewm@0: andrewm@0: // Send the vibrato message of a given number of a semitones. Send by OSC, andrewm@0: // which can be mapped to MIDI CC externally andrewm@0: void TouchkeyVibratoMapping::sendVibratoMessage(float pitchBendSemitones, bool force) { andrewm@0: if(force || !suspended_) { andrewm@0: //if(vibratoType_ == kVibratoTypePitchBend) andrewm@0: // keyboard_.sendMessage("/touchkeys/vibrato", "if", noteNumber_, pitchBendSemitones, LO_ARGS_END); andrewm@0: //else if(vibratoType_ == kVibratoTypeAmplitude) andrewm@0: keyboard_.sendMessage(controlName_.c_str(), "if", noteNumber_, pitchBendSemitones, LO_ARGS_END); andrewm@0: // Otherwise, if unknown type, ignore. andrewm@0: } andrewm@0: } andrewm@0: