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