andrewm@0
|
1 /*
|
andrewm@0
|
2 TouchKeys: multi-touch musical keyboard control software
|
andrewm@0
|
3 Copyright (c) 2013 Andrew McPherson
|
andrewm@0
|
4
|
andrewm@0
|
5 This program is free software: you can redistribute it and/or modify
|
andrewm@0
|
6 it under the terms of the GNU General Public License as published by
|
andrewm@0
|
7 the Free Software Foundation, either version 3 of the License, or
|
andrewm@0
|
8 (at your option) any later version.
|
andrewm@0
|
9
|
andrewm@0
|
10 This program is distributed in the hope that it will be useful,
|
andrewm@0
|
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
|
andrewm@0
|
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
andrewm@0
|
13 GNU General Public License for more details.
|
andrewm@0
|
14
|
andrewm@0
|
15 You should have received a copy of the GNU General Public License
|
andrewm@0
|
16 along with this program. If not, see <http://www.gnu.org/licenses/>.
|
andrewm@0
|
17
|
andrewm@0
|
18 =====================================================================
|
andrewm@0
|
19
|
andrewm@0
|
20 TouchkeyVibratoMapping.cpp: per-note mapping for the vibrato mapping class,
|
andrewm@0
|
21 which creates vibrato through side-to-side motion of the finger on the
|
andrewm@0
|
22 key surface.
|
andrewm@0
|
23 */
|
andrewm@0
|
24
|
andrewm@0
|
25 #include "TouchkeyVibratoMapping.h"
|
andrewm@0
|
26 #include "../MappingScheduler.h"
|
andrewm@0
|
27 #include <vector>
|
andrewm@0
|
28 #include <climits>
|
andrewm@0
|
29 #include <cmath>
|
andrewm@0
|
30
|
andrewm@0
|
31 #undef DEBUG_TOUCHKEY_VIBRATO_MAPPING
|
andrewm@0
|
32
|
andrewm@0
|
33 // Class constants
|
andrewm@0
|
34 const int TouchkeyVibratoMapping::kDefaultMIDIChannel = 0;
|
andrewm@0
|
35 const int TouchkeyVibratoMapping::kDefaultFilterBufferLength = 30;
|
andrewm@0
|
36
|
andrewm@0
|
37 const float TouchkeyVibratoMapping::kDefaultVibratoThresholdX = 0.05;
|
andrewm@0
|
38 const float TouchkeyVibratoMapping::kDefaultVibratoRatioX = 0.3;
|
andrewm@0
|
39 const float TouchkeyVibratoMapping::kDefaultVibratoThresholdY = 0.02;
|
andrewm@0
|
40 const float TouchkeyVibratoMapping::kDefaultVibratoRatioY = 0.8;
|
andrewm@0
|
41 const timestamp_diff_type TouchkeyVibratoMapping::kDefaultVibratoTimeout = microseconds_to_timestamp(400000); // 0.4s
|
andrewm@0
|
42 const float TouchkeyVibratoMapping::kDefaultVibratoPrescaler = 2.0;
|
andrewm@0
|
43 const float TouchkeyVibratoMapping::kDefaultVibratoRangeSemitones = 1.25;
|
andrewm@0
|
44
|
andrewm@0
|
45 const timestamp_diff_type TouchkeyVibratoMapping::kZeroCrossingMinimumTime = microseconds_to_timestamp(50000); // 50ms
|
andrewm@0
|
46 const timestamp_diff_type TouchkeyVibratoMapping::kMinimumOnsetTime = microseconds_to_timestamp(30000); // 30ms
|
andrewm@0
|
47 const timestamp_diff_type TouchkeyVibratoMapping::kMaximumOnsetTime = microseconds_to_timestamp(300000); // 300ms
|
andrewm@0
|
48 const timestamp_diff_type TouchkeyVibratoMapping::kMinimumReleaseTime = microseconds_to_timestamp(30000); // 30ms
|
andrewm@0
|
49 const timestamp_diff_type TouchkeyVibratoMapping::kMaximumReleaseTime = microseconds_to_timestamp(300000); // 300ms
|
andrewm@0
|
50
|
andrewm@0
|
51 const float TouchkeyVibratoMapping::kWhiteKeySingleAxisThreshold = (7.0 / 19.0);
|
andrewm@0
|
52
|
andrewm@0
|
53 // Main constructor takes references/pointers from objects which keep track
|
andrewm@0
|
54 // of touch location, continuous key position and the state detected from that
|
andrewm@0
|
55 // position. The PianoKeyboard object is strictly required as it gives access to
|
andrewm@0
|
56 // Scheduler and OSC methods. The others are optional since any given system may
|
andrewm@0
|
57 // contain only one of continuous key position or touch sensitivity
|
andrewm@0
|
58
|
andrewm@0
|
59 TouchkeyVibratoMapping::TouchkeyVibratoMapping(PianoKeyboard &keyboard, MappingFactory *factory, int noteNumber, Node<KeyTouchFrame>* touchBuffer,
|
andrewm@0
|
60 Node<key_position>* positionBuffer, KeyPositionTracker* positionTracker)
|
andrewm@0
|
61 : TouchkeyBaseMapping(keyboard, factory, noteNumber, touchBuffer, positionBuffer, positionTracker),
|
andrewm@0
|
62 vibratoState_(kStateInactive),
|
andrewm@0
|
63 rampBeginTime_(missing_value<timestamp_type>::missing()),
|
andrewm@0
|
64 rampScaleValue_(0),
|
andrewm@0
|
65 rampLength_(0),
|
andrewm@0
|
66 lastCalculatedRampValue_(0),
|
andrewm@0
|
67 onsetThresholdX_(kDefaultVibratoThresholdX), onsetThresholdY_(kDefaultVibratoThresholdY),
|
andrewm@0
|
68 onsetRatioX_(kDefaultVibratoRatioX), onsetRatioY_(kDefaultVibratoRatioY),
|
andrewm@0
|
69 onsetTimeout_(kDefaultVibratoTimeout),
|
andrewm@0
|
70 onsetLocationX_(missing_value<float>::missing()),
|
andrewm@0
|
71 onsetLocationY_(missing_value<float>::missing()),
|
andrewm@0
|
72 lastX_(missing_value<float>::missing()), lastY_(missing_value<float>::missing()),
|
andrewm@0
|
73 idOfCurrentTouch_(-1),
|
andrewm@0
|
74 lastTimestamp_(missing_value<timestamp_type>::missing()),
|
andrewm@0
|
75 lastProcessedIndex_(0),
|
andrewm@0
|
76 lastZeroCrossingTimestamp_(missing_value<timestamp_type>::missing()),
|
andrewm@0
|
77 lastZeroCrossingInterval_(0),
|
andrewm@0
|
78 lastSampleWasPositive_(false),
|
andrewm@0
|
79 foundFirstExtremum_(false),
|
andrewm@0
|
80 firstExtremumX_(0), firstExtremumY_(0),
|
andrewm@0
|
81 firstExtremumTimestamp_(missing_value<timestamp_diff_type>::missing()),
|
andrewm@0
|
82 lastExtremumTimestamp_(missing_value<timestamp_diff_type>::missing()),
|
andrewm@0
|
83 //vibratoType_(kDefaultVibratoType),
|
andrewm@0
|
84 vibratoPrescaler_(kDefaultVibratoPrescaler),
|
andrewm@0
|
85 vibratoRangeSemitones_(kDefaultVibratoRangeSemitones),
|
andrewm@0
|
86 lastPitchBendSemitones_(0),
|
andrewm@0
|
87 rawDistance_(kDefaultFilterBufferLength),
|
andrewm@0
|
88 filteredDistance_(kDefaultFilterBufferLength, rawDistance_)
|
andrewm@0
|
89 {
|
andrewm@0
|
90 // Initialize the filter coefficients for filtered key velocity (used for vibrato detection)
|
andrewm@0
|
91 std::vector<double> bCoeffs, aCoeffs;
|
andrewm@0
|
92 designSecondOrderBandpass(bCoeffs, aCoeffs, 9.0, 0.707, 200.0);
|
andrewm@0
|
93 std::vector<float> bCf(bCoeffs.begin(), bCoeffs.end()), aCf(aCoeffs.begin(), aCoeffs.end());
|
andrewm@0
|
94 filteredDistance_.setCoefficients(bCf, aCf);
|
andrewm@0
|
95 filteredDistance_.setAutoCalculate(true);
|
andrewm@0
|
96
|
andrewm@0
|
97 //setOscController(&keyboard_);
|
andrewm@0
|
98 resetDetectionState();
|
andrewm@0
|
99 }
|
andrewm@0
|
100
|
andrewm@0
|
101 TouchkeyVibratoMapping::~TouchkeyVibratoMapping() {
|
andrewm@0
|
102 }
|
andrewm@0
|
103
|
andrewm@0
|
104 // Turn off mapping of data. Remove our callback from the scheduler
|
andrewm@0
|
105 void TouchkeyVibratoMapping::disengage(bool shouldDelete) {
|
andrewm@0
|
106 sendVibratoMessage(0.0);
|
andrewm@0
|
107 TouchkeyBaseMapping::disengage(shouldDelete);
|
andrewm@0
|
108 }
|
andrewm@0
|
109
|
andrewm@0
|
110 // Reset state back to defaults
|
andrewm@0
|
111 void TouchkeyVibratoMapping::reset() {
|
andrewm@0
|
112 TouchkeyBaseMapping::reset();
|
andrewm@0
|
113 sendVibratoMessage(0.0);
|
andrewm@0
|
114 resetDetectionState();
|
andrewm@0
|
115 }
|
andrewm@0
|
116
|
andrewm@0
|
117 // Resend all current parameters
|
andrewm@0
|
118 void TouchkeyVibratoMapping::resend() {
|
andrewm@0
|
119 sendVibratoMessage(lastPitchBendSemitones_, true);
|
andrewm@0
|
120 }
|
andrewm@0
|
121
|
andrewm@0
|
122 // Set the range of vibrato
|
andrewm@0
|
123 void TouchkeyVibratoMapping::setRange(float rangeSemitones) {
|
andrewm@0
|
124 vibratoRangeSemitones_ = rangeSemitones;
|
andrewm@0
|
125 }
|
andrewm@0
|
126
|
andrewm@0
|
127 // Set the vibrato prescaler
|
andrewm@0
|
128 void TouchkeyVibratoMapping::setPrescaler(float prescaler) {
|
andrewm@0
|
129 vibratoPrescaler_ = prescaler;
|
andrewm@0
|
130 }
|
andrewm@0
|
131
|
andrewm@0
|
132 // Set the vibrato detection thresholds
|
andrewm@0
|
133 void TouchkeyVibratoMapping::setThresholds(float thresholdX, float thresholdY, float ratioX, float ratioY) {
|
andrewm@0
|
134 onsetThresholdX_ = thresholdX;
|
andrewm@0
|
135 onsetThresholdY_ = thresholdY;
|
andrewm@0
|
136 onsetRatioX_ = ratioX;
|
andrewm@0
|
137 onsetRatioY_ = ratioY;
|
andrewm@0
|
138 }
|
andrewm@0
|
139
|
andrewm@0
|
140 // Set the timeout for vibrato detection
|
andrewm@0
|
141 void TouchkeyVibratoMapping::setTimeout(timestamp_diff_type timeout) {
|
andrewm@0
|
142 onsetTimeout_ = timeout;
|
andrewm@0
|
143 }
|
andrewm@0
|
144
|
andrewm@0
|
145 // Trigger method. This receives updates from the TouchKey data or from state changes in
|
andrewm@0
|
146 // the continuous key position (KeyPositionTracker). It will potentially change the scheduled
|
andrewm@0
|
147 // behavior of future mapping calls, but the actual OSC messages should be transmitted in a different
|
andrewm@0
|
148 // thread.
|
andrewm@0
|
149 void TouchkeyVibratoMapping::triggerReceived(TriggerSource* who, timestamp_type timestamp) {
|
andrewm@0
|
150 if(who == 0)
|
andrewm@0
|
151 return;
|
andrewm@0
|
152
|
andrewm@0
|
153 if(who == touchBuffer_) {
|
andrewm@0
|
154 if(!touchBuffer_->empty()) {
|
andrewm@0
|
155 // New touch data is available. Find the distance from the onset location.
|
andrewm@0
|
156 KeyTouchFrame frame = touchBuffer_->latest();
|
andrewm@0
|
157 lastTimestamp_ = timestamp;
|
andrewm@0
|
158
|
andrewm@0
|
159 if(frame.count == 0) {
|
andrewm@0
|
160 // No touches. Last values are "missing", and we're not tracking any
|
andrewm@0
|
161 // particular touch ID
|
andrewm@0
|
162 lastX_ = lastY_ = missing_value<float>::missing();
|
andrewm@0
|
163 idOfCurrentTouch_ = -1;
|
andrewm@0
|
164 #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
|
andrewm@0
|
165 std::cout << "Touch off\n";
|
andrewm@0
|
166 #endif
|
andrewm@0
|
167 }
|
andrewm@0
|
168 else {
|
andrewm@0
|
169 // At least one touch. Check if we are already tracking an ID and, if so,
|
andrewm@0
|
170 // use its coordinates. Otherwise grab the lowest current ID.
|
andrewm@0
|
171
|
andrewm@0
|
172 bool foundCurrentTouch = false;
|
andrewm@0
|
173
|
andrewm@0
|
174 if(idOfCurrentTouch_ >= 0) {
|
andrewm@0
|
175 for(int i = 0; i < frame.count; i++) {
|
andrewm@0
|
176 if(frame.ids[i] == idOfCurrentTouch_) {
|
andrewm@0
|
177 lastY_ = frame.locs[i];
|
andrewm@0
|
178 if(frame.locH < 0 || (keyIsWhite() && lastY_ > kWhiteKeySingleAxisThreshold))
|
andrewm@0
|
179 lastX_ = missing_value<float>::missing();
|
andrewm@0
|
180 else
|
andrewm@0
|
181 lastX_ = frame.locH;
|
andrewm@0
|
182 foundCurrentTouch = true;
|
andrewm@0
|
183 break;
|
andrewm@0
|
184 }
|
andrewm@0
|
185 }
|
andrewm@0
|
186 }
|
andrewm@0
|
187
|
andrewm@0
|
188 if(!foundCurrentTouch) {
|
andrewm@0
|
189 // Assign a new touch to be tracked
|
andrewm@0
|
190 int lowestRemainingId = INT_MAX;
|
andrewm@0
|
191 int lowestIndex = 0;
|
andrewm@0
|
192
|
andrewm@0
|
193 for(int i = 0; i < frame.count; i++) {
|
andrewm@0
|
194 if(frame.ids[i] < lowestRemainingId) {
|
andrewm@0
|
195 lowestRemainingId = frame.ids[i];
|
andrewm@0
|
196 lowestIndex = i;
|
andrewm@0
|
197 }
|
andrewm@0
|
198 }
|
andrewm@0
|
199
|
andrewm@0
|
200 idOfCurrentTouch_ = lowestRemainingId;
|
andrewm@0
|
201 lastY_ = frame.locs[lowestIndex];
|
andrewm@0
|
202 if(frame.locH < 0 || (keyIsWhite() && lastY_ > kWhiteKeySingleAxisThreshold))
|
andrewm@0
|
203 lastX_ = missing_value<float>::missing();
|
andrewm@0
|
204 else
|
andrewm@0
|
205 lastX_ = frame.locH;
|
andrewm@0
|
206 #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
|
andrewm@0
|
207 std::cout << "Previous touch stopped; now ID " << idOfCurrentTouch_ << " at (" << lastX_ << ", " << lastY_ << ")\n";
|
andrewm@0
|
208 #endif
|
andrewm@0
|
209 }
|
andrewm@0
|
210
|
andrewm@0
|
211 // Now we have an X and (maybe) a Y coordinate for the most recent touch.
|
andrewm@0
|
212 // Check whether we have an initial location (if the note is active).
|
andrewm@0
|
213 if(noteIsOn_) {
|
andrewm@0
|
214 //ScopedLock sl(distanceAccessMutex_);
|
andrewm@0
|
215
|
andrewm@0
|
216 if(missing_value<float>::isMissing(onsetLocationY_) ||
|
andrewm@0
|
217 !foundCurrentTouch) {
|
andrewm@0
|
218 // Note is on but touch hasn't yet arrived --> this touch becomes
|
andrewm@0
|
219 // our onset location. Alternatively, the current touch is a different
|
andrewm@0
|
220 // ID from the previous one.
|
andrewm@0
|
221 onsetLocationY_ = lastY_;
|
andrewm@0
|
222 onsetLocationX_ = lastX_;
|
andrewm@0
|
223
|
andrewm@0
|
224 // Clear buffer and start with 0 distance for this point
|
andrewm@0
|
225 clearBuffers();
|
andrewm@0
|
226
|
andrewm@0
|
227 #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
|
andrewm@0
|
228 std::cout << "Starting at (" << onsetLocationX_ << ", " << onsetLocationY_ << ")\n";
|
andrewm@0
|
229 #endif
|
andrewm@0
|
230 }
|
andrewm@0
|
231 else {
|
andrewm@0
|
232 float distance = 0.0;
|
andrewm@0
|
233
|
andrewm@0
|
234 // Note is on and a start location exists. Calculate distance between
|
andrewm@0
|
235 // start location and the current point.
|
andrewm@0
|
236
|
andrewm@0
|
237 if(missing_value<float>::isMissing(onsetLocationX_) &&
|
andrewm@0
|
238 !missing_value<float>::isMissing(lastX_)) {
|
andrewm@0
|
239 // No X location indicated for onset but we have one now.
|
andrewm@0
|
240 // Update the onset X location.
|
andrewm@0
|
241 onsetLocationX_ = lastX_;
|
andrewm@0
|
242 #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
|
andrewm@0
|
243 std::cout << "Found first X location at " << onsetLocationX_ << std::endl;
|
andrewm@0
|
244 #endif
|
andrewm@0
|
245 }
|
andrewm@0
|
246
|
andrewm@0
|
247
|
andrewm@0
|
248 if(missing_value<float>::isMissing(lastX_) ||
|
andrewm@0
|
249 missing_value<float>::isMissing(onsetLocationX_)) {
|
andrewm@0
|
250 // If no X value is available on the current touch, calculate the distance
|
andrewm@0
|
251 // based on Y only. TODO: check whether we should do this by keeping the
|
andrewm@0
|
252 // last X value we recorded.
|
andrewm@0
|
253
|
andrewm@0
|
254 //distance = fabsf(lastY_ - onsetLocationY_);
|
andrewm@0
|
255 distance = lastY_ - onsetLocationY_;
|
andrewm@53
|
256 //distance = 0; // TESTING
|
andrewm@0
|
257 }
|
andrewm@0
|
258 else {
|
andrewm@0
|
259 // Euclidean distance between points
|
andrewm@0
|
260 //distance = sqrtf((lastY_ - onsetLocationY_) * (lastY_ - onsetLocationY_) +
|
andrewm@0
|
261 // (lastX_ - onsetLocationX_) * (lastX_ - onsetLocationX_));
|
andrewm@0
|
262 distance = lastX_ - onsetLocationX_;
|
andrewm@0
|
263 }
|
andrewm@0
|
264
|
andrewm@0
|
265 // Insert raw distance into the buffer. Bandpass filter calculates the next
|
andrewm@0
|
266 // sample automatically. The rest of the processing takes place in the dedicated
|
andrewm@0
|
267 // thread so as not to slow down commmunication with the hardware.
|
andrewm@0
|
268 rawDistance_.insert(distance, timestamp);
|
andrewm@0
|
269
|
andrewm@0
|
270 // Move the current scheduled event up to the present time.
|
andrewm@0
|
271 // FIXME: this may be more inefficient than just doing everything in the current thread!
|
andrewm@0
|
272 #ifdef NEW_MAPPING_SCHEDULER
|
andrewm@0
|
273 keyboard_.mappingScheduler().scheduleNow(this);
|
andrewm@0
|
274 #else
|
andrewm@0
|
275 keyboard_.unscheduleEvent(this);
|
andrewm@0
|
276 keyboard_.scheduleEvent(this, mappingAction_, keyboard_.schedulerCurrentTimestamp());
|
andrewm@0
|
277 #endif
|
andrewm@0
|
278
|
andrewm@0
|
279 //std::cout << "Raw distance " << distance << " filtered " << filteredDistance_.latest() << std::endl;
|
andrewm@0
|
280 }
|
andrewm@0
|
281 }
|
andrewm@0
|
282 }
|
andrewm@0
|
283 }
|
andrewm@0
|
284 }
|
andrewm@0
|
285 }
|
andrewm@0
|
286
|
andrewm@0
|
287 // Mapping method. This actually does the real work of sending OSC data in response to the
|
andrewm@0
|
288 // latest information from the touch sensors or continuous key angle
|
andrewm@0
|
289 timestamp_type TouchkeyVibratoMapping::performMapping() {
|
andrewm@0
|
290 //ScopedLock sl(distanceAccessMutex_);
|
andrewm@0
|
291
|
andrewm@0
|
292 timestamp_type currentTimestamp = keyboard_.schedulerCurrentTimestamp();
|
andrewm@0
|
293 bool newSamplePresent = false;
|
andrewm@0
|
294
|
andrewm@0
|
295 // Go through the filtered distance samples that are remaining to process.
|
andrewm@0
|
296 if(lastProcessedIndex_ < filteredDistance_.beginIndex() + 1) {
|
andrewm@0
|
297 // Fell off the beginning of the position buffer. Skip to the samples we have
|
andrewm@0
|
298 // (shouldn't happen except in cases of exceptional system load, and not too
|
andrewm@0
|
299 // consequential if it does happen).
|
andrewm@0
|
300 lastProcessedIndex_ = filteredDistance_.beginIndex() + 1;
|
andrewm@0
|
301 }
|
andrewm@0
|
302
|
andrewm@0
|
303 while(lastProcessedIndex_ < filteredDistance_.endIndex()) {
|
andrewm@0
|
304 float distance = filteredDistance_[lastProcessedIndex_];
|
andrewm@0
|
305 timestamp_type timestamp = filteredDistance_.timestampAt(lastProcessedIndex_);
|
andrewm@0
|
306 newSamplePresent = true;
|
andrewm@0
|
307
|
andrewm@0
|
308 if((distance > 0 && !lastSampleWasPositive_) ||
|
andrewm@0
|
309 (distance < 0 && lastSampleWasPositive_)) {
|
andrewm@0
|
310 // Found a zero crossing: save it if we're active or have at least found the
|
andrewm@0
|
311 // first extremum
|
andrewm@0
|
312 if(!missing_value<timestamp_type>::isMissing(lastZeroCrossingTimestamp_) &&
|
andrewm@0
|
313 (timestamp - lastZeroCrossingTimestamp_ > kZeroCrossingMinimumTime)) {
|
andrewm@0
|
314 if(vibratoState_ == kStateActive || vibratoState_ == kStateSwitchingOn ||
|
andrewm@0
|
315 foundFirstExtremum_) {
|
andrewm@0
|
316 lastZeroCrossingInterval_ = timestamp - lastZeroCrossingTimestamp_;
|
andrewm@0
|
317 #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
|
andrewm@0
|
318 std::cout << "Zero crossing interval " << lastZeroCrossingInterval_ << std::endl;
|
andrewm@0
|
319 #endif
|
andrewm@0
|
320 }
|
andrewm@0
|
321 }
|
andrewm@0
|
322 lastZeroCrossingTimestamp_ = timestamp;
|
andrewm@0
|
323 }
|
andrewm@0
|
324 lastSampleWasPositive_ = (distance > 0);
|
andrewm@0
|
325
|
andrewm@0
|
326 // If not currently engaged, check for the pattern of side-to-side motion that
|
andrewm@0
|
327 // begins a vibrato gesture.
|
andrewm@0
|
328 if(vibratoState_ == kStateInactive || vibratoState_ == kStateSwitchingOff) {
|
andrewm@0
|
329 if(foundFirstExtremum_) {
|
andrewm@0
|
330 // Already found first extremum. Look for second extremum in the opposite
|
andrewm@0
|
331 // direction of the given ratio from the original.
|
andrewm@0
|
332 if((firstExtremumX_ > 0 && distance < 0) ||
|
andrewm@0
|
333 (firstExtremumX_ < 0 && distance > 0)) {
|
andrewm@0
|
334 if(fabsf(distance) >= fabsf(firstExtremumX_) * onsetRatioX_) {
|
andrewm@0
|
335 #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
|
andrewm@0
|
336 std::cout << "Found second extremum at " << distance << ", TS " << timestamp << std::endl;
|
andrewm@0
|
337 #endif
|
andrewm@0
|
338 changeStateSwitchingOn(timestamp);
|
andrewm@0
|
339 }
|
andrewm@0
|
340 }
|
andrewm@0
|
341 else if(timestamp - lastExtremumTimestamp_ > onsetTimeout_) {
|
andrewm@0
|
342 #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
|
andrewm@0
|
343 std::cout << "Onset timeout at " << timestamp << endl;
|
andrewm@0
|
344 #endif
|
andrewm@0
|
345 resetDetectionState();
|
andrewm@0
|
346 }
|
andrewm@0
|
347 }
|
andrewm@0
|
348 else {
|
andrewm@0
|
349 if(fabsf(distance) >= onsetThresholdX_) {
|
andrewm@0
|
350 // TODO: differentiate X/Y here
|
andrewm@0
|
351 if(missing_value<float>::isMissing(firstExtremumX_) ||
|
andrewm@0
|
352 fabsf(distance) > fabsf(firstExtremumX_)) {
|
andrewm@0
|
353 firstExtremumX_ = distance;
|
andrewm@0
|
354 lastExtremumTimestamp_ = timestamp;
|
andrewm@0
|
355 #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
|
andrewm@0
|
356 std::cout << "First extremum candidate at " << firstExtremumX_ << ", TS " << lastExtremumTimestamp_ << std::endl;
|
andrewm@0
|
357 #endif
|
andrewm@0
|
358 }
|
andrewm@0
|
359 }
|
andrewm@0
|
360 else if(!missing_value<float>::isMissing(firstExtremumX_) &&
|
andrewm@0
|
361 fabsf(firstExtremumX_) > onsetThresholdX_) {
|
andrewm@0
|
362 // We must have found the first extremum since its maximum value is
|
andrewm@0
|
363 // above the threshold, and we must have moved away from it since we are
|
andrewm@0
|
364 // now below the threshold. Next step will be to look for extremum in
|
andrewm@0
|
365 // opposite direction. Save the timestamp of this location in case
|
andrewm@0
|
366 // another extremum is found later.
|
andrewm@0
|
367 firstExtremumTimestamp_ = lastExtremumTimestamp_;
|
andrewm@0
|
368 foundFirstExtremum_ = true;
|
andrewm@0
|
369 #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
|
andrewm@0
|
370 std::cout << "Found first extremum at " << firstExtremumX_ << ", TS " << lastExtremumTimestamp_ << std::endl;
|
andrewm@0
|
371 #endif
|
andrewm@0
|
372 }
|
andrewm@0
|
373 }
|
andrewm@0
|
374 }
|
andrewm@0
|
375 else {
|
andrewm@0
|
376 // Currently engaged. Look for timeout, defined as the finger staying below the lower (ratio-adjusted) threshold.
|
andrewm@0
|
377 if(fabsf(distance) >= onsetThresholdX_ * onsetRatioX_)
|
andrewm@0
|
378 lastExtremumTimestamp_ = timestamp;
|
andrewm@0
|
379 if(timestamp - lastExtremumTimestamp_ > onsetTimeout_) {
|
andrewm@0
|
380 #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
|
andrewm@0
|
381 std::cout << "Vibrato timeout at " << timestamp << " (last was " << lastExtremumTimestamp_ << ")" << endl;
|
andrewm@0
|
382 #endif
|
andrewm@0
|
383 changeStateSwitchingOff(timestamp);
|
andrewm@0
|
384 }
|
andrewm@0
|
385 }
|
andrewm@0
|
386
|
andrewm@0
|
387 lastProcessedIndex_++;
|
andrewm@0
|
388 }
|
andrewm@0
|
389
|
andrewm@0
|
390 // Having processed every sample individually for detection, send a pitch bend message based on the most
|
andrewm@0
|
391 // recent one (no sense in sending multiple pitch bend messages simultaneously).
|
andrewm@0
|
392 if(newSamplePresent && vibratoState_ != kStateInactive) {
|
andrewm@0
|
393 float distance = filteredDistance_.latest();
|
andrewm@0
|
394 float scale = 1.0;
|
andrewm@0
|
395
|
andrewm@0
|
396 if(vibratoState_ == kStateSwitchingOn) {
|
andrewm@0
|
397 // Switching on state gradually scales vibrato depth from 0 to
|
andrewm@0
|
398 // its final value over a specified switch-on time.
|
andrewm@0
|
399 if(rampLength_ <= 0 || (currentTimestamp - rampBeginTime_ >= rampLength_)) {
|
andrewm@0
|
400 scale = 1.0;
|
andrewm@0
|
401 changeStateActive(currentTimestamp);
|
andrewm@0
|
402 #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
|
andrewm@0
|
403 std::cout << "Vibrato switch on finished, going to Active\n";
|
andrewm@0
|
404 #endif
|
andrewm@0
|
405 }
|
andrewm@0
|
406 else {
|
andrewm@0
|
407 lastCalculatedRampValue_ = rampScaleValue_ * (float)(currentTimestamp - rampBeginTime_)/(float)rampLength_;
|
andrewm@0
|
408 scale = lastCalculatedRampValue_;
|
andrewm@0
|
409 //std::cout << "Vibrato scale " << scale << ", TS " << currentTimestamp - rampBeginTime_ << std::endl;
|
andrewm@0
|
410 }
|
andrewm@0
|
411 }
|
andrewm@0
|
412 else if(vibratoState_ == kStateSwitchingOff) {
|
andrewm@0
|
413 // Switching off state gradually scales vibrato depth from full
|
andrewm@0
|
414 // value to 0 over a specified switch-off time.
|
andrewm@0
|
415 if(rampLength_ <= 0 || (currentTimestamp - rampBeginTime_ >= rampLength_)) {
|
andrewm@0
|
416 scale = 0.0;
|
andrewm@0
|
417 changeStateInactive(currentTimestamp);
|
andrewm@0
|
418 #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
|
andrewm@0
|
419 std::cout << "Vibrato switch off finished, going to Inactive\n";
|
andrewm@0
|
420 #endif
|
andrewm@0
|
421 }
|
andrewm@0
|
422 else {
|
andrewm@0
|
423 lastCalculatedRampValue_ = rampScaleValue_ * (1.0 - (float)(currentTimestamp - rampBeginTime_)/(float)rampLength_);
|
andrewm@0
|
424 scale = lastCalculatedRampValue_;
|
andrewm@0
|
425 //std::cout << "Vibrato scale " << scale << ", TS " << currentTimestamp - rampBeginTime_ << std::endl;
|
andrewm@0
|
426 }
|
andrewm@0
|
427 }
|
andrewm@0
|
428
|
andrewm@0
|
429 // Calculate pitch bend based on current distance, with a non-linear scaling to accentuate
|
andrewm@0
|
430 // smaller motions.
|
andrewm@0
|
431 float pitchBendSemitones = vibratoRangeSemitones_ * tanhf(vibratoPrescaler_ * scale * distance);
|
andrewm@0
|
432
|
andrewm@0
|
433 sendVibratoMessage(pitchBendSemitones);
|
andrewm@0
|
434 lastPitchBendSemitones_ = pitchBendSemitones;
|
andrewm@0
|
435 }
|
andrewm@0
|
436
|
andrewm@0
|
437 // We may have arrived here without a new touch, just based on timing. Check for timeouts and process
|
andrewm@0
|
438 // any release in progress.
|
andrewm@0
|
439 if(!newSamplePresent) {
|
andrewm@0
|
440 if(vibratoState_ == kStateSwitchingOff) {
|
andrewm@0
|
441 // No new information in the distance buffer, but we do need to gradually reduce the pitch bend to zero
|
andrewm@0
|
442 if(rampLength_ <= 0 || (currentTimestamp - rampBeginTime_ >= rampLength_)) {
|
andrewm@0
|
443 sendVibratoMessage(0.0);
|
andrewm@0
|
444 lastPitchBendSemitones_ = 0;
|
andrewm@0
|
445 changeStateInactive(currentTimestamp);
|
andrewm@0
|
446 #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
|
andrewm@0
|
447 std::cout << "Vibrato switch off finished, going to Inactive\n";
|
andrewm@0
|
448 #endif
|
andrewm@0
|
449 }
|
andrewm@0
|
450 else {
|
andrewm@0
|
451 // Still in the middle of the ramp. Calculate its current value based on the last one
|
andrewm@0
|
452 // that actually had a touch data point (lastPitchBendSemitones_).
|
andrewm@0
|
453
|
andrewm@0
|
454 lastCalculatedRampValue_ = rampScaleValue_ * (1.0 - (float)(currentTimestamp - rampBeginTime_)/(float)rampLength_);
|
andrewm@0
|
455 float pitchBendSemitones = lastPitchBendSemitones_ * lastCalculatedRampValue_;
|
andrewm@0
|
456
|
andrewm@0
|
457 sendVibratoMessage(pitchBendSemitones);
|
andrewm@0
|
458 }
|
andrewm@0
|
459 }
|
andrewm@0
|
460 else if(vibratoState_ != kStateInactive) {
|
andrewm@0
|
461 // Might still be active but with no data coming in. We need to look for a timeout here too.
|
andrewm@0
|
462 if(currentTimestamp - lastExtremumTimestamp_ > onsetTimeout_) {
|
andrewm@0
|
463 #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
|
andrewm@0
|
464 std::cout << "Vibrato timeout at " << currentTimestamp << " (2; last was " << lastExtremumTimestamp_ << ")" << endl;
|
andrewm@0
|
465 #endif
|
andrewm@0
|
466 changeStateSwitchingOff(currentTimestamp);
|
andrewm@0
|
467 }
|
andrewm@0
|
468 }
|
andrewm@0
|
469 }
|
andrewm@0
|
470
|
andrewm@0
|
471 // Register for the next update by returning its timestamp
|
andrewm@0
|
472 nextScheduledTimestamp_ = currentTimestamp + updateInterval_;
|
andrewm@0
|
473 return nextScheduledTimestamp_;
|
andrewm@0
|
474 }
|
andrewm@0
|
475
|
andrewm@0
|
476 // MIDI note-on message received
|
andrewm@0
|
477 void TouchkeyVibratoMapping::midiNoteOnReceived(int channel, int velocity) {
|
andrewm@0
|
478 // MIDI note has gone on. Set the starting location to be most recent
|
andrewm@0
|
479 // location. It's possible there has been no touch data before this,
|
andrewm@0
|
480 // in which case lastX and lastY will hold missing values.
|
andrewm@0
|
481 onsetLocationX_ = lastX_;
|
andrewm@0
|
482 onsetLocationY_ = lastY_;
|
andrewm@0
|
483 if(!missing_value<float>::isMissing(onsetLocationY_)) {
|
andrewm@0
|
484 // Already have touch data. Clear the buffer here.
|
andrewm@0
|
485 // Clear buffer and start with 0 distance for this point
|
andrewm@0
|
486 clearBuffers();
|
andrewm@0
|
487
|
andrewm@0
|
488 #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
|
andrewm@0
|
489 std::cout << "MIDI on: starting at (" << onsetLocationX_ << ", " << onsetLocationY_ << ")\n";
|
andrewm@0
|
490 #endif
|
andrewm@0
|
491 }
|
andrewm@0
|
492 else {
|
andrewm@0
|
493 #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
|
andrewm@0
|
494 std::cout << "MIDI on but no touch\n";
|
andrewm@0
|
495 #endif
|
andrewm@0
|
496 }
|
andrewm@0
|
497 }
|
andrewm@0
|
498
|
andrewm@0
|
499 // MIDI note-off message received
|
andrewm@0
|
500 void TouchkeyVibratoMapping::midiNoteOffReceived(int channel) {
|
andrewm@0
|
501 if(vibratoState_ == kStateActive || vibratoState_ == kStateSwitchingOn) {
|
andrewm@0
|
502 changeStateSwitchingOff(keyboard_.schedulerCurrentTimestamp());
|
andrewm@0
|
503 }
|
andrewm@0
|
504 }
|
andrewm@0
|
505
|
andrewm@0
|
506 // Internal state-change methods, which keep the state variables in sync
|
andrewm@0
|
507 void TouchkeyVibratoMapping::changeStateSwitchingOn(timestamp_type timestamp) {
|
andrewm@0
|
508 // Go to SwitchingOn state, which brings the vibrato value gradually up to full amplitude
|
andrewm@0
|
509
|
andrewm@0
|
510 // TODO: need to start from a non-zero value if SwitchingOff
|
andrewm@0
|
511 rampScaleValue_ = 1.0;
|
andrewm@0
|
512 rampBeginTime_ = timestamp;
|
andrewm@0
|
513 rampLength_ = 0.0;
|
andrewm@0
|
514 // Interval between peak and zero crossing will be a quarter of a cycle.
|
andrewm@0
|
515 // From this, figure out how much longer we have to go to get to the next
|
andrewm@0
|
516 // peak if the rate remains the same.
|
andrewm@0
|
517 if(!missing_value<timestamp_type>::isMissing(lastZeroCrossingTimestamp_) &&
|
andrewm@0
|
518 !missing_value<timestamp_type>::isMissing(firstExtremumTimestamp_)) {
|
andrewm@0
|
519 timestamp_type estimatedPeakTimestamp = lastZeroCrossingTimestamp_ + (lastZeroCrossingTimestamp_ - firstExtremumTimestamp_);
|
andrewm@0
|
520 rampLength_ = estimatedPeakTimestamp - timestamp;
|
andrewm@0
|
521 if(rampLength_ < kMinimumOnsetTime)
|
andrewm@0
|
522 rampLength_ = kMinimumOnsetTime;
|
andrewm@0
|
523 if(rampLength_ > kMaximumOnsetTime)
|
andrewm@0
|
524 rampLength_ = kMaximumOnsetTime;
|
andrewm@0
|
525 #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
|
andrewm@0
|
526 std::cout << "Switching on with ramp length " << rampLength_ << " (peak " << firstExtremumTimestamp_ << ", zero " << lastZeroCrossingTimestamp_ << ")" << std::endl;
|
andrewm@0
|
527 #endif
|
andrewm@0
|
528 }
|
andrewm@0
|
529
|
andrewm@0
|
530 vibratoState_ = kStateSwitchingOn;
|
andrewm@0
|
531 }
|
andrewm@0
|
532
|
andrewm@0
|
533 void TouchkeyVibratoMapping::changeStateSwitchingOff(timestamp_type timestamp) {
|
andrewm@0
|
534 // Go to SwitchingOff state, which brings the vibrato value gradually down to 0
|
andrewm@0
|
535
|
andrewm@0
|
536 if(vibratoState_ == kStateSwitchingOn) {
|
andrewm@0
|
537 // Might already be in the midst of a ramp up. Start from its current value
|
andrewm@0
|
538 rampScaleValue_ = lastCalculatedRampValue_;
|
andrewm@0
|
539 }
|
andrewm@0
|
540 else
|
andrewm@0
|
541 rampScaleValue_ = 1.0;
|
andrewm@0
|
542
|
andrewm@0
|
543 rampBeginTime_ = timestamp;
|
andrewm@0
|
544 rampLength_ = lastZeroCrossingInterval_;
|
andrewm@0
|
545 if(rampLength_ < kMinimumReleaseTime)
|
andrewm@0
|
546 rampLength_ = kMinimumReleaseTime;
|
andrewm@0
|
547 if(rampLength_ > kMaximumReleaseTime)
|
andrewm@0
|
548 rampLength_ = kMaximumReleaseTime;
|
andrewm@0
|
549
|
andrewm@0
|
550 #ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
|
andrewm@0
|
551 std::cout << "Switching off with ramp length " << rampLength_ << std::endl;
|
andrewm@0
|
552 #endif
|
andrewm@0
|
553
|
andrewm@0
|
554 resetDetectionState();
|
andrewm@0
|
555 vibratoState_ = kStateSwitchingOff;
|
andrewm@0
|
556 }
|
andrewm@0
|
557
|
andrewm@0
|
558 void TouchkeyVibratoMapping::changeStateActive(timestamp_type timestamp) {
|
andrewm@0
|
559 vibratoState_ = kStateActive;
|
andrewm@0
|
560 }
|
andrewm@0
|
561
|
andrewm@0
|
562 void TouchkeyVibratoMapping::changeStateInactive(timestamp_type timestamp) {
|
andrewm@0
|
563 vibratoState_ = kStateInactive;
|
andrewm@0
|
564 }
|
andrewm@0
|
565
|
andrewm@0
|
566 // Reset variables involved in detecting a vibrato gesture
|
andrewm@0
|
567 void TouchkeyVibratoMapping::resetDetectionState() {
|
andrewm@0
|
568 foundFirstExtremum_ = false;
|
andrewm@0
|
569 firstExtremumX_ = firstExtremumY_ = 0.0;
|
andrewm@0
|
570 lastExtremumTimestamp_ = firstExtremumTimestamp_ = lastZeroCrossingTimestamp_ = missing_value<timestamp_type>::missing();
|
andrewm@0
|
571 }
|
andrewm@0
|
572
|
andrewm@0
|
573 // Clear the buffers that hold distance measurements
|
andrewm@0
|
574 void TouchkeyVibratoMapping::clearBuffers() {
|
andrewm@0
|
575 rawDistance_.clear();
|
andrewm@0
|
576 filteredDistance_.clear();
|
andrewm@0
|
577 rawDistance_.insert(0.0, lastTimestamp_);
|
andrewm@0
|
578 lastProcessedIndex_ = 0;
|
andrewm@0
|
579 }
|
andrewm@0
|
580
|
andrewm@0
|
581 bool TouchkeyVibratoMapping::keyIsWhite() {
|
andrewm@0
|
582 int modNoteNumber = noteNumber_ % 12;
|
andrewm@0
|
583 if(modNoteNumber == 1 ||
|
andrewm@0
|
584 modNoteNumber == 3 ||
|
andrewm@0
|
585 modNoteNumber == 6 ||
|
andrewm@0
|
586 modNoteNumber == 8 ||
|
andrewm@0
|
587 modNoteNumber == 10)
|
andrewm@0
|
588 return false;
|
andrewm@0
|
589 return true;
|
andrewm@0
|
590 }
|
andrewm@0
|
591
|
andrewm@0
|
592 // Send the vibrato message of a given number of a semitones. Send by OSC,
|
andrewm@0
|
593 // which can be mapped to MIDI CC externally
|
andrewm@0
|
594 void TouchkeyVibratoMapping::sendVibratoMessage(float pitchBendSemitones, bool force) {
|
andrewm@0
|
595 if(force || !suspended_) {
|
andrewm@0
|
596 //if(vibratoType_ == kVibratoTypePitchBend)
|
andrewm@0
|
597 // keyboard_.sendMessage("/touchkeys/vibrato", "if", noteNumber_, pitchBendSemitones, LO_ARGS_END);
|
andrewm@0
|
598 //else if(vibratoType_ == kVibratoTypeAmplitude)
|
andrewm@0
|
599 keyboard_.sendMessage(controlName_.c_str(), "if", noteNumber_, pitchBendSemitones, LO_ARGS_END);
|
andrewm@0
|
600 // Otherwise, if unknown type, ignore.
|
andrewm@0
|
601 }
|
andrewm@0
|
602 }
|
andrewm@0
|
603
|