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 TouchkeyReleaseAngleMapping.cpp: per-note mapping for the release angle
|
andrewm@0
|
21 mapping, which measures the speed of finger motion along the key at
|
andrewm@0
|
22 the time of MIDI note off.
|
andrewm@0
|
23 */
|
andrewm@0
|
24
|
andrewm@0
|
25 #include "TouchkeyReleaseAngleMapping.h"
|
andrewm@46
|
26 #include "TouchkeyReleaseAngleMappingFactory.h"
|
andrewm@0
|
27 #include "../MappingFactory.h"
|
andrewm@0
|
28 #include "../../TouchKeys/MidiOutputController.h"
|
andrewm@0
|
29 #include "../MappingScheduler.h"
|
andrewm@0
|
30
|
andrewm@46
|
31 #define DEBUG_RELEASE_ANGLE_MAPPING
|
andrewm@46
|
32
|
andrewm@0
|
33 // Class constants
|
andrewm@0
|
34 const int TouchkeyReleaseAngleMapping::kDefaultFilterBufferLength = 30;
|
andrewm@0
|
35 const timestamp_diff_type TouchkeyReleaseAngleMapping::kDefaultMaxLookbackTime = milliseconds_to_timestamp(100);
|
andrewm@0
|
36
|
andrewm@46
|
37 const float TouchkeyReleaseAngleMapping::kDefaultUpMinimumAngle = 1.0;
|
andrewm@46
|
38 const float TouchkeyReleaseAngleMapping::kDefaultDownMinimumAngle = 1.0;
|
andrewm@46
|
39
|
andrewm@0
|
40 // Main constructor takes references/pointers from objects which keep track
|
andrewm@0
|
41 // of touch location, continuous key position and the state detected from that
|
andrewm@0
|
42 // position. The PianoKeyboard object is strictly required as it gives access to
|
andrewm@0
|
43 // Scheduler and OSC methods. The others are optional since any given system may
|
andrewm@0
|
44 // contain only one of continuous key position or touch sensitivity
|
andrewm@0
|
45 TouchkeyReleaseAngleMapping::TouchkeyReleaseAngleMapping(PianoKeyboard &keyboard, MappingFactory *factory, int noteNumber, Node<KeyTouchFrame>* touchBuffer,
|
andrewm@0
|
46 Node<key_position>* positionBuffer, KeyPositionTracker* positionTracker)
|
andrewm@0
|
47 : TouchkeyBaseMapping(keyboard, factory, noteNumber, touchBuffer, positionBuffer, positionTracker, false),
|
andrewm@46
|
48 upEnabled_(true), downEnabled_(true), upMinimumAngle_(kDefaultUpMinimumAngle), downMinimumAngle_(kDefaultDownMinimumAngle),
|
andrewm@0
|
49 pastSamples_(kDefaultFilterBufferLength), maxLookbackTime_(kDefaultMaxLookbackTime)
|
andrewm@0
|
50 {
|
andrewm@46
|
51 for(int i = 0; i < RELEASE_ANGLE_MAX_SEQUENCE_LENGTH; i++)
|
andrewm@46
|
52 upNotes_[i] = downNotes_[i] = upVelocities_[i] = downVelocities_[i] = 0;
|
andrewm@0
|
53 }
|
andrewm@0
|
54
|
andrewm@0
|
55 // Reset state back to defaults
|
andrewm@0
|
56 void TouchkeyReleaseAngleMapping::reset() {
|
andrewm@0
|
57 ScopedLock sl(sampleBufferMutex_);
|
andrewm@0
|
58
|
andrewm@0
|
59 TouchkeyBaseMapping::reset();
|
andrewm@0
|
60 pastSamples_.clear();
|
andrewm@0
|
61 }
|
andrewm@0
|
62
|
andrewm@0
|
63 // Resend all current parameters
|
andrewm@0
|
64 void TouchkeyReleaseAngleMapping::resend() {
|
andrewm@0
|
65 // Message is only sent at release; resend may not apply here.
|
andrewm@0
|
66 }
|
andrewm@0
|
67
|
andrewm@46
|
68 // Parameters for release angle algorithm
|
andrewm@46
|
69 void TouchkeyReleaseAngleMapping::setWindowSize(float windowSize) {
|
andrewm@46
|
70 // This was passed in in milliseconds and needs to be converted to a timestamp type
|
andrewm@46
|
71 maxLookbackTime_ = milliseconds_to_timestamp(windowSize);
|
andrewm@46
|
72 }
|
andrewm@46
|
73
|
andrewm@46
|
74 void TouchkeyReleaseAngleMapping::setUpMessagesEnabled(bool enable) {
|
andrewm@46
|
75 upEnabled_ = enable;
|
andrewm@46
|
76 }
|
andrewm@46
|
77
|
andrewm@46
|
78 void TouchkeyReleaseAngleMapping::setDownMessagesEnabled(bool enable) {
|
andrewm@46
|
79 downEnabled_ = enable;
|
andrewm@46
|
80 }
|
andrewm@46
|
81
|
andrewm@46
|
82 void TouchkeyReleaseAngleMapping::setUpMinimumAngle(float minAngle) {
|
andrewm@46
|
83 upMinimumAngle_ = fabsf(minAngle);
|
andrewm@46
|
84 }
|
andrewm@46
|
85
|
andrewm@46
|
86 void TouchkeyReleaseAngleMapping::setUpNote(int sequence, int note) {
|
andrewm@46
|
87 if(sequence < 0 || sequence >= RELEASE_ANGLE_MAX_SEQUENCE_LENGTH)
|
andrewm@46
|
88 return;
|
andrewm@46
|
89 if(note < 0 || note > 127)
|
andrewm@46
|
90 upNotes_[sequence] = 0;
|
andrewm@46
|
91 else
|
andrewm@46
|
92 upNotes_[sequence] = note;
|
andrewm@46
|
93 }
|
andrewm@46
|
94
|
andrewm@46
|
95 void TouchkeyReleaseAngleMapping::setUpVelocity(int sequence, int velocity) {
|
andrewm@46
|
96 if(sequence < 0 || sequence >= RELEASE_ANGLE_MAX_SEQUENCE_LENGTH)
|
andrewm@46
|
97 return;
|
andrewm@46
|
98 if(velocity < 0 || velocity > 127)
|
andrewm@46
|
99 upVelocities_[sequence] = 0;
|
andrewm@46
|
100 else
|
andrewm@46
|
101 upVelocities_[sequence] = velocity;
|
andrewm@46
|
102 }
|
andrewm@46
|
103
|
andrewm@46
|
104 void TouchkeyReleaseAngleMapping::setDownMinimumAngle(float minAngle) {
|
andrewm@46
|
105 downMinimumAngle_ = fabsf(minAngle);
|
andrewm@46
|
106 }
|
andrewm@46
|
107
|
andrewm@46
|
108 void TouchkeyReleaseAngleMapping::setDownNote(int sequence, int note) {
|
andrewm@46
|
109 if(sequence < 0 || sequence >= RELEASE_ANGLE_MAX_SEQUENCE_LENGTH)
|
andrewm@46
|
110 return;
|
andrewm@46
|
111 if(note < 0 || note > 127)
|
andrewm@46
|
112 downNotes_[sequence] = 0;
|
andrewm@46
|
113 else
|
andrewm@46
|
114 downNotes_[sequence] = note;
|
andrewm@46
|
115 }
|
andrewm@46
|
116
|
andrewm@46
|
117 void TouchkeyReleaseAngleMapping::setDownVelocity(int sequence, int velocity) {
|
andrewm@46
|
118 if(sequence < 0 || sequence >= RELEASE_ANGLE_MAX_SEQUENCE_LENGTH)
|
andrewm@46
|
119 return;
|
andrewm@46
|
120 if(velocity < 0 || velocity > 127)
|
andrewm@46
|
121 downVelocities_[sequence] = 0;
|
andrewm@46
|
122 else
|
andrewm@46
|
123 downVelocities_[sequence] = velocity;
|
andrewm@46
|
124 }
|
andrewm@46
|
125
|
andrewm@0
|
126 // This method receives data from the touch buffer or possibly the continuous key angle (not used here)
|
andrewm@0
|
127 void TouchkeyReleaseAngleMapping::triggerReceived(TriggerSource* who, timestamp_type timestamp) {
|
andrewm@0
|
128 if(who == touchBuffer_) {
|
andrewm@0
|
129 ScopedLock sl(sampleBufferMutex_);
|
andrewm@0
|
130
|
andrewm@0
|
131 // Save the latest frame, even if it is an empty touch (we need to know what happened even
|
andrewm@0
|
132 // after the touch ends since the MIDI off may come later)
|
andrewm@0
|
133 if(!touchBuffer_->empty())
|
andrewm@0
|
134 pastSamples_.insert(touchBuffer_->latest(), touchBuffer_->latestTimestamp());
|
andrewm@0
|
135 }
|
andrewm@0
|
136 }
|
andrewm@0
|
137
|
andrewm@0
|
138 // Mapping method. This actually does the real work of sending OSC data in response to the
|
andrewm@0
|
139 // latest information from the touch sensors or continuous key angle
|
andrewm@0
|
140 timestamp_type TouchkeyReleaseAngleMapping::performMapping() {
|
andrewm@0
|
141 // Nothing to do here until note is released.
|
andrewm@0
|
142 // Register for the next update by returning its timestamp
|
andrewm@0
|
143 // TODO: do we even need this? Check Mapping::engage() and Mapping::disengage()
|
andrewm@0
|
144 timestamp_type currentTimestamp = keyboard_.schedulerCurrentTimestamp();
|
andrewm@0
|
145 nextScheduledTimestamp_ = currentTimestamp + updateInterval_;
|
andrewm@0
|
146 return nextScheduledTimestamp_;
|
andrewm@0
|
147 }
|
andrewm@0
|
148
|
andrewm@46
|
149 void TouchkeyReleaseAngleMapping::midiNoteOffReceived(int channel) {
|
andrewm@46
|
150 processRelease();
|
andrewm@46
|
151 }
|
andrewm@46
|
152
|
andrewm@46
|
153 void TouchkeyReleaseAngleMapping::processRelease(/*timestamp_type timestamp*/) {
|
andrewm@0
|
154 if(!noteIsOn_) {
|
andrewm@0
|
155 return;
|
andrewm@0
|
156 }
|
andrewm@0
|
157
|
andrewm@0
|
158 sampleBufferMutex_.enter();
|
andrewm@0
|
159
|
andrewm@0
|
160 // Look backwards from the current timestamp to find the velocity
|
andrewm@0
|
161 float calculatedVelocity = missing_value<float>::missing();
|
andrewm@0
|
162 bool touchWasOn = false;
|
andrewm@0
|
163
|
andrewm@0
|
164 if(!pastSamples_.empty()) {
|
andrewm@0
|
165 Node<KeyTouchFrame>::size_type index = pastSamples_.endIndex() - 1;
|
andrewm@0
|
166 Node<KeyTouchFrame>::size_type mostRecentTouchPresentIndex = pastSamples_.endIndex() - 1;
|
andrewm@46
|
167 timestamp_type lastTimestamp = pastSamples_.timestampAt(index);
|
andrewm@46
|
168
|
andrewm@0
|
169 while(index >= pastSamples_.beginIndex()) {
|
andrewm@46
|
170 #ifdef DEBUG_RELEASE_ANGLE_MAPPING
|
andrewm@46
|
171 std::cout << "examining sample " << index << " with " << pastSamples_[index].count << " touches and time diff " << lastTimestamp - pastSamples_.timestampAt(index) << "\n";
|
andrewm@46
|
172 #endif
|
andrewm@46
|
173 if(lastTimestamp - pastSamples_.timestampAt(index) >= maxLookbackTime_)
|
andrewm@0
|
174 break;
|
andrewm@0
|
175 if(pastSamples_[index].count == 0) {
|
andrewm@0
|
176 if(touchWasOn) {
|
andrewm@0
|
177 // We found a break in the touch; stop here. But don't stop
|
andrewm@0
|
178 // if the first frames we consider have no touches.
|
andrewm@0
|
179 if(index < pastSamples_.endIndex() - 1)
|
andrewm@0
|
180 index++;
|
andrewm@0
|
181 break;
|
andrewm@0
|
182 }
|
andrewm@0
|
183 }
|
andrewm@0
|
184 else if(!touchWasOn) {
|
andrewm@0
|
185 mostRecentTouchPresentIndex = index;
|
andrewm@0
|
186 touchWasOn = true;
|
andrewm@0
|
187 }
|
andrewm@0
|
188 // Can't decrement past 0 in an unsigned type
|
andrewm@0
|
189 if(index == 0)
|
andrewm@0
|
190 break;
|
andrewm@0
|
191 index--;
|
andrewm@0
|
192 }
|
andrewm@0
|
193
|
andrewm@0
|
194 // If we fell off the beginning of the buffer, back up.
|
andrewm@0
|
195 if(index < pastSamples_.beginIndex())
|
andrewm@0
|
196 index = pastSamples_.beginIndex();
|
andrewm@0
|
197
|
andrewm@0
|
198 // Need at least two points for this calculation to work
|
andrewm@0
|
199 timestamp_type endingTimestamp = pastSamples_.timestampAt(mostRecentTouchPresentIndex);
|
andrewm@0
|
200 timestamp_type startingTimestamp = pastSamples_.timestampAt(index);
|
andrewm@0
|
201 if(endingTimestamp - startingTimestamp > 0) {
|
andrewm@0
|
202 float endingPosition = pastSamples_[mostRecentTouchPresentIndex].locs[0];
|
andrewm@0
|
203 float startingPosition = pastSamples_[index].locs[0];
|
andrewm@0
|
204 calculatedVelocity = (endingPosition - startingPosition) / (endingTimestamp - startingTimestamp);
|
andrewm@0
|
205 }
|
andrewm@0
|
206 else { // DEBUG
|
andrewm@46
|
207 #ifdef DEBUG_RELEASE_ANGLE_MAPPING
|
andrewm@0
|
208 std::cout << "Found 0 timestamp difference on key release (indices " << index << " and " << pastSamples_.endIndex() - 1 << "\n";
|
andrewm@46
|
209 #endif
|
andrewm@0
|
210 }
|
andrewm@0
|
211 }
|
andrewm@46
|
212 else {
|
andrewm@46
|
213 #ifdef DEBUG_RELEASE_ANGLE_MAPPING
|
andrewm@0
|
214 std::cout << "Found empty touch buffer on key release\n";
|
andrewm@46
|
215 #endif
|
andrewm@46
|
216 }
|
andrewm@0
|
217
|
andrewm@0
|
218 sampleBufferMutex_.exit();
|
andrewm@0
|
219
|
andrewm@0
|
220 if(!missing_value<float>::isMissing(calculatedVelocity)) {
|
andrewm@46
|
221 #ifdef DEBUG_RELEASE_ANGLE_MAPPING
|
andrewm@0
|
222 std::cout << "Found release velocity " << calculatedVelocity << " on note " << noteNumber_ << std::endl;
|
andrewm@46
|
223 #endif
|
andrewm@0
|
224 sendReleaseAngleMessage(calculatedVelocity);
|
andrewm@0
|
225 }
|
andrewm@0
|
226
|
andrewm@0
|
227
|
andrewm@46
|
228 // Check if we're supposed to clean up now
|
andrewm@0
|
229 finished_ = true;
|
andrewm@0
|
230 if(finishRequested_)
|
andrewm@0
|
231 acknowledgeFinish();
|
andrewm@0
|
232 // KLUDGE
|
andrewm@0
|
233 }
|
andrewm@0
|
234
|
andrewm@0
|
235 void TouchkeyReleaseAngleMapping::sendReleaseAngleMessage(float releaseAngle, bool force) {
|
andrewm@0
|
236 if(force || !suspended_) {
|
andrewm@0
|
237 keyboard_.sendMessage("/touchkeys/releaseangle", "if", noteNumber_, releaseAngle, LO_ARGS_END);
|
andrewm@0
|
238
|
andrewm@46
|
239 if(keyboard_.midiOutputController() == 0)
|
andrewm@46
|
240 return;
|
andrewm@46
|
241
|
andrewm@46
|
242 int port = static_cast<TouchkeyReleaseAngleMappingFactory*>(factory_)->segment().outputPort();
|
andrewm@46
|
243 int ch = keyboard_.key(noteNumber_)->midiChannel();
|
andrewm@46
|
244
|
andrewm@46
|
245 // Check if the release angle exceeds either the up or down threshold
|
andrewm@46
|
246 if(releaseAngle > 0 && fabs(releaseAngle) >= upMinimumAngle_ && upEnabled_) {
|
andrewm@46
|
247 #ifdef DEBUG_RELEASE_ANGLE_MAPPING
|
andrewm@46
|
248 std::cout << "Send up-release messages for note " << noteNumber_ << " on channel " << ch << "\n";
|
andrewm@46
|
249 #endif
|
andrewm@46
|
250 // Send key switches: note on and note off in reverse orders
|
andrewm@46
|
251 for(int i = 0; i < RELEASE_ANGLE_MAX_SEQUENCE_LENGTH; i++) {
|
andrewm@46
|
252 if(upNotes_[i] != 0)
|
andrewm@46
|
253 keyboard_.midiOutputController()->sendNoteOn(port, ch, upNotes_[i], upVelocities_[i]);
|
andrewm@46
|
254 }
|
andrewm@46
|
255
|
andrewm@46
|
256 for(int i = RELEASE_ANGLE_MAX_SEQUENCE_LENGTH - 1; i >= 0; i--) {
|
andrewm@46
|
257 if(upNotes_[i] != 0)
|
andrewm@46
|
258 keyboard_.midiOutputController()->sendNoteOff(port, ch, upNotes_[i]);
|
andrewm@46
|
259 }
|
andrewm@46
|
260 }
|
andrewm@46
|
261 else if(releaseAngle < 0 && fabs(releaseAngle) >= downMinimumAngle_ && downEnabled_) {
|
andrewm@46
|
262 #ifdef DEBUG_RELEASE_ANGLE_MAPPING
|
andrewm@46
|
263 std::cout << "Send down-release messages for note " << noteNumber_ << " on channel " << ch << "\n";
|
andrewm@46
|
264 #endif
|
andrewm@46
|
265 // Send key switches: note on and note off in reverse orders
|
andrewm@46
|
266 for(int i = 0; i < RELEASE_ANGLE_MAX_SEQUENCE_LENGTH; i++) {
|
andrewm@46
|
267 if(downNotes_[i] != 0)
|
andrewm@46
|
268 keyboard_.midiOutputController()->sendNoteOn(port, ch, downNotes_[i], downVelocities_[i]);
|
andrewm@46
|
269 }
|
andrewm@46
|
270
|
andrewm@46
|
271 for(int i = RELEASE_ANGLE_MAX_SEQUENCE_LENGTH - 1; i >= 0; i--) {
|
andrewm@46
|
272 if(downNotes_[i] != 0)
|
andrewm@46
|
273 keyboard_.midiOutputController()->sendNoteOff(port, ch, downNotes_[i]);
|
andrewm@46
|
274 }
|
andrewm@46
|
275 }
|
andrewm@46
|
276
|
andrewm@46
|
277 // TODO: delayed release
|
andrewm@46
|
278
|
andrewm@0
|
279 #ifdef TROMBONE
|
andrewm@0
|
280 // KLUDGE: figure out how to do this more elegantly
|
andrewm@0
|
281 if(keyboard_.midiOutputController() != 0) {
|
andrewm@0
|
282 if(releaseAngle > 1.0) {
|
andrewm@0
|
283 keyboard_.midiOutputController()->sendNoteOn(0, 0, 36, 64);
|
andrewm@0
|
284 keyboard_.midiOutputController()->sendNoteOn(0, 0, 31, 96);
|
andrewm@0
|
285 keyboard_.midiOutputController()->sendNoteOff(0, 0, 31);
|
andrewm@0
|
286 keyboard_.midiOutputController()->sendNoteOff(0, 0, 36);
|
andrewm@0
|
287 }
|
andrewm@0
|
288 else if(releaseAngle < -1.5) {
|
andrewm@0
|
289 keyboard_.midiOutputController()->sendNoteOn(0, 0, 36, 64);
|
andrewm@0
|
290 keyboard_.midiOutputController()->sendNoteOn(0, 0, 33, 80);
|
andrewm@0
|
291 keyboard_.midiOutputController()->sendNoteOff(0, 0, 33);
|
andrewm@0
|
292 keyboard_.midiOutputController()->sendNoteOff(0, 0, 36);
|
andrewm@0
|
293 }
|
andrewm@0
|
294 }
|
andrewm@0
|
295 #elif defined(TRUMPET)
|
andrewm@0
|
296 if(keyboard_.midiOutputController() != 0) {
|
andrewm@0
|
297 if(releaseAngle > 1.0) {
|
andrewm@0
|
298 keyboard_.midiOutputController()->sendNoteOn(0, 0, 48, 64);
|
andrewm@0
|
299 keyboard_.midiOutputController()->sendNoteOn(0, 0, 42, 96);
|
andrewm@0
|
300 //keyboard_.midiOutputController()->sendNoteOff(0, 0, 42);
|
andrewm@0
|
301 keyboard_.midiOutputController()->sendNoteOff(0, 0, 48);
|
andrewm@0
|
302 keyboard_.scheduleEvent(this, boost::bind(&TouchkeyReleaseAngleMapping::releaseKeySwitch, this),
|
andrewm@0
|
303 keyboard_.schedulerCurrentTimestamp() + milliseconds_to_timestamp(250));
|
andrewm@0
|
304 }
|
andrewm@0
|
305 else if(releaseAngle < -1.5) {
|
andrewm@0
|
306 keyboard_.midiOutputController()->sendNoteOn(0, 0, 48, 64);
|
andrewm@0
|
307 keyboard_.midiOutputController()->sendNoteOn(0, 0, 46, 96);
|
andrewm@0
|
308 //keyboard_.midiOutputController()->sendNoteOff(0, 0, 47);
|
andrewm@0
|
309 keyboard_.midiOutputController()->sendNoteOff(0, 0, 48);
|
andrewm@0
|
310 keyboard_.scheduleEvent(this, boost::bind(&TouchkeyReleaseAngleMapping::releaseKeySwitch, this),
|
andrewm@0
|
311 keyboard_.schedulerCurrentTimestamp() + milliseconds_to_timestamp(250));
|
andrewm@0
|
312 }
|
andrewm@0
|
313 else {
|
andrewm@0
|
314 // Check if we're suppose to clean up now
|
andrewm@0
|
315 finished_ = true;
|
andrewm@0
|
316 if(finishRequested_)
|
andrewm@0
|
317 acknowledgeFinish();
|
andrewm@0
|
318 }
|
andrewm@0
|
319 }
|
andrewm@0
|
320 #endif
|
andrewm@0
|
321 }
|
andrewm@0
|
322 }
|
andrewm@0
|
323
|
andrewm@0
|
324 timestamp_type TouchkeyReleaseAngleMapping::releaseKeySwitch() {
|
andrewm@0
|
325 keyboard_.midiOutputController()->sendNoteOff(0, 0, 42);
|
andrewm@0
|
326 keyboard_.midiOutputController()->sendNoteOff(0, 0, 46);
|
andrewm@0
|
327
|
andrewm@0
|
328 // Check if we're suppose to clean up now
|
andrewm@0
|
329 /*finished_ = true;
|
andrewm@0
|
330 if(finishRequested_)
|
andrewm@0
|
331 acknowledgeFinish();*/
|
andrewm@0
|
332
|
andrewm@0
|
333 return 0;
|
andrewm@0
|
334 } |