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