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 TouchkeyOnsetAngleMapping.cpp: per-note mapping for the onset angle mapping,
|
andrewm@0
|
21 which measures the speed of finger motion along the key surface at the
|
andrewm@0
|
22 time of MIDI note onset.
|
andrewm@0
|
23 */
|
andrewm@0
|
24
|
andrewm@0
|
25 #include "TouchkeyOnsetAngleMapping.h"
|
andrewm@0
|
26 #include "../MappingFactory.h"
|
andrewm@0
|
27
|
andrewm@0
|
28 #define DEBUG_NOTE_ONSET_MAPPING
|
andrewm@0
|
29
|
andrewm@0
|
30 // Class constants
|
andrewm@0
|
31 const int TouchkeyOnsetAngleMapping::kDefaultFilterBufferLength = 30;
|
andrewm@0
|
32 const timestamp_diff_type TouchkeyOnsetAngleMapping::kDefaultMaxLookbackTime = milliseconds_to_timestamp(100);
|
andrewm@0
|
33 const int TouchkeyOnsetAngleMapping::kDefaultMaxLookbackSamples = 3;
|
andrewm@0
|
34
|
andrewm@0
|
35 // Main constructor takes references/pointers from objects which keep track
|
andrewm@0
|
36 // of touch location, continuous key position and the state detected from that
|
andrewm@0
|
37 // position. The PianoKeyboard object is strictly required as it gives access to
|
andrewm@0
|
38 // Scheduler and OSC methods. The others are optional since any given system may
|
andrewm@0
|
39 // contain only one of continuous key position or touch sensitivity
|
andrewm@0
|
40 TouchkeyOnsetAngleMapping::TouchkeyOnsetAngleMapping(PianoKeyboard &keyboard, MappingFactory *factory, int noteNumber, Node<KeyTouchFrame>* touchBuffer,
|
andrewm@0
|
41 Node<key_position>* positionBuffer, KeyPositionTracker* positionTracker)
|
andrewm@0
|
42 : TouchkeyBaseMapping(keyboard, factory, noteNumber, touchBuffer, positionBuffer, positionTracker),
|
andrewm@0
|
43 pastSamples_(kDefaultFilterBufferLength), maxLookbackTime_(kDefaultMaxLookbackTime),
|
andrewm@0
|
44 startingPitchBendSemitones_(0), lastPitchBendSemitones_(0),
|
andrewm@0
|
45 rampBeginTime_(missing_value<timestamp_type>::missing()), rampLength_(0)
|
andrewm@0
|
46 {
|
andrewm@0
|
47 }
|
andrewm@0
|
48
|
andrewm@0
|
49 // Reset state back to defaults
|
andrewm@0
|
50 void TouchkeyOnsetAngleMapping::reset() {
|
andrewm@0
|
51 ScopedLock sl(sampleBufferMutex_);
|
andrewm@0
|
52
|
andrewm@0
|
53 TouchkeyBaseMapping::reset();
|
andrewm@0
|
54 pastSamples_.clear();
|
andrewm@0
|
55 }
|
andrewm@0
|
56
|
andrewm@0
|
57 // Resend all current parameters
|
andrewm@0
|
58 void TouchkeyOnsetAngleMapping::resend() {
|
andrewm@0
|
59 // Message is only sent at release; resend may not apply here.
|
andrewm@0
|
60 }
|
andrewm@0
|
61
|
andrewm@0
|
62 // This method receives data from the touch buffer or possibly the continuous key angle (not used here)
|
andrewm@0
|
63 void TouchkeyOnsetAngleMapping::triggerReceived(TriggerSource* who, timestamp_type timestamp) {
|
andrewm@0
|
64 if(who == touchBuffer_) {
|
andrewm@0
|
65 ScopedLock sl(sampleBufferMutex_);
|
andrewm@0
|
66
|
andrewm@0
|
67 // Save the latest frame, even if it is an empty touch (we need to know what happened even
|
andrewm@0
|
68 // after the touch ends since the MIDI off may come later)
|
andrewm@0
|
69 if(!touchBuffer_->empty())
|
andrewm@0
|
70 pastSamples_.insert(touchBuffer_->latest(), touchBuffer_->latestTimestamp());
|
andrewm@0
|
71 }
|
andrewm@0
|
72 }
|
andrewm@0
|
73
|
andrewm@0
|
74 // Mapping method. This actually does the real work of sending OSC data in response to the
|
andrewm@0
|
75 // latest information from the touch sensors or continuous key angle
|
andrewm@0
|
76 timestamp_type TouchkeyOnsetAngleMapping::performMapping() {
|
andrewm@0
|
77 timestamp_type currentTimestamp = keyboard_.schedulerCurrentTimestamp();
|
andrewm@0
|
78
|
andrewm@0
|
79 if(rampLength_ != 0 && currentTimestamp <= rampBeginTime_ + rampLength_) {
|
andrewm@0
|
80 float rampValue = 1.0 - (float)(currentTimestamp - rampBeginTime_)/(float)rampLength_;
|
andrewm@0
|
81
|
andrewm@0
|
82 lastPitchBendSemitones_ = startingPitchBendSemitones_ * rampValue;
|
andrewm@0
|
83 #ifdef DEBUG_NOTE_ONSET_MAPPING
|
andrewm@0
|
84 std::cout << "onset pitch = " << lastPitchBendSemitones_ << endl;
|
andrewm@0
|
85 #endif
|
andrewm@0
|
86 sendPitchBendMessage(lastPitchBendSemitones_);
|
andrewm@0
|
87 }
|
andrewm@0
|
88 else if(lastPitchBendSemitones_ != 0) {
|
andrewm@0
|
89 lastPitchBendSemitones_ = 0;
|
andrewm@0
|
90 #ifdef DEBUG_NOTE_ONSET_MAPPING
|
andrewm@0
|
91 std::cout << "onset pitch = " << lastPitchBendSemitones_ << endl;
|
andrewm@0
|
92 #endif
|
andrewm@0
|
93 sendPitchBendMessage(lastPitchBendSemitones_);
|
andrewm@0
|
94 }
|
andrewm@0
|
95
|
andrewm@0
|
96 // Register for the next update by returning its timestamp
|
andrewm@0
|
97 nextScheduledTimestamp_ = currentTimestamp + updateInterval_;
|
andrewm@0
|
98 return nextScheduledTimestamp_;
|
andrewm@0
|
99 }
|
andrewm@0
|
100
|
andrewm@0
|
101 void TouchkeyOnsetAngleMapping::processOnset(timestamp_type timestamp) {
|
andrewm@0
|
102
|
andrewm@0
|
103 sampleBufferMutex_.enter();
|
andrewm@0
|
104
|
andrewm@0
|
105 // Look backwards from the current timestamp to find the velocity
|
andrewm@0
|
106 float calculatedVelocity = missing_value<float>::missing();
|
andrewm@0
|
107 bool touchWasOn = false;
|
andrewm@0
|
108 int sampleCount = 0;
|
andrewm@0
|
109
|
andrewm@0
|
110 #ifdef DEBUG_NOTE_ONSET_MAPPING
|
andrewm@0
|
111 std::cout << "processOnset begin = " << pastSamples_.beginIndex() << " end = " << pastSamples_.endIndex() << "\n";
|
andrewm@0
|
112 #endif
|
andrewm@0
|
113
|
andrewm@0
|
114 if(!pastSamples_.empty()) {
|
andrewm@0
|
115 Node<KeyTouchFrame>::size_type index = pastSamples_.endIndex() - 1;
|
andrewm@0
|
116 Node<KeyTouchFrame>::size_type mostRecentTouchPresentIndex = pastSamples_.endIndex() - 1;
|
andrewm@0
|
117 while(index >= pastSamples_.beginIndex()) {
|
andrewm@0
|
118 #ifdef DEBUG_NOTE_ONSET_MAPPING
|
andrewm@0
|
119 std::cout << "examining sample " << index << " with " << pastSamples_[index].count << " touches and time diff " << timestamp - pastSamples_.timestampAt(index) << "\n";
|
andrewm@0
|
120 #endif
|
andrewm@0
|
121 if(timestamp - pastSamples_.timestampAt(index) >= maxLookbackTime_)
|
andrewm@0
|
122 break;
|
andrewm@0
|
123 if(pastSamples_[index].count == 0) {
|
andrewm@0
|
124 if(touchWasOn) {
|
andrewm@0
|
125 // We found a break in the touch; stop here. But don't stop
|
andrewm@0
|
126 // if the first frames we consider have no touches.
|
andrewm@0
|
127 if(index < pastSamples_.endIndex() - 1)
|
andrewm@0
|
128 index++;
|
andrewm@0
|
129 break;
|
andrewm@0
|
130 }
|
andrewm@0
|
131 }
|
andrewm@0
|
132 else if(!touchWasOn) {
|
andrewm@0
|
133 mostRecentTouchPresentIndex = index;
|
andrewm@0
|
134 touchWasOn = true;
|
andrewm@0
|
135 }
|
andrewm@0
|
136 if(sampleCount++ >= kDefaultMaxLookbackSamples)
|
andrewm@0
|
137 break;
|
andrewm@0
|
138
|
andrewm@0
|
139 // Can't decrement past 0 in an unsigned type
|
andrewm@0
|
140 if(index == 0)
|
andrewm@0
|
141 break;
|
andrewm@0
|
142 index--;
|
andrewm@0
|
143 }
|
andrewm@0
|
144
|
andrewm@0
|
145 // If we fell off the beginning of the buffer, back up.
|
andrewm@0
|
146 if(index < pastSamples_.beginIndex())
|
andrewm@0
|
147 index = pastSamples_.beginIndex();
|
andrewm@0
|
148
|
andrewm@0
|
149 // Need at least two points for this calculation to work
|
andrewm@0
|
150 timestamp_type endingTimestamp = pastSamples_.timestampAt(mostRecentTouchPresentIndex);
|
andrewm@0
|
151 timestamp_type startingTimestamp = pastSamples_.timestampAt(index);
|
andrewm@0
|
152 if(endingTimestamp - startingTimestamp > 0) {
|
andrewm@0
|
153 float endingPosition = pastSamples_[mostRecentTouchPresentIndex].locs[0];
|
andrewm@0
|
154 float startingPosition = pastSamples_[index].locs[0];
|
andrewm@0
|
155 calculatedVelocity = (endingPosition - startingPosition) / (endingTimestamp - startingTimestamp);
|
andrewm@0
|
156 }
|
andrewm@0
|
157 else { // DEBUG
|
andrewm@0
|
158 #ifdef DEBUG_NOTE_ONSET_MAPPING
|
andrewm@0
|
159 std::cout << "Found 0 timestamp difference on key onset (indices " << index << " and " << pastSamples_.endIndex() - 1 << "\n";
|
andrewm@0
|
160 #endif
|
andrewm@0
|
161 }
|
andrewm@0
|
162 }
|
andrewm@0
|
163 else {
|
andrewm@0
|
164 #ifdef DEBUG_NOTE_ONSET_MAPPING
|
andrewm@0
|
165 std::cout << "Found empty touch buffer on key onset\n";
|
andrewm@0
|
166 #endif
|
andrewm@0
|
167 }
|
andrewm@0
|
168
|
andrewm@0
|
169 sampleBufferMutex_.exit();
|
andrewm@0
|
170
|
andrewm@0
|
171 if(!missing_value<float>::isMissing(calculatedVelocity)) {
|
andrewm@0
|
172 #ifdef DEBUG_NOTE_ONSET_MAPPING
|
andrewm@0
|
173 std::cout << "Found onset velocity " << calculatedVelocity << " on note " << noteNumber_ << std::endl;
|
andrewm@0
|
174 #endif
|
andrewm@0
|
175 if(calculatedVelocity > 6.0)
|
andrewm@0
|
176 calculatedVelocity = 6.0;
|
andrewm@0
|
177
|
andrewm@0
|
178 if(calculatedVelocity > 1.5) {
|
andrewm@0
|
179 startingPitchBendSemitones_ = -1.0 * (calculatedVelocity / 5.0);
|
andrewm@0
|
180 rampLength_ = milliseconds_to_timestamp((50.0 + calculatedVelocity*25.0));
|
andrewm@0
|
181 rampBeginTime_ = keyboard_.schedulerCurrentTimestamp();
|
andrewm@0
|
182 }
|
andrewm@0
|
183 else
|
andrewm@0
|
184 rampLength_ = 0;
|
andrewm@0
|
185
|
andrewm@0
|
186 sendOnsetAngleMessage(calculatedVelocity);
|
andrewm@0
|
187 }
|
andrewm@0
|
188 }
|
andrewm@0
|
189
|
andrewm@0
|
190 void TouchkeyOnsetAngleMapping::sendOnsetAngleMessage(float onsetAngle, bool force) {
|
andrewm@0
|
191 if(force || !suspended_) {
|
andrewm@0
|
192 keyboard_.sendMessage("/touchkeys/onsetangle", "if", noteNumber_, onsetAngle, LO_ARGS_END);
|
andrewm@0
|
193 }
|
andrewm@0
|
194 }
|
andrewm@0
|
195
|
andrewm@0
|
196 // Send the pitch bend message of a given number of a semitones. Send by OSC,
|
andrewm@0
|
197 // which can be mapped to MIDI CC externally
|
andrewm@0
|
198 void TouchkeyOnsetAngleMapping::sendPitchBendMessage(float pitchBendSemitones, bool force) {
|
andrewm@0
|
199 if(force || !suspended_)
|
andrewm@0
|
200 keyboard_.sendMessage("/touchkeys/scoop", "if", noteNumber_, pitchBendSemitones, LO_ARGS_END);
|
andrewm@0
|
201 }
|