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: