andrewm@9
|
1 /*
|
andrewm@9
|
2 TouchKeys: multi-touch musical keyboard control software
|
andrewm@9
|
3 Copyright (c) 2013 Andrew McPherson
|
andrewm@9
|
4
|
andrewm@9
|
5 This program is free software: you can redistribute it and/or modify
|
andrewm@9
|
6 it under the terms of the GNU General Public License as published by
|
andrewm@9
|
7 the Free Software Foundation, either version 3 of the License, or
|
andrewm@9
|
8 (at your option) any later version.
|
andrewm@9
|
9
|
andrewm@9
|
10 This program is distributed in the hope that it will be useful,
|
andrewm@9
|
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
|
andrewm@9
|
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
andrewm@9
|
13 GNU General Public License for more details.
|
andrewm@9
|
14
|
andrewm@9
|
15 You should have received a copy of the GNU General Public License
|
andrewm@9
|
16 along with this program. If not, see <http://www.gnu.org/licenses/>.
|
andrewm@9
|
17
|
andrewm@9
|
18 =====================================================================
|
andrewm@9
|
19
|
andrewm@9
|
20 TouchkeyOscEmulator.cpp: emulates a TouchKeys source using OSC messages
|
andrewm@9
|
21 */
|
andrewm@9
|
22
|
andrewm@9
|
23 #include "TouchkeyOscEmulator.h"
|
andrewm@9
|
24 #include <cstdlib>
|
andrewm@9
|
25 #include <cstring>
|
andrewm@9
|
26
|
andrewm@9
|
27 // Main constructor
|
andrewm@9
|
28 TouchkeyOscEmulator::TouchkeyOscEmulator(PianoKeyboard& keyboard, OscMessageSource& messageSource)
|
andrewm@9
|
29 : keyboard_(keyboard), source_(messageSource), lowestMidiNote_(48)
|
andrewm@9
|
30 {
|
andrewm@9
|
31 allTouchesOff(false);
|
andrewm@9
|
32 setOscController(&source_);
|
andrewm@9
|
33 addOscListener("/emulation0*");
|
andrewm@9
|
34 }
|
andrewm@9
|
35
|
andrewm@9
|
36 // Main destructor
|
andrewm@9
|
37 TouchkeyOscEmulator::~TouchkeyOscEmulator()
|
andrewm@9
|
38 {
|
andrewm@9
|
39 }
|
andrewm@9
|
40
|
andrewm@9
|
41 // Turn off all touches on the virtual TouchKeys keyboard
|
andrewm@9
|
42 // send indicates whether to send messages for touches that are disabled
|
andrewm@9
|
43 // (false silently resets state)
|
andrewm@9
|
44 void TouchkeyOscEmulator::allTouchesOff(bool send) {
|
andrewm@9
|
45 for(int i = 0; i < 127; i++) {
|
andrewm@9
|
46 if(send && touchFrames_[i].count > 0) {
|
andrewm@9
|
47 if(keyboard_.key(i) != 0)
|
andrewm@9
|
48 keyboard_.key(i)->touchOff(keyboard_.schedulerCurrentTimestamp());
|
andrewm@9
|
49 }
|
andrewm@9
|
50 clearTouchData(i);
|
andrewm@9
|
51 }
|
andrewm@9
|
52 }
|
andrewm@9
|
53
|
andrewm@9
|
54 // OSC handler method, called when a registered message is received
|
andrewm@9
|
55 bool TouchkeyOscEmulator::oscHandlerMethod(const char *path, const char *types,
|
andrewm@9
|
56 int numValues, lo_arg **values, void *data) {
|
andrewm@9
|
57 // Parse the OSC path looking for particular emulation messages
|
andrewm@9
|
58 if(!strncmp(path, "/emulation0/note", 16) && strlen(path) > 16) {
|
andrewm@9
|
59 // Emulation0 messages are of this form:
|
andrewm@9
|
60 // noteN/M
|
andrewm@9
|
61 // noteN/M/z
|
andrewm@9
|
62 // Here, N specifies the number of the note (with 0 being the lowest on the controller)
|
andrewm@9
|
63 // and M specifies the touch number (starting at 1 for the first touch). The messages ending
|
andrewm@9
|
64 // in /z indicate a touch on-off event for that particular touch.
|
andrewm@9
|
65 std::string subpath(&path[16]);
|
andrewm@9
|
66 int separatorLoc = subpath.find_first_of('/');
|
andrewm@9
|
67 if(separatorLoc == std::string::npos || separatorLoc == subpath.length() - 1) {
|
andrewm@9
|
68 // Malformed input (no slash or it's the last character): ignore
|
andrewm@9
|
69 return false;
|
andrewm@9
|
70 }
|
andrewm@9
|
71 const char *noteNumberStr = subpath.substr(0, separatorLoc).c_str();
|
andrewm@9
|
72 if(noteNumberStr == 0) // Malformed input
|
andrewm@9
|
73 return false;
|
andrewm@9
|
74 int noteNumber = atoi(noteNumberStr);
|
andrewm@9
|
75 if(noteNumber < 0) // Unknown note number
|
andrewm@9
|
76 return false;
|
andrewm@9
|
77 // Now we have a note number from the OSC path
|
andrewm@9
|
78 // Figure out the touch number, starting after the separator
|
andrewm@9
|
79 subpath = subpath.substr(separatorLoc + 1);
|
andrewm@9
|
80 separatorLoc = subpath.find_first_of("/z");
|
andrewm@9
|
81 bool isZ = false;
|
andrewm@9
|
82 if(separatorLoc != std::string::npos) {
|
andrewm@9
|
83 // This is a z message; drop the last part
|
andrewm@9
|
84 isZ = true;
|
andrewm@9
|
85 subpath = subpath.substr(0, separatorLoc);
|
andrewm@9
|
86 }
|
andrewm@9
|
87 int touchNumber = atoi(subpath.c_str());
|
andrewm@9
|
88
|
andrewm@9
|
89 // We only care about touch numbers 1-3, since we're emulating the capabilities
|
andrewm@9
|
90 // of the TouchKeys
|
andrewm@9
|
91 if(touchNumber < 1 || touchNumber > 3)
|
andrewm@9
|
92 return false;
|
andrewm@9
|
93
|
andrewm@9
|
94 if(isZ) {
|
andrewm@9
|
95 // Z messages indicate touch on/off. We only respond specifically
|
andrewm@9
|
96 // to the off message: the on message is implicit in receiving XY data
|
andrewm@9
|
97 if(numValues >= 1) {
|
andrewm@9
|
98 if(types[0] == 'i') {
|
andrewm@9
|
99 if(values[0]->i == 0)
|
andrewm@9
|
100 touchOffReceived(noteNumber, touchNumber);
|
andrewm@9
|
101 }
|
andrewm@9
|
102 else if(types[0] == 'f') {
|
andrewm@9
|
103 if(values[0]->f == 0)
|
andrewm@9
|
104 touchOffReceived(noteNumber, touchNumber);
|
andrewm@9
|
105 }
|
andrewm@9
|
106 }
|
andrewm@9
|
107 }
|
andrewm@9
|
108 else {
|
andrewm@9
|
109 // Other messages contain XY data for the given touch, but with Y first as
|
andrewm@9
|
110 // the layout is turned sideways (landscape)
|
andrewm@9
|
111 if(numValues >= 2) {
|
andrewm@9
|
112 if(types[0] == 'f' && types[1] == 'f')
|
andrewm@9
|
113 touchReceived(noteNumber, touchNumber, values[1]->f, values[0]->f);
|
andrewm@9
|
114 }
|
andrewm@9
|
115 }
|
andrewm@9
|
116 }
|
andrewm@9
|
117
|
andrewm@9
|
118 return true;
|
andrewm@9
|
119 }
|
andrewm@9
|
120
|
andrewm@9
|
121 // New touch data point received
|
andrewm@9
|
122 void TouchkeyOscEmulator::touchReceived(int key, int touch, float x, float y) {
|
andrewm@11
|
123 // std::cout << "Key " << key << " touch " << touch << ": (" << x << ", " << y << ")\n";
|
andrewm@9
|
124
|
andrewm@9
|
125 int noteNumber = lowestMidiNote_ + key;
|
andrewm@9
|
126
|
andrewm@9
|
127 // Sanity checks
|
andrewm@9
|
128 if(noteNumber < 0 || noteNumber > 127)
|
andrewm@9
|
129 return;
|
andrewm@9
|
130 if(touch < 1 || touch > 3)
|
andrewm@9
|
131 return;
|
andrewm@9
|
132 if(y < 0 || y > 1.0 || x > 1.0) // Okay for x < 0
|
andrewm@9
|
133 return;
|
andrewm@9
|
134
|
andrewm@9
|
135 // Find TouchKeys ID associated with this OSC touch ID
|
andrewm@9
|
136 int touchId = touchIdAssignments_[noteNumber][touch - 1];
|
andrewm@9
|
137 bool updatedExistingTouch = false;
|
andrewm@9
|
138
|
andrewm@9
|
139 if(touchId >= 0) {
|
andrewm@9
|
140 for(int i = 0; i < 3; i++) {
|
andrewm@9
|
141 if(touchFrames_[noteNumber].ids[i] == touchId) {
|
andrewm@9
|
142 // Found continuing touch
|
andrewm@11
|
143 // std::cout << "matched touch " << touch << " to ID " << touchId << std::endl;
|
andrewm@9
|
144 updateTouchInFrame(noteNumber, i, x, y);
|
andrewm@9
|
145 updatedExistingTouch = true;
|
andrewm@9
|
146 }
|
andrewm@9
|
147 }
|
andrewm@9
|
148 }
|
andrewm@9
|
149
|
andrewm@9
|
150 if(!updatedExistingTouch) {
|
andrewm@9
|
151 // Didn't find an ID for this touch: add it to the frame
|
andrewm@9
|
152 // provided there aren't 3 existing touches (shouldn't happen)
|
andrewm@9
|
153 if(touchFrames_[noteNumber].count < 3) {
|
andrewm@11
|
154 // std::cout << "assigning touch " << touch << " to ID " << touchFrames_[noteNumber].nextId << std::endl;
|
andrewm@9
|
155 touchIdAssignments_[noteNumber][touch - 1] = touchFrames_[noteNumber].nextId++;
|
andrewm@9
|
156 addTouchToFrame(noteNumber, touchIdAssignments_[noteNumber][touch - 1], x, y);
|
andrewm@9
|
157 }
|
andrewm@9
|
158 }
|
andrewm@9
|
159
|
andrewm@10
|
160 if(keyboard_.key(noteNumber) != 0) {
|
andrewm@10
|
161 // Pass the frame to the keyboard by copy since PianoKey does its own ID number tracking.
|
andrewm@10
|
162 // The important thing is that the Y values are always ordered. If ID tracking later changes
|
andrewm@10
|
163 // in PianoKey it's of no consequence here as long as we retain the ability to track OSC
|
andrewm@10
|
164 // touch IDs.
|
andrewm@10
|
165 KeyTouchFrame copyFrame(touchFrames_[noteNumber]);
|
andrewm@10
|
166 keyboard_.key(noteNumber)->touchInsertFrame(copyFrame, keyboard_.schedulerCurrentTimestamp());
|
andrewm@10
|
167 }
|
andrewm@9
|
168 }
|
andrewm@9
|
169
|
andrewm@9
|
170 // Touch removed
|
andrewm@9
|
171 void TouchkeyOscEmulator::touchOffReceived(int key, int touch) {
|
andrewm@9
|
172 int noteNumber = lowestMidiNote_ + key;
|
andrewm@9
|
173
|
andrewm@9
|
174 // Sanity checks
|
andrewm@9
|
175 if(noteNumber < 0 || noteNumber > 127)
|
andrewm@9
|
176 return;
|
andrewm@9
|
177 if(touch < 1 || touch > 3)
|
andrewm@9
|
178 return;
|
andrewm@9
|
179
|
andrewm@9
|
180 // Find TouchKeys ID associated with this OSC touch ID and
|
andrewm@9
|
181 // meanwhile disassociate this touch with any future touch ID
|
andrewm@9
|
182 int touchId = touchIdAssignments_[noteNumber][touch - 1];
|
andrewm@9
|
183 touchIdAssignments_[noteNumber][touch - 1] = -1;
|
andrewm@9
|
184
|
andrewm@9
|
185 if(touchId < 0) // No known touch with this ID
|
andrewm@9
|
186 return;
|
andrewm@9
|
187
|
andrewm@9
|
188 for(int i = 0; i < 3; i++) {
|
andrewm@9
|
189 if(touchFrames_[noteNumber].ids[i] == touchId) {
|
andrewm@9
|
190 // Found the touch: remove it
|
andrewm@9
|
191 removeTouchFromFrame(noteNumber, i);
|
andrewm@9
|
192
|
andrewm@9
|
193 // Anything left?
|
andrewm@9
|
194 if(touchFrames_[noteNumber].count == 0) {
|
andrewm@9
|
195 clearTouchData(noteNumber);
|
andrewm@9
|
196 if(keyboard_.key(noteNumber) != 0)
|
andrewm@9
|
197 keyboard_.key(noteNumber)->touchOff(keyboard_.schedulerCurrentTimestamp());
|
andrewm@9
|
198 }
|
andrewm@10
|
199 else if(keyboard_.key(noteNumber) != 0) {
|
andrewm@10
|
200 KeyTouchFrame copyFrame(touchFrames_[noteNumber]);
|
andrewm@10
|
201 keyboard_.key(noteNumber)->touchInsertFrame(copyFrame, keyboard_.schedulerCurrentTimestamp());
|
andrewm@10
|
202 }
|
andrewm@9
|
203 break;
|
andrewm@9
|
204 }
|
andrewm@9
|
205 }
|
andrewm@9
|
206 }
|
andrewm@9
|
207
|
andrewm@9
|
208 // Clear touch data for a particular key
|
andrewm@9
|
209 void TouchkeyOscEmulator::clearTouchData(int i) {
|
andrewm@9
|
210 touchFrames_[i].count = 0;
|
andrewm@9
|
211 touchFrames_[i].locH = -1.0;
|
andrewm@9
|
212 touchFrames_[i].nextId = 0;
|
andrewm@9
|
213 for(int j = 0; j < 3; j++) {
|
andrewm@9
|
214 touchFrames_[i].ids[j] = -1;
|
andrewm@9
|
215 touchFrames_[i].locs[j] = -1.0;
|
andrewm@9
|
216 touchFrames_[i].sizes[j] = 0;
|
andrewm@9
|
217 touchIdAssignments_[i][j] = -1;
|
andrewm@9
|
218 }
|
andrewm@9
|
219 int key = i % 12;
|
andrewm@9
|
220 if(key == 1 || key == 3 || key == 6 || key == 8 || key == 10)
|
andrewm@9
|
221 touchFrames_[i].white = false;
|
andrewm@9
|
222 else
|
andrewm@9
|
223 touchFrames_[i].white = true;
|
andrewm@9
|
224 }
|
andrewm@9
|
225
|
andrewm@9
|
226 void TouchkeyOscEmulator::removeTouchFromFrame(int note, int index) {
|
andrewm@9
|
227 // Remove the touch and collapse the other touches around it down
|
andrewm@9
|
228 // so they stay in order.
|
andrewm@9
|
229 int lastTouchIndex = touchFrames_[note].count - 1;
|
andrewm@9
|
230 if(lastTouchIndex < 0)
|
andrewm@9
|
231 return;
|
andrewm@9
|
232
|
andrewm@9
|
233 for(int i = index; i < lastTouchIndex; i++) {
|
andrewm@9
|
234 touchFrames_[note].ids[i] = touchFrames_[note].ids[i+1];
|
andrewm@9
|
235 touchFrames_[note].locs[i] = touchFrames_[note].locs[i+1];
|
andrewm@9
|
236 touchFrames_[note].sizes[i] = touchFrames_[note].sizes[i+1];
|
andrewm@9
|
237 }
|
andrewm@9
|
238
|
andrewm@9
|
239 touchFrames_[note].ids[lastTouchIndex] = -1;
|
andrewm@9
|
240 touchFrames_[note].locs[lastTouchIndex] = -1.0;
|
andrewm@9
|
241 touchFrames_[note].sizes[lastTouchIndex] = 0.0;
|
andrewm@9
|
242 touchFrames_[note].count--;
|
andrewm@9
|
243 }
|
andrewm@9
|
244
|
andrewm@9
|
245 // Add a touch with the indicated ID to the frame. Its position in the KeyTouchFrame
|
andrewm@9
|
246 // order may depend on its y value
|
andrewm@9
|
247 void TouchkeyOscEmulator::addTouchToFrame(int note, int touchId, float x, float y) {
|
andrewm@9
|
248 if(touchFrames_[note].count >= 3) // Already full?
|
andrewm@9
|
249 return;
|
andrewm@9
|
250
|
andrewm@9
|
251 // Touches are ordered by y position. Look for where this fits in the sequence
|
andrewm@9
|
252 // and insert it there.
|
andrewm@9
|
253 int insertLoc;
|
andrewm@9
|
254 for(insertLoc = 0; insertLoc < touchFrames_[note].count; insertLoc++) {
|
andrewm@9
|
255 if(touchFrames_[note].locs[insertLoc] > y)
|
andrewm@9
|
256 break;
|
andrewm@9
|
257 }
|
andrewm@9
|
258
|
andrewm@9
|
259 // Move the other touches back to make space
|
andrewm@9
|
260 for(int i = touchFrames_[note].count; i > insertLoc; i--) {
|
andrewm@9
|
261 touchFrames_[note].ids[i] = touchFrames_[note].ids[i-1];
|
andrewm@9
|
262 touchFrames_[note].locs[i] = touchFrames_[note].locs[i-1];
|
andrewm@9
|
263 touchFrames_[note].sizes[i] = touchFrames_[note].sizes[i-1];
|
andrewm@9
|
264 }
|
andrewm@9
|
265
|
andrewm@9
|
266 // Add the new touch in the vacant spot
|
andrewm@9
|
267 touchFrames_[note].ids[insertLoc] = touchId;
|
andrewm@9
|
268 touchFrames_[note].locs[insertLoc] = y;
|
andrewm@9
|
269 touchFrames_[note].sizes[insertLoc] = 1.0; // Size is not supported over OSC emulation
|
andrewm@11
|
270 touchFrames_[note].count++;
|
andrewm@9
|
271
|
andrewm@9
|
272 // Lowest touch controls the horizontal position
|
andrewm@12
|
273 if(insertLoc == 0) {
|
andrewm@12
|
274 // Emulate the partial X sensing of the white TouchKeys
|
andrewm@12
|
275 if(touchFrames_[note].locs[0] > kWhiteFrontBackCutoff && touchFrames_[note].white)
|
andrewm@12
|
276 touchFrames_[note].locH = -1.0;
|
andrewm@12
|
277 else
|
andrewm@12
|
278 touchFrames_[note].locH = x;
|
andrewm@12
|
279 }
|
andrewm@9
|
280 }
|
andrewm@9
|
281
|
andrewm@9
|
282 // Update the touch at the given index to the new value. Depending on the values, it
|
andrewm@9
|
283 // may be necessary to reorder the touches to keep them in order of increasing Y value.
|
andrewm@9
|
284 void TouchkeyOscEmulator::updateTouchInFrame(int note, int index, float x, float y) {
|
andrewm@9
|
285 // Is it still in proper order?
|
andrewm@9
|
286 bool ordered = true;
|
andrewm@9
|
287 if(index > 0) {
|
andrewm@9
|
288 if(touchFrames_[note].locs[index-1] > y)
|
andrewm@9
|
289 ordered = false;
|
andrewm@9
|
290 }
|
andrewm@9
|
291 if(index < touchFrames_[note].count - 1) {
|
andrewm@9
|
292 if(touchFrames_[note].locs[index + 1] < y)
|
andrewm@9
|
293 ordered = false;
|
andrewm@9
|
294 }
|
andrewm@9
|
295
|
andrewm@9
|
296 // If out of order, the simplest strategy is to remove the touch and re-add it which
|
andrewm@9
|
297 // will keep everything in order. Otherwise just update the information
|
andrewm@9
|
298 if(ordered) {
|
andrewm@9
|
299 touchFrames_[note].locs[index] = y;
|
andrewm@12
|
300 if(index == 0) {
|
andrewm@12
|
301 if(y > kWhiteFrontBackCutoff && touchFrames_[note].white)
|
andrewm@12
|
302 touchFrames_[note].locH = -1;
|
andrewm@12
|
303 else
|
andrewm@12
|
304 touchFrames_[note].locH = x;
|
andrewm@12
|
305 }
|
andrewm@9
|
306 }
|
andrewm@9
|
307 else {
|
andrewm@9
|
308 int currentId = touchFrames_[note].ids[index];
|
andrewm@9
|
309 removeTouchFromFrame(note, index);
|
andrewm@9
|
310 addTouchToFrame(note, currentId, x, y);
|
andrewm@9
|
311 }
|
andrewm@9
|
312 } |