andrewm@9: /* andrewm@9: TouchKeys: multi-touch musical keyboard control software andrewm@9: Copyright (c) 2013 Andrew McPherson andrewm@9: andrewm@9: This program is free software: you can redistribute it and/or modify andrewm@9: it under the terms of the GNU General Public License as published by andrewm@9: the Free Software Foundation, either version 3 of the License, or andrewm@9: (at your option) any later version. andrewm@9: andrewm@9: This program is distributed in the hope that it will be useful, andrewm@9: but WITHOUT ANY WARRANTY; without even the implied warranty of andrewm@9: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the andrewm@9: GNU General Public License for more details. andrewm@9: andrewm@9: You should have received a copy of the GNU General Public License andrewm@9: along with this program. If not, see . andrewm@9: andrewm@9: ===================================================================== andrewm@9: andrewm@9: TouchkeyOscEmulator.cpp: emulates a TouchKeys source using OSC messages andrewm@9: */ andrewm@9: andrewm@9: #include andrewm@9: #include andrewm@44: #include andrewm@20: #include "TouchkeyOscEmulator.h" andrewm@9: andrewm@9: // Main constructor andrewm@9: TouchkeyOscEmulator::TouchkeyOscEmulator(PianoKeyboard& keyboard, OscMessageSource& messageSource) andrewm@9: : keyboard_(keyboard), source_(messageSource), lowestMidiNote_(48) andrewm@9: { andrewm@9: allTouchesOff(false); andrewm@9: setOscController(&source_); andrewm@9: addOscListener("/emulation0*"); andrewm@9: } andrewm@9: andrewm@9: // Main destructor andrewm@9: TouchkeyOscEmulator::~TouchkeyOscEmulator() andrewm@9: { andrewm@9: } andrewm@9: andrewm@9: // Turn off all touches on the virtual TouchKeys keyboard andrewm@9: // send indicates whether to send messages for touches that are disabled andrewm@9: // (false silently resets state) andrewm@9: void TouchkeyOscEmulator::allTouchesOff(bool send) { andrewm@9: for(int i = 0; i < 127; i++) { andrewm@9: if(send && touchFrames_[i].count > 0) { andrewm@9: if(keyboard_.key(i) != 0) andrewm@9: keyboard_.key(i)->touchOff(keyboard_.schedulerCurrentTimestamp()); andrewm@9: } andrewm@9: clearTouchData(i); andrewm@9: } andrewm@9: } andrewm@9: andrewm@9: // OSC handler method, called when a registered message is received andrewm@9: bool TouchkeyOscEmulator::oscHandlerMethod(const char *path, const char *types, andrewm@9: int numValues, lo_arg **values, void *data) { andrewm@44: try { andrewm@44: // Parse the OSC path looking for particular emulation messages andrewm@44: if(!strncmp(path, "/emulation0/note", 16) && strlen(path) > 16) { andrewm@44: // Emulation0 messages are of this form: andrewm@44: // noteN/M andrewm@44: // noteN/M/z andrewm@44: // Here, N specifies the number of the note (with 0 being the lowest on the controller) andrewm@44: // and M specifies the touch number (starting at 1 for the first touch). The messages ending andrewm@44: // in /z indicate a touch on-off event for that particular touch. andrewm@44: std::string subpath(&path[16]); andrewm@44: int separatorLoc = subpath.find_first_of('/'); andrewm@44: if(separatorLoc == std::string::npos || separatorLoc == subpath.length() - 1) { andrewm@44: // Malformed input (no slash or it's the last character): ignore andrewm@44: return false; andrewm@44: } andrewm@44: std::stringstream noteNumberSStream(subpath.substr(0, separatorLoc)); andrewm@44: andrewm@44: int noteNumber = 0; andrewm@44: noteNumberSStream >> noteNumber; andrewm@44: andrewm@44: if(noteNumber < 0) // Unknown note number andrewm@44: return false; andrewm@44: // Now we have a note number from the OSC path andrewm@44: // Figure out the touch number, starting after the separator andrewm@44: subpath = subpath.substr(separatorLoc + 1); andrewm@44: separatorLoc = subpath.find_first_of("/z"); andrewm@44: bool isZ = false; andrewm@44: if(separatorLoc != std::string::npos) { andrewm@44: // This is a z message; drop the last part andrewm@44: isZ = true; andrewm@44: subpath = subpath.substr(0, separatorLoc); andrewm@44: } andrewm@44: andrewm@44: std::stringstream touchNumberSStream(subpath); andrewm@44: int touchNumber = 0; andrewm@44: touchNumberSStream >> touchNumber; andrewm@44: andrewm@44: // We only care about touch numbers 1-3, since we're emulating the capabilities andrewm@44: // of the TouchKeys andrewm@44: if(touchNumber < 1 || touchNumber > 3) andrewm@44: return false; andrewm@44: andrewm@44: if(isZ) { andrewm@44: // Z messages indicate touch on/off. We only respond specifically andrewm@44: // to the off message: the on message is implicit in receiving XY data andrewm@44: if(numValues >= 1) { andrewm@44: if(types[0] == 'i') { andrewm@44: if(values[0]->i == 0) andrewm@44: touchOffReceived(noteNumber, touchNumber); andrewm@44: } andrewm@44: else if(types[0] == 'f') { andrewm@44: if(values[0]->f == 0) andrewm@44: touchOffReceived(noteNumber, touchNumber); andrewm@44: } andrewm@9: } andrewm@44: } andrewm@44: else { andrewm@44: // Other messages contain XY data for the given touch, but with Y first as andrewm@44: // the layout is turned sideways (landscape) andrewm@44: if(numValues >= 2) { andrewm@44: if(types[0] == 'f' && types[1] == 'f') andrewm@44: touchReceived(noteNumber, touchNumber, values[1]->f, values[0]->f); andrewm@9: } andrewm@9: } andrewm@9: } andrewm@9: } andrewm@44: catch(...) { andrewm@44: return false; andrewm@44: } andrewm@44: andrewm@9: return true; andrewm@9: } andrewm@9: andrewm@9: // New touch data point received andrewm@9: void TouchkeyOscEmulator::touchReceived(int key, int touch, float x, float y) { andrewm@11: // std::cout << "Key " << key << " touch " << touch << ": (" << x << ", " << y << ")\n"; andrewm@9: andrewm@9: int noteNumber = lowestMidiNote_ + key; andrewm@9: andrewm@9: // Sanity checks andrewm@9: if(noteNumber < 0 || noteNumber > 127) andrewm@9: return; andrewm@9: if(touch < 1 || touch > 3) andrewm@9: return; andrewm@9: if(y < 0 || y > 1.0 || x > 1.0) // Okay for x < 0 andrewm@9: return; andrewm@9: andrewm@9: // Find TouchKeys ID associated with this OSC touch ID andrewm@9: int touchId = touchIdAssignments_[noteNumber][touch - 1]; andrewm@9: bool updatedExistingTouch = false; andrewm@9: andrewm@9: if(touchId >= 0) { andrewm@9: for(int i = 0; i < 3; i++) { andrewm@9: if(touchFrames_[noteNumber].ids[i] == touchId) { andrewm@9: // Found continuing touch andrewm@11: // std::cout << "matched touch " << touch << " to ID " << touchId << std::endl; andrewm@9: updateTouchInFrame(noteNumber, i, x, y); andrewm@9: updatedExistingTouch = true; andrewm@9: } andrewm@9: } andrewm@9: } andrewm@9: andrewm@9: if(!updatedExistingTouch) { andrewm@9: // Didn't find an ID for this touch: add it to the frame andrewm@9: // provided there aren't 3 existing touches (shouldn't happen) andrewm@9: if(touchFrames_[noteNumber].count < 3) { andrewm@11: // std::cout << "assigning touch " << touch << " to ID " << touchFrames_[noteNumber].nextId << std::endl; andrewm@9: touchIdAssignments_[noteNumber][touch - 1] = touchFrames_[noteNumber].nextId++; andrewm@9: addTouchToFrame(noteNumber, touchIdAssignments_[noteNumber][touch - 1], x, y); andrewm@9: } andrewm@9: } andrewm@9: andrewm@10: if(keyboard_.key(noteNumber) != 0) { andrewm@10: // Pass the frame to the keyboard by copy since PianoKey does its own ID number tracking. andrewm@10: // The important thing is that the Y values are always ordered. If ID tracking later changes andrewm@10: // in PianoKey it's of no consequence here as long as we retain the ability to track OSC andrewm@10: // touch IDs. andrewm@10: KeyTouchFrame copyFrame(touchFrames_[noteNumber]); andrewm@10: keyboard_.key(noteNumber)->touchInsertFrame(copyFrame, keyboard_.schedulerCurrentTimestamp()); andrewm@10: } andrewm@9: } andrewm@9: andrewm@9: // Touch removed andrewm@9: void TouchkeyOscEmulator::touchOffReceived(int key, int touch) { andrewm@9: int noteNumber = lowestMidiNote_ + key; andrewm@9: andrewm@9: // Sanity checks andrewm@9: if(noteNumber < 0 || noteNumber > 127) andrewm@9: return; andrewm@9: if(touch < 1 || touch > 3) andrewm@9: return; andrewm@9: andrewm@9: // Find TouchKeys ID associated with this OSC touch ID and andrewm@9: // meanwhile disassociate this touch with any future touch ID andrewm@9: int touchId = touchIdAssignments_[noteNumber][touch - 1]; andrewm@9: touchIdAssignments_[noteNumber][touch - 1] = -1; andrewm@9: andrewm@9: if(touchId < 0) // No known touch with this ID andrewm@9: return; andrewm@9: andrewm@9: for(int i = 0; i < 3; i++) { andrewm@9: if(touchFrames_[noteNumber].ids[i] == touchId) { andrewm@9: // Found the touch: remove it andrewm@9: removeTouchFromFrame(noteNumber, i); andrewm@9: andrewm@9: // Anything left? andrewm@9: if(touchFrames_[noteNumber].count == 0) { andrewm@9: clearTouchData(noteNumber); andrewm@9: if(keyboard_.key(noteNumber) != 0) andrewm@9: keyboard_.key(noteNumber)->touchOff(keyboard_.schedulerCurrentTimestamp()); andrewm@9: } andrewm@10: else if(keyboard_.key(noteNumber) != 0) { andrewm@10: KeyTouchFrame copyFrame(touchFrames_[noteNumber]); andrewm@10: keyboard_.key(noteNumber)->touchInsertFrame(copyFrame, keyboard_.schedulerCurrentTimestamp()); andrewm@10: } andrewm@9: break; andrewm@9: } andrewm@9: } andrewm@9: } andrewm@9: andrewm@9: // Clear touch data for a particular key andrewm@9: void TouchkeyOscEmulator::clearTouchData(int i) { andrewm@9: touchFrames_[i].count = 0; andrewm@9: touchFrames_[i].locH = -1.0; andrewm@9: touchFrames_[i].nextId = 0; andrewm@9: for(int j = 0; j < 3; j++) { andrewm@9: touchFrames_[i].ids[j] = -1; andrewm@9: touchFrames_[i].locs[j] = -1.0; andrewm@9: touchFrames_[i].sizes[j] = 0; andrewm@9: touchIdAssignments_[i][j] = -1; andrewm@9: } andrewm@9: int key = i % 12; andrewm@9: if(key == 1 || key == 3 || key == 6 || key == 8 || key == 10) andrewm@9: touchFrames_[i].white = false; andrewm@9: else andrewm@9: touchFrames_[i].white = true; andrewm@9: } andrewm@9: andrewm@9: void TouchkeyOscEmulator::removeTouchFromFrame(int note, int index) { andrewm@9: // Remove the touch and collapse the other touches around it down andrewm@9: // so they stay in order. andrewm@9: int lastTouchIndex = touchFrames_[note].count - 1; andrewm@9: if(lastTouchIndex < 0) andrewm@9: return; andrewm@9: andrewm@9: for(int i = index; i < lastTouchIndex; i++) { andrewm@9: touchFrames_[note].ids[i] = touchFrames_[note].ids[i+1]; andrewm@9: touchFrames_[note].locs[i] = touchFrames_[note].locs[i+1]; andrewm@9: touchFrames_[note].sizes[i] = touchFrames_[note].sizes[i+1]; andrewm@9: } andrewm@9: andrewm@9: touchFrames_[note].ids[lastTouchIndex] = -1; andrewm@9: touchFrames_[note].locs[lastTouchIndex] = -1.0; andrewm@9: touchFrames_[note].sizes[lastTouchIndex] = 0.0; andrewm@9: touchFrames_[note].count--; andrewm@9: } andrewm@9: andrewm@9: // Add a touch with the indicated ID to the frame. Its position in the KeyTouchFrame andrewm@9: // order may depend on its y value andrewm@9: void TouchkeyOscEmulator::addTouchToFrame(int note, int touchId, float x, float y) { andrewm@9: if(touchFrames_[note].count >= 3) // Already full? andrewm@9: return; andrewm@9: andrewm@9: // Touches are ordered by y position. Look for where this fits in the sequence andrewm@9: // and insert it there. andrewm@9: int insertLoc; andrewm@9: for(insertLoc = 0; insertLoc < touchFrames_[note].count; insertLoc++) { andrewm@9: if(touchFrames_[note].locs[insertLoc] > y) andrewm@9: break; andrewm@9: } andrewm@9: andrewm@9: // Move the other touches back to make space andrewm@9: for(int i = touchFrames_[note].count; i > insertLoc; i--) { andrewm@9: touchFrames_[note].ids[i] = touchFrames_[note].ids[i-1]; andrewm@9: touchFrames_[note].locs[i] = touchFrames_[note].locs[i-1]; andrewm@9: touchFrames_[note].sizes[i] = touchFrames_[note].sizes[i-1]; andrewm@9: } andrewm@9: andrewm@9: // Add the new touch in the vacant spot andrewm@9: touchFrames_[note].ids[insertLoc] = touchId; andrewm@9: touchFrames_[note].locs[insertLoc] = y; andrewm@9: touchFrames_[note].sizes[insertLoc] = 1.0; // Size is not supported over OSC emulation andrewm@11: touchFrames_[note].count++; andrewm@9: andrewm@9: // Lowest touch controls the horizontal position andrewm@12: if(insertLoc == 0) { andrewm@12: // Emulate the partial X sensing of the white TouchKeys andrewm@12: if(touchFrames_[note].locs[0] > kWhiteFrontBackCutoff && touchFrames_[note].white) andrewm@12: touchFrames_[note].locH = -1.0; andrewm@12: else andrewm@12: touchFrames_[note].locH = x; andrewm@12: } andrewm@9: } andrewm@9: andrewm@9: // Update the touch at the given index to the new value. Depending on the values, it andrewm@9: // may be necessary to reorder the touches to keep them in order of increasing Y value. andrewm@9: void TouchkeyOscEmulator::updateTouchInFrame(int note, int index, float x, float y) { andrewm@9: // Is it still in proper order? andrewm@9: bool ordered = true; andrewm@9: if(index > 0) { andrewm@9: if(touchFrames_[note].locs[index-1] > y) andrewm@9: ordered = false; andrewm@9: } andrewm@9: if(index < touchFrames_[note].count - 1) { andrewm@9: if(touchFrames_[note].locs[index + 1] < y) andrewm@9: ordered = false; andrewm@9: } andrewm@9: andrewm@9: // If out of order, the simplest strategy is to remove the touch and re-add it which andrewm@9: // will keep everything in order. Otherwise just update the information andrewm@9: if(ordered) { andrewm@9: touchFrames_[note].locs[index] = y; andrewm@12: if(index == 0) { andrewm@12: if(y > kWhiteFrontBackCutoff && touchFrames_[note].white) andrewm@12: touchFrames_[note].locH = -1; andrewm@12: else andrewm@12: touchFrames_[note].locH = x; andrewm@12: } andrewm@9: } andrewm@9: else { andrewm@9: int currentId = touchFrames_[note].ids[index]; andrewm@9: removeTouchFromFrame(note, index); andrewm@9: addTouchToFrame(note, currentId, x, y); andrewm@9: } andrewm@9: }