andrewm@0: /* andrewm@0: TouchKeys: multi-touch musical keyboard control software andrewm@0: Copyright (c) 2013 Andrew McPherson andrewm@0: andrewm@0: This program is free software: you can redistribute it and/or modify andrewm@0: it under the terms of the GNU General Public License as published by andrewm@0: the Free Software Foundation, either version 3 of the License, or andrewm@0: (at your option) any later version. andrewm@0: andrewm@0: This program is distributed in the hope that it will be useful, andrewm@0: but WITHOUT ANY WARRANTY; without even the implied warranty of andrewm@0: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the andrewm@0: GNU General Public License for more details. andrewm@0: andrewm@0: You should have received a copy of the GNU General Public License andrewm@0: along with this program. If not, see . andrewm@0: andrewm@0: ===================================================================== andrewm@0: andrewm@0: PianoKey.cpp: handles the operation of a single key on the keyboard, andrewm@0: including fusing touch and MIDI data. Also has hooks for continuous andrewm@0: key angle. andrewm@0: */ andrewm@0: andrewm@0: #include "PianoKey.h" andrewm@0: #include "PianoKeyboard.h" andrewm@0: #include "../Mappings/MappingFactory.h" andrewm@0: andrewm@0: #undef TOUCHKEYS_LEGACY_OSC andrewm@0: andrewm@0: // Default constructor andrewm@0: PianoKey::PianoKey(PianoKeyboard& keyboard, int noteNumber, int bufferLength) andrewm@0: : TriggerDestination(), keyboard_(keyboard), noteNumber_(noteNumber), andrewm@0: midiNoteIsOn_(false), midiChannel_(-1), midiOutputPort_(0), midiVelocity_(0), andrewm@0: midiAftertouch_(bufferLength), midiOnTimestamp_(0), midiOffTimestamp_(0), andrewm@0: positionBuffer_(bufferLength), andrewm@0: idleDetector_(kPianoKeyIdleBufferLength, positionBuffer_, kPianoKeyDefaultIdlePositionThreshold, andrewm@0: kPianoKeyDefaultIdleActivityThreshold, kPianoKeyDefaultIdleCounter), andrewm@0: positionTracker_(kPianoKeyPositionTrackerBufferLength, positionBuffer_), andrewm@0: stateBuffer_(kPianoKeyStateBufferLength), state_(kKeyStateToBeInitialized), andrewm@0: touchSensorsArePresent_(true), touchIsActive_(false), andrewm@0: touchBuffer_(bufferLength), touchIsWaiting_(false), touchWaitingSource_(0), andrewm@0: touchWaitingTimestamp_(0), andrewm@0: touchTimeoutInterval_(kPianoKeyDefaultTouchTimeoutInterval) andrewm@0: { andrewm@0: enable(); andrewm@0: registerForTrigger(&idleDetector_); andrewm@0: } andrewm@0: andrewm@0: // Destructor andrewm@0: PianoKey::~PianoKey() { andrewm@0: // Remove any mappings we've created andrewm@0: //keyboard_.removeMapping(noteNumber_); andrewm@0: } andrewm@0: andrewm@0: andrewm@0: // Disable the key from sending events. Do this by removing anything that andrewm@0: // listens to its status. andrewm@0: void PianoKey::disable() { andrewm@0: ScopedLock sl(stateMutex_); andrewm@0: andrewm@0: if(state_ == kKeyStateDisabled) { andrewm@0: return; andrewm@0: } andrewm@0: // No longer run the idle comparator. This ensures that no further state andrewm@0: // changes take place, and that the idle coefficient is not calculated. andrewm@0: //idleComparator_.clearTriggers(); andrewm@0: andrewm@0: terminateActivity(); andrewm@0: changeState(kKeyStateDisabled); andrewm@0: } andrewm@0: andrewm@0: // Start listening for key activity. This will allow the state to transition to andrewm@0: // idle, and then to active as appropriate. andrewm@0: void PianoKey::enable() { andrewm@0: ScopedLock sl(stateMutex_); andrewm@0: andrewm@0: if(state_ != kKeyStateDisabled) { andrewm@0: return; andrewm@0: } andrewm@0: changeState(kKeyStateUnknown); andrewm@0: } andrewm@0: andrewm@0: // Reset the key to its default state andrewm@0: void PianoKey::reset() { andrewm@0: ScopedLock sl(stateMutex_); andrewm@0: andrewm@0: terminateActivity(); // Stop any current activity andrewm@0: positionBuffer_.clear(); // Clear all history andrewm@0: stateBuffer_.clear(); andrewm@0: idleDetector_.clear(); andrewm@0: changeState(kKeyStateUnknown); // Reinitialize with unknown state andrewm@0: } andrewm@0: andrewm@0: // Insert a new sample in the key buffer andrewm@0: void PianoKey::insertSample(key_position pos, timestamp_type ts) { andrewm@0: positionBuffer_.insert(pos, ts); andrewm@0: andrewm@0: if((timestamp_diff_type)ts - (timestamp_diff_type)timeOfLastGuiUpdate_ > kPianoKeyGuiUpdateInterval) { andrewm@0: timeOfLastGuiUpdate_ = ts; andrewm@0: if(keyboard_.gui() != 0) { andrewm@0: keyboard_.gui()->setAnalogValueForKey(noteNumber_, pos); andrewm@0: } andrewm@0: } andrewm@0: andrewm@0: /*if((timestamp_diff_type)ts - (timestamp_diff_type)timeOfLastDebugPrint_ > 1.0) { andrewm@0: timeOfLastDebugPrint_ = ts; andrewm@0: key_position kmin = missing_value::missing(), kmax = missing_value::missing(); andrewm@0: key_position mean = 0; andrewm@0: int count = 0; andrewm@0: Node::iterator it = positionBuffer_.begin(); andrewm@0: while(it != positionBuffer_.end()) { andrewm@0: if(missing_value::isMissing(*it)) andrewm@0: continue; andrewm@0: if(missing_value::isMissing(kmin) || *it < kmin) andrewm@0: kmin = *it; andrewm@0: if(missing_value::isMissing(kmax) || *it > kmax) andrewm@0: kmax = *it; andrewm@0: mean += *it; andrewm@0: it++; andrewm@0: count++; andrewm@0: } andrewm@0: mean /= (key_position)count; andrewm@0: andrewm@0: key_position var = 0; andrewm@0: it = positionBuffer_.begin(); andrewm@0: while(it != positionBuffer_.end()) { andrewm@0: if(missing_value::isMissing(*it)) andrewm@0: continue; andrewm@0: var += (*it - mean)*(*it - mean); andrewm@0: it++; andrewm@0: } andrewm@0: var /= (key_position)count; andrewm@0: andrewm@0: std::cout << "Key " << noteNumber_ << " mean " << mean << " var " << var << std::endl; andrewm@0: }*/ andrewm@0: } andrewm@0: andrewm@0: // If a key is active, force it to become idle, stopping any processes that it has created andrewm@0: void PianoKey::forceIdle() { andrewm@0: ScopedLock sl(stateMutex_); andrewm@0: andrewm@0: if(state_ == kKeyStateDisabled || state_ == kKeyStateIdle) { andrewm@0: return; andrewm@0: } andrewm@0: terminateActivity(); andrewm@0: changeState(kKeyStateIdle); andrewm@0: } andrewm@0: andrewm@0: // Handle triggers sent when specific conditions are met (called by various objects) andrewm@0: andrewm@0: void PianoKey::triggerReceived(TriggerSource* who, timestamp_type timestamp) { andrewm@0: ScopedLock sl(stateMutex_); andrewm@0: andrewm@0: if(who == &idleDetector_) { andrewm@0: //std::cout << "Key " << noteNumber_ << ": IdleDetector says: " << idleDetector_.latest() << std::endl; andrewm@0: andrewm@0: if(idleDetector_.latest() == kIdleDetectorIdle) { andrewm@0: cout << "Key " << noteNumber_ << " --> Idle\n"; andrewm@0: andrewm@0: keyboard_.tellAllMappingFactoriesKeyMotionIdle(noteNumber_, midiNoteIsOn_, touchIsActive_, andrewm@0: &touchBuffer_, &positionBuffer_, &positionTracker_); andrewm@0: andrewm@0: // Remove any mapping present on this key andrewm@0: //keyboard_.removeMapping(noteNumber_); andrewm@0: andrewm@0: positionTracker_.disengage(); andrewm@0: unregisterForTrigger(&positionTracker_); andrewm@0: terminateActivity(); andrewm@0: changeState(kKeyStateIdle); andrewm@0: keyboard_.setKeyLEDColorRGB(noteNumber_, 0, 0, 0); andrewm@0: } andrewm@0: else if(idleDetector_.latest() == kIdleDetectorActive && state_ != kKeyStateUnknown) { andrewm@0: cout << "Key " << noteNumber_ << " --> Active\n"; andrewm@0: andrewm@0: keyboard_.tellAllMappingFactoriesKeyMotionActive(noteNumber_, midiNoteIsOn_, touchIsActive_, andrewm@0: &touchBuffer_, &positionBuffer_, &positionTracker_); andrewm@0: andrewm@0: // Only allow transition to active from a known previous state andrewm@0: // TODO: set up min/max listener andrewm@0: // TODO: may want to change the parameters on the idleDetector andrewm@0: changeState(kKeyStateActive); andrewm@0: //keyboard_.setKeyLEDColorRGB(noteNumber_, 1.0, 0.0, 0); andrewm@0: andrewm@0: // Engage the position tracker that handles specific measurement of key states andrewm@0: registerForTrigger(&positionTracker_); andrewm@0: positionTracker_.reset(); andrewm@0: positionTracker_.engage(); andrewm@0: andrewm@0: // Allocate a new mapping that converts key position gestures to sound andrewm@0: // control messages. TODO: how do we handle this with the TouchKey data too? andrewm@0: //MRPMapping *mapping = new MRPMapping(keyboard_, noteNumber_, &touchBuffer_, andrewm@0: // &positionBuffer_, &positionTracker_); andrewm@0: //MIDIKeyPositionMapping *mapping = new MIDIKeyPositionMapping(keyboard_, noteNumber_, &touchBuffer_, andrewm@0: // &positionBuffer_, &positionTracker_); andrewm@0: //keyboard_.addMapping(noteNumber_, mapping); andrewm@0: //mapping->setPercussivenessMIDIChannel(1); andrewm@0: //mapping->engage(); andrewm@0: } andrewm@0: } andrewm@0: else if(who == &positionTracker_ && !positionTracker_.empty()) { andrewm@0: KeyPositionTrackerNotification notification = positionTracker_.latest(); andrewm@0: andrewm@0: if(notification.type == KeyPositionTrackerNotification::kNotificationTypeStateChange) { andrewm@0: int positionTrackerState = notification.state; andrewm@0: andrewm@0: KeyPositionTracker::Event recentEvent; andrewm@0: std::pair velocityInfo; andrewm@0: cout << "Key " << noteNumber_ << " --> State " << positionTrackerState << endl; andrewm@0: andrewm@0: switch(positionTrackerState) { andrewm@0: case kPositionTrackerStatePartialPressAwaitingMax: andrewm@0: //keyboard_.setKeyLEDColorRGB(noteNumber_, 1.0, 0.0, 0); andrewm@0: recentEvent = positionTracker_.pressStart(); andrewm@0: cout << " start = (" << recentEvent.index << ", " << recentEvent.position << ", " << recentEvent.timestamp << ")\n"; andrewm@0: break; andrewm@0: case kPositionTrackerStatePartialPressFoundMax: andrewm@0: //keyboard_.setKeyLEDColorRGB(noteNumber_, 1.0, 0.6, 0); andrewm@0: recentEvent = positionTracker_.currentMax(); andrewm@0: cout << " max = (" << recentEvent.index << ", " << recentEvent.position << ", " << recentEvent.timestamp << ")\n"; andrewm@0: break; andrewm@0: case kPositionTrackerStatePressInProgress: andrewm@0: //keyboard_.setKeyLEDColorRGB(noteNumber_, 0.8, 0.8, 0); andrewm@0: velocityInfo = positionTracker_.pressVelocity(); andrewm@0: cout << " escapement time = " << velocityInfo.first << " velocity = " << velocityInfo.second << endl; andrewm@0: break; andrewm@0: case kPositionTrackerStateDown: andrewm@0: //keyboard_.setKeyLEDColorRGB(noteNumber_, 0, 1.0, 0); andrewm@0: recentEvent = positionTracker_.pressStart(); andrewm@0: cout << " start = (" << recentEvent.index << ", " << recentEvent.position << ", " << recentEvent.timestamp << ")\n"; andrewm@0: recentEvent = positionTracker_.pressFinish(); andrewm@0: cout << " finish = (" << recentEvent.index << ", " << recentEvent.position << ", " << recentEvent.timestamp << ")\n"; andrewm@0: velocityInfo = positionTracker_.pressVelocity(); andrewm@0: cout << " escapement time = " << velocityInfo.first << " velocity = " << velocityInfo.second << endl; andrewm@0: andrewm@0: if(keyboard_.graphGUI() != 0) { andrewm@0: keyboard_.graphGUI()->setKeyPressStart(positionTracker_.pressStart().position, positionTracker_.pressStart().timestamp); andrewm@0: keyboard_.graphGUI()->setKeyPressFinish(positionTracker_.pressFinish().position, positionTracker_.pressFinish().timestamp); andrewm@0: keyboard_.graphGUI()->copyKeyDataFromBuffer(positionBuffer_, positionTracker_.pressStart().index - 10, andrewm@0: positionBuffer_.endIndex()); andrewm@0: } andrewm@0: break; andrewm@0: case kPositionTrackerStateReleaseInProgress: andrewm@0: //keyboard_.setKeyLEDColorRGB(noteNumber_, 0, 0, 1.0); andrewm@0: recentEvent = positionTracker_.releaseStart(); andrewm@0: cout << " start = (" << recentEvent.index << ", " << recentEvent.position << ", " << recentEvent.timestamp << ")\n"; andrewm@0: if(keyboard_.graphGUI() != 0) { andrewm@0: keyboard_.graphGUI()->setKeyReleaseStart(positionTracker_.releaseStart().position, positionTracker_.releaseStart().timestamp); andrewm@0: keyboard_.graphGUI()->copyKeyDataFromBuffer(positionBuffer_, positionTracker_.pressStart().index - 10, andrewm@0: positionBuffer_.endIndex()); andrewm@0: //keyboard_.graphGUI()->copyKeyDataFromBuffer(testFilter_, testFilter_.indexNearestTo(positionBuffer_.timestampAt(positionTracker_.pressStart().index - 10)), testFilter_.endIndex()); andrewm@0: } andrewm@0: break; andrewm@0: case kPositionTrackerStateReleaseFinished: andrewm@0: //keyboard_.setKeyLEDColorRGB(noteNumber_, 0.5, 0, 1.0); andrewm@0: recentEvent = positionTracker_.releaseFinish(); andrewm@0: cout << " finish = (" << recentEvent.index << ", " << recentEvent.position << ", " << recentEvent.timestamp << ")\n"; andrewm@0: if(keyboard_.graphGUI() != 0) { andrewm@0: keyboard_.graphGUI()->setKeyReleaseStart(positionTracker_.releaseStart().position, positionTracker_.releaseStart().timestamp); andrewm@0: keyboard_.graphGUI()->setKeyReleaseFinish(positionTracker_.releaseFinish().position, positionTracker_.releaseFinish().timestamp); andrewm@0: keyboard_.graphGUI()->copyKeyDataFromBuffer(positionBuffer_, positionTracker_.pressStart().index - 10, andrewm@0: positionBuffer_.endIndex()); andrewm@0: //keyboard_.graphGUI()->copyKeyDataFromBuffer(testFilter_, testFilter_.indexNearestTo(positionBuffer_.timestampAt(positionTracker_.pressStart().index - 10)), testFilter_.endIndex()); andrewm@0: } andrewm@0: break; andrewm@0: default: andrewm@0: break; andrewm@0: } andrewm@0: } andrewm@0: } andrewm@0: } andrewm@0: andrewm@0: // Update the current state andrewm@0: andrewm@0: void PianoKey::changeState(key_state newState) { andrewm@0: if(!positionBuffer_.empty()) andrewm@0: changeState(newState, positionBuffer_.latestTimestamp()); andrewm@0: else andrewm@0: changeState(newState, 0); andrewm@0: } andrewm@0: andrewm@0: void PianoKey::changeState(key_state newState, timestamp_type timestamp) { andrewm@0: stateBuffer_.insert(newState, timestamp); andrewm@0: state_ = newState; andrewm@0: } andrewm@0: andrewm@0: // Stop any activity that's currently taking place on account of the key motion andrewm@0: andrewm@0: void PianoKey::terminateActivity() { andrewm@0: andrewm@0: } andrewm@0: andrewm@0: #pragma mark MIDI Methods andrewm@0: // ***** MIDI Methods ***** andrewm@0: andrewm@0: // Note On message from associated MIDI keyboard: record channel we should use andrewm@0: // for the duration of this note, as well as the note's velocity andrewm@0: void PianoKey::midiNoteOn(MidiKeyboardSegment *who, int velocity, int channel, timestamp_type timestamp) { andrewm@0: midiNoteIsOn_ = true; andrewm@0: midiChannel_ = channel; andrewm@0: midiVelocity_ = velocity; andrewm@0: midiOnTimestamp_ = timestamp; andrewm@0: andrewm@0: if(keyboard_.mappingFactory(who) != 0) andrewm@0: keyboard_.mappingFactory(who)->midiNoteOn(noteNumber_, touchIsActive_, (idleDetector_.idleState() == kIdleDetectorActive), andrewm@0: &touchBuffer_, &positionBuffer_, &positionTracker_); andrewm@0: andrewm@0: // Start the note right away unless we either need to wait for a touch or a delay has been enforced andrewm@0: if((touchIsActive_ && !touchBuffer_.empty()) || !touchSensorsArePresent_ || (touchTimeoutInterval_ == 0)) { andrewm@0: midiNoteOnHelper(who); andrewm@0: } andrewm@0: else { andrewm@0: // If touch isn't yet active, we might delay the note onset for a short andrewm@0: // time to wait for it. andrewm@0: andrewm@0: // Cases: andrewm@0: // (1) Touch was active --> send above messages and tell the MidiController to go ahead andrewm@0: // (a) Channel Selection mode: OSC messages will be used to choose a channel; messages andrewm@0: // that translate to control changes need to be archived and resent once the channel is known andrewm@0: // (b) Polyphonic and other modes: OSC messages used to generate control changes; channel andrewm@0: // is already known andrewm@0: // (2) Touch not yet active --> schedule a note on for a specified time interval in the future andrewm@0: // (a) Touch event arrives on this key before that --> send OSC and call the MidiController, andrewm@0: // removing the scheduled future event andrewm@0: // (b) No touch event arrives --> future event triggers without touch info, call MidiController andrewm@0: // and tell it to use defaults andrewm@0: touchWaitingSource_ = who; andrewm@0: touchIsWaiting_ = true; andrewm@0: touchWaitingTimestamp_ = keyboard_.schedulerCurrentTimestamp() + touchTimeoutInterval_; andrewm@0: keyboard_.scheduleEvent(this, andrewm@0: boost::bind(&PianoKey::touchTimedOut, this), andrewm@0: touchWaitingTimestamp_); andrewm@0: } andrewm@0: } andrewm@0: andrewm@0: // This private method does the real work of midiNoteOn(). It's separated because under certain andrewm@0: // circumstances, the helper function is called in a delayed manner, after a touch has been received. andrewm@0: void PianoKey::midiNoteOnHelper(MidiKeyboardSegment *who) { andrewm@0: touchIsWaiting_ = false; andrewm@0: touchWaitingSource_ = 0; andrewm@0: andrewm@0: if(!touchBuffer_.empty()) { andrewm@0: const KeyTouchFrame& frame(touchBuffer_.latest()); andrewm@0: int indexOfFirstTouch = 0; andrewm@0: andrewm@0: // Find which touch happened first so we can report its location andrewm@0: for(int i = 0; i < frame.count; i++) { andrewm@0: if(frame.ids[i] < frame.ids[indexOfFirstTouch]) andrewm@0: indexOfFirstTouch = i; andrewm@0: } andrewm@0: andrewm@0: // Send a message reporting the touch location of the first touch and the andrewm@0: // current number of touches. The target (either MidiInputController or external) andrewm@0: // may use this to change its behavior independently of later changes in touch. andrewm@0: andrewm@0: keyboard_.sendMessage("/touchkeys/preonset", "iiiiiiffiffifff", andrewm@0: noteNumber_, midiChannel_, midiVelocity_, // MIDI data andrewm@0: frame.count, indexOfFirstTouch, // General information: how many touches, which was first? andrewm@0: frame.ids[0], frame.locs[0], frame.sizes[0], // Specific touch information andrewm@0: frame.ids[1], frame.locs[1], frame.sizes[1], andrewm@0: frame.ids[2], frame.locs[2], frame.sizes[2], andrewm@0: frame.locH, LO_ARGS_END); andrewm@0: andrewm@0: // ---- andrewm@0: // The above function will trigger the callback in MidiInputController, if it is enabled. andrewm@0: // Therefore, the calls below will take place after MidiInputController has handled its callback. andrewm@0: // ---- andrewm@0: andrewm@0: #ifdef TOUCHKEYS_LEGACY_OSC andrewm@0: // Send move and resize gestures for each active touch andrewm@0: for(int i = 0; i < frame.count; i++) { andrewm@0: keyboard_.sendMessage("/touchkeys/move", "iiff", noteNumber_, frame.ids[i], andrewm@0: frame.locs[i], frame.horizontal(i), LO_ARGS_END); andrewm@0: keyboard_.sendMessage("/touchkeys/resize", "iif", noteNumber_, frame.ids[i], andrewm@0: frame.sizes[i], LO_ARGS_END); andrewm@0: } andrewm@0: andrewm@0: // If more than one touch is present, resend any pinch and slide gestures andrewm@0: // before we start. andrewm@0: if(frame.count == 2) { andrewm@0: float newCentroid = (frame.locs[0] + frame.locs[1]) / 2.0; andrewm@0: float newWidth = frame.locs[1] - frame.locs[0]; andrewm@0: andrewm@0: keyboard_.sendMessage("/touchkeys/twofinger/pinch", "iiif", andrewm@0: noteNumber_, frame.ids[0], frame.ids[1], newWidth, LO_ARGS_END); andrewm@0: keyboard_.sendMessage("/touchkeys/twofinger/slide", "iiif", andrewm@0: noteNumber_, frame.ids[0], frame.ids[1], newCentroid, LO_ARGS_END); andrewm@0: } andrewm@0: else if(frame.count == 3) { andrewm@0: float newCentroid = (frame.locs[0] + frame.locs[1] + frame.locs[2]) / 3.0; andrewm@0: float newWidth = frame.locs[2] - frame.locs[0]; andrewm@0: andrewm@0: keyboard_.sendMessage("/touchkeys/threefinger/pinch", "iiiif", andrewm@0: noteNumber_, frame.ids[0], frame.ids[1], frame.ids[2], newWidth, LO_ARGS_END); andrewm@0: keyboard_.sendMessage("/touchkeys/threefinger/slide", "iiiif", andrewm@0: noteNumber_, frame.ids[0], frame.ids[1], frame.ids[2], newCentroid, LO_ARGS_END); andrewm@0: } andrewm@0: #endif andrewm@0: } andrewm@0: andrewm@0: // Before the note starts, inform the mapping factory in case there are default values to be sent out. andrewm@0: if(keyboard_.mappingFactory(who) != 0) andrewm@0: keyboard_.mappingFactory(who)->noteWillBegin(noteNumber_, midiChannel_, midiVelocity_); andrewm@0: andrewm@0: keyboard_.sendMessage("/midi/noteon", "iii", noteNumber_, midiChannel_, midiVelocity_, LO_ARGS_END); andrewm@0: andrewm@0: // Update GUI if it is available. TODO: fix the ordering problem for real! andrewm@0: if(keyboard_.gui() != 0 && midiNoteIsOn_) { andrewm@0: keyboard_.gui()->setMidiActive(noteNumber_, true); andrewm@0: } andrewm@0: } andrewm@0: andrewm@0: // Note Off message from associated MIDI keyboard. Clear all old MIDI state. andrewm@0: void PianoKey::midiNoteOff(MidiKeyboardSegment *who, timestamp_type timestamp) { andrewm@0: midiNoteIsOn_ = false; andrewm@0: midiOffTimestamp_ = timestamp; andrewm@0: andrewm@0: if(keyboard_.mappingFactory(who) != 0) andrewm@0: keyboard_.mappingFactory(who)->midiNoteOff(noteNumber_, touchIsActive_, (idleDetector_.idleState() == kIdleDetectorActive), andrewm@0: &touchBuffer_, &positionBuffer_, &positionTracker_); andrewm@0: andrewm@0: keyboard_.sendMessage("/midi/noteoff", "ii", noteNumber_, midiChannel_, LO_ARGS_END); andrewm@0: andrewm@46: midiVelocity_ = 0; andrewm@46: midiChannel_ = -1; andrewm@46: midiAftertouch_.clear(); andrewm@46: andrewm@0: // Update GUI if it is available andrewm@0: if(keyboard_.gui() != 0) { andrewm@0: keyboard_.gui()->setMidiActive(noteNumber_, false); andrewm@0: } andrewm@0: } andrewm@0: andrewm@0: // Aftertouch (either channel or polyphonic) message from associated MIDI keyboard andrewm@0: void PianoKey::midiAftertouch(MidiKeyboardSegment *who, int value, timestamp_type timestamp) { andrewm@0: if(!midiNoteIsOn_) andrewm@0: return; andrewm@0: midiAftertouch_.insert(value, timestamp); andrewm@0: andrewm@0: keyboard_.sendMessage("/midi/aftertouch-poly", "iii", noteNumber_, midiChannel_, value, LO_ARGS_END); andrewm@0: } andrewm@0: andrewm@0: #pragma mark Touch Methods andrewm@0: // ***** Touch Methods ***** andrewm@0: andrewm@0: // Insert a new frame of touchkey data, making any necessary status changes andrewm@0: // (i.e. touch active, possibly changing number of active touches) andrewm@0: andrewm@0: void PianoKey::touchInsertFrame(KeyTouchFrame& newFrame, timestamp_type timestamp) { andrewm@0: if(!touchSensorsArePresent_) andrewm@0: return; andrewm@9: andrewm@0: // First check if the key was previously inactive. If so, send a message andrewm@0: // that the touch has begun andrewm@0: if(!touchIsActive_) { andrewm@0: keyboard_.sendMessage("/touchkeys/on", "i", noteNumber_, LO_ARGS_END); andrewm@0: keyboard_.tellAllMappingFactoriesTouchBegan(noteNumber_, midiNoteIsOn_, (idleDetector_.idleState() == kIdleDetectorActive), andrewm@0: &touchBuffer_, &positionBuffer_, &positionTracker_); andrewm@0: } andrewm@0: andrewm@0: touchIsActive_ = true; andrewm@0: andrewm@0: // If previous touch frames are present on this key, check the preceding andrewm@0: // frame to see if the state has changed in any important ways andrewm@0: if(!touchBuffer_.empty()) { andrewm@0: const KeyTouchFrame& lastFrame(touchBuffer_.latest()); andrewm@0: andrewm@0: // Next ID is the touch ID that should be used for any new touches. Scoop this andrewm@0: // info from the last frame. andrewm@0: andrewm@0: newFrame.nextId = lastFrame.nextId; andrewm@0: andrewm@0: // Assign ID numbers to each touch. This is easy if the number of touches andrewm@0: // from the previous frame to this one stayed the same, somewhat more complex andrewm@0: // if a touch was added or removed. andrewm@0: andrewm@0: if(newFrame.count > lastFrame.count) { andrewm@0: // One or more points have been added. Match the new points to the old ones to figure out andrewm@0: // which points have been added, versus which moved from before. andrewm@0: andrewm@0: std::set availableNewPoints; andrewm@0: for(int i = 0; i < newFrame.count; i++) andrewm@0: availableNewPoints.insert(i); andrewm@0: andrewm@0: std::list ordering(touchMatchClosestPoints(lastFrame.locs, newFrame.locs, 3, 0, availableNewPoints, 0.0).second); andrewm@0: andrewm@0: // ordering tells us the index of the new point corresponding to each old index, andrewm@0: // e.g. {2, 0, 1} --> old point 0 goes to new point 2, old point 1 goes to new point 0, ... andrewm@0: andrewm@0: // new points are still in ascending position order, so we use this matching to assign unique IDs andrewm@0: // and send relevant "add" messages andrewm@0: andrewm@0: int counter = 0; andrewm@0: for(std::list::iterator it = ordering.begin(); it != ordering.end(); ++it) { andrewm@0: newFrame.ids[*it] = lastFrame.ids[counter]; andrewm@0: andrewm@0: if(newFrame.ids[*it] < 0) { andrewm@0: // Matching to a negative ID means the touch is new andrewm@0: andrewm@0: newFrame.ids[*it] = newFrame.nextId++; andrewm@0: touchAdd(newFrame, *it, timestamp); andrewm@0: } andrewm@0: else { andrewm@0: #ifdef TOUCHKEYS_LEGACY_OSC andrewm@0: // Send "move" messages for the points that have moved andrewm@0: if(fabsf(newFrame.locs[*it] - lastFrame.locs[counter]) > 0 /*moveThreshold_*/) andrewm@0: keyboard_.sendMessage("/touchkeys/move", "iiff", noteNumber_, newFrame.ids[*it], andrewm@0: newFrame.locs[*it], newFrame.horizontal(*it), LO_ARGS_END); andrewm@0: if(fabsf(newFrame.sizes[*it] - lastFrame.sizes[counter]) > 0 /*resizeThreshold_*/) andrewm@0: keyboard_.sendMessage("/touchkeys/resize", "iif", noteNumber_, newFrame.ids[*it], andrewm@0: newFrame.sizes[*it], LO_ARGS_END); andrewm@0: #endif andrewm@0: } andrewm@0: andrewm@0: counter++; andrewm@0: } andrewm@0: } andrewm@0: else if(newFrame.count < lastFrame.count) { andrewm@0: // One or more points have been removed. Match the new points to the old ones to figure out andrewm@0: // which points have been removed, versus which moved from before. andrewm@0: andrewm@0: std::set availableNewPoints; andrewm@0: for(int i = 0; i < 3; i++) andrewm@0: availableNewPoints.insert(i); andrewm@0: andrewm@0: std::list ordering(touchMatchClosestPoints(lastFrame.locs, newFrame.locs, 3, 0, availableNewPoints, 0.0).second); andrewm@0: andrewm@0: // ordering tells us the index of the new point corresponding to each old index, andrewm@0: // e.g. {2, 0, 1} --> old point 0 goes to new point 2, old point 1 goes to new point 0, ... andrewm@0: andrewm@0: // new points are still in ascending position order, so we use this matching to assign unique IDs andrewm@0: // and send relevant "add" messages andrewm@0: andrewm@0: int counter = 0; andrewm@0: for(std::list::iterator it = ordering.begin(); it != ordering.end(); ++it) { andrewm@0: if(*it < newFrame.count) { andrewm@0: // Old index {counter} matches a valid new touch andrewm@0: andrewm@0: newFrame.ids[*it] = lastFrame.ids[counter]; // Match IDs for currently active touches andrewm@0: andrewm@0: #ifdef TOUCHKEYS_LEGACY_OSC andrewm@0: // Send "move" messages for the points that have moved andrewm@0: if(fabsf(newFrame.locs[*it] - lastFrame.locs[counter]) > 0 /*moveThreshold_*/) andrewm@0: keyboard_.sendMessage("/touchkeys/move", "iiff", noteNumber_, newFrame.ids[*it], andrewm@0: newFrame.locs[*it], newFrame.horizontal(*it), LO_ARGS_END); andrewm@0: if(fabsf(newFrame.sizes[*it] - lastFrame.sizes[counter]) > 0 /*resizeThreshold_*/) andrewm@0: keyboard_.sendMessage("/touchkeys/resize", "iif", noteNumber_, newFrame.ids[*it], andrewm@0: newFrame.sizes[*it], LO_ARGS_END); andrewm@0: #endif andrewm@0: } andrewm@0: else if(lastFrame.ids[counter] >= 0) { andrewm@0: // Old index {counter} matches an invalid new index, meaning a touch has been removed. andrewm@0: touchRemove(lastFrame, lastFrame.ids[counter], newFrame.count, timestamp); andrewm@0: } andrewm@0: andrewm@0: counter++; andrewm@0: } andrewm@0: } andrewm@0: else { andrewm@0: // Same number of touches as before. Touches are always stored in increasing order, andrewm@0: // so we just need to copy these over, maintaining the same ID numbers. andrewm@0: andrewm@0: for(int i = 0; i < newFrame.count; i++) { andrewm@0: newFrame.ids[i] = lastFrame.ids[i]; andrewm@0: andrewm@0: #ifdef TOUCHKEYS_LEGACY_OSC andrewm@0: // Send "move" messages for the points that have moved andrewm@0: if(fabsf(newFrame.locs[i] - lastFrame.locs[i]) > 0 /*moveThreshold_*/) andrewm@0: keyboard_.sendMessage("/touchkeys/move", "iiff", noteNumber_, newFrame.ids[i], andrewm@0: newFrame.locs[i], newFrame.horizontal(i), LO_ARGS_END); andrewm@0: if(fabsf(newFrame.sizes[i] - lastFrame.sizes[i]) > 0 /*resizeThreshold_*/) andrewm@0: keyboard_.sendMessage("/touchkeys/resize", "iif", noteNumber_, newFrame.ids[i], andrewm@0: newFrame.sizes[i], LO_ARGS_END); andrewm@0: #endif andrewm@0: } andrewm@0: andrewm@0: // If the number of touches has stayed the same, look for multi-finger gestures (pinch and slide) andrewm@0: if(newFrame.count > 1) { andrewm@0: touchMultiFingerGestures(lastFrame, newFrame, timestamp); andrewm@0: } andrewm@0: } andrewm@0: } andrewm@0: else { andrewm@0: // With no previous frame to compare to, assign IDs to each active touch sequentially andrewm@0: andrewm@0: newFrame.nextId = 0; andrewm@0: for(int i = 0; i < newFrame.count; i++) { andrewm@0: newFrame.ids[i] = newFrame.nextId++; andrewm@0: touchAdd(newFrame, i, timestamp); andrewm@0: } andrewm@0: } andrewm@0: andrewm@0: // Add the new touch andrewm@0: touchBuffer_.insert(newFrame, timestamp); andrewm@0: andrewm@0: if(touchIsWaiting_) { andrewm@0: // If this flag was set, we were waiting for a touch to occur before taking further andrewm@0: // action. A timeout will have been scheduled, which we should clear. andrewm@0: keyboard_.unscheduleEvent(this, touchWaitingTimestamp_); andrewm@0: andrewm@0: // Send the queued up MIDI/OSC events andrewm@0: midiNoteOnHelper(touchWaitingSource_); andrewm@0: } andrewm@0: andrewm@0: // Update GUI if it is available andrewm@0: if(keyboard_.gui() != 0) { andrewm@0: keyboard_.gui()->setTouchForKey(noteNumber_, newFrame); andrewm@0: } andrewm@0: } andrewm@0: andrewm@0: // This is called when all touch is removed from a key. Clear out the previous state andrewm@0: andrewm@0: void PianoKey::touchOff(timestamp_type timestamp) { andrewm@0: if(!touchIsActive_ || !touchSensorsArePresent_) andrewm@0: return; andrewm@0: andrewm@0: keyboard_.tellAllMappingFactoriesTouchEnded(noteNumber_, midiNoteIsOn_, (idleDetector_.idleState() == kIdleDetectorActive), andrewm@0: &touchBuffer_, &positionBuffer_, &positionTracker_); andrewm@0: andrewm@0: touchEvents_.clear(); andrewm@0: andrewm@0: // Create a new event that records the timestamp of the idle event andrewm@0: // and the last frame before it occurred (but only if we have data andrewm@0: // on at least one frame before idle occurred) andrewm@0: if(!touchBuffer_.empty()) { andrewm@0: KeyTouchEvent event = { kTouchEventIdle, timestamp, touchBuffer_.latest() }; andrewm@0: touchEvents_.insert(std::pair(-1, event)); andrewm@0: } andrewm@0: andrewm@0: // Insert a blank touch frame into the buffer so anyone listening knows the touch has gone off andrewm@0: KeyTouchFrame emptyFrame; andrewm@0: emptyFrame.count = 0; andrewm@0: touchBuffer_.insert(emptyFrame, timestamp); andrewm@0: andrewm@0: // Send a message that the touch has ended andrewm@0: touchIsActive_ = false; andrewm@0: touchBuffer_.clear(); andrewm@0: keyboard_.sendMessage("/touchkeys/off", "i", noteNumber_, LO_ARGS_END); andrewm@0: // Update GUI if it is available andrewm@0: if(keyboard_.gui() != 0) { andrewm@0: keyboard_.gui()->clearTouchForKey(noteNumber_); andrewm@0: } andrewm@0: } andrewm@0: andrewm@0: // This function is called when we time out waiting for a touch on the given note andrewm@0: andrewm@0: timestamp_type PianoKey::touchTimedOut() { andrewm@0: //cout << "Touch timed out on note " << noteNumber_ << endl; andrewm@0: andrewm@0: // Do all the things we were planning to do once the touch was received. andrewm@0: midiNoteOnHelper(touchWaitingSource_); andrewm@0: andrewm@0: return 0; andrewm@0: } andrewm@0: andrewm@0: // Recursive function for matching old and new frames of touch locations, each with up to (count) points andrewm@0: // andrewm@0: // Example: old points 1-3, new points A-C andrewm@0: // 1A *2A* 3A andrewm@0: // *1B* 2B 3B andrewm@0: // 1C 2C *3C* andrewm@0: andrewm@0: std::pair > PianoKey::touchMatchClosestPoints(const float* oldPoints, const float *newPoints, float count, andrewm@0: int oldIndex, std::set& availableNewPoints, float currentTotalDistance) { andrewm@0: if(availableNewPoints.size() == 0) // Shouldn't happen but prevent an infinite loop andrewm@0: throw new std::exception; andrewm@0: andrewm@0: // End case: only one possible point available andrewm@0: if(availableNewPoints.size() == 1) { andrewm@0: int newIndex = *(availableNewPoints.begin()); andrewm@0: andrewm@0: std::list singleOrder; andrewm@0: singleOrder.push_front(newIndex); andrewm@0: andrewm@0: if(oldPoints[oldIndex] < 0.0 || newPoints[newIndex] < 0.0) { andrewm@0: //if(verbose_ >= 4) andrewm@0: // cout << " -> [" << newIndex << "] (" << currentTotalDistance + 100.0 << ")\n"; andrewm@0: andrewm@0: // Return the distance between the last old point and the only available new point andrewm@0: return std::pair > (currentTotalDistance + 100.0, singleOrder); andrewm@0: } andrewm@0: else { andrewm@0: //if(verbose_ >= 4) andrewm@0: // cout << " -> [" << newIndex << "] (" << currentTotalDistance + (oldPoints[oldIndex] - newPoints[newIndex])*(oldPoints[oldIndex] - newPoints[newIndex]) << ")\n"; andrewm@0: andrewm@0: // Return the distance between the last old point and the only available new point andrewm@0: return std::pair > (currentTotalDistance + (oldPoints[oldIndex] - newPoints[newIndex])*(oldPoints[oldIndex] - newPoints[newIndex]), singleOrder); andrewm@0: } andrewm@0: } andrewm@0: andrewm@20: float minVal = std::numeric_limits::infinity(); andrewm@0: std::set newPointsCopy(availableNewPoints); andrewm@0: std::set::iterator it; andrewm@0: std::list order; andrewm@0: andrewm@0: // Go through all available new points andrewm@0: for(it = availableNewPoints.begin(); it != availableNewPoints.end(); ++it) { andrewm@0: // Temporarily remove (and test) one point and recursively call ourselves andrewm@0: newPointsCopy.erase(*it); andrewm@0: andrewm@0: float dist; andrewm@0: if(newPoints[*it] >= 0.0 && oldPoints[oldIndex] >= 0.0) andrewm@0: dist = (oldPoints[oldIndex] - newPoints[*it])*(oldPoints[oldIndex] - newPoints[*it]); andrewm@0: else andrewm@0: dist = 100.0; andrewm@0: andrewm@0: std::pair > rval = touchMatchClosestPoints(oldPoints, newPoints, count, oldIndex + 1, newPointsCopy, andrewm@0: currentTotalDistance + dist); andrewm@0: andrewm@0: //if(verbose_ >= 4) andrewm@0: // cout << " from " << *it << " got " << rval.first << endl; andrewm@0: andrewm@0: if(rval.first < minVal) { andrewm@0: minVal = rval.first; andrewm@0: order = rval.second; andrewm@0: order.push_front(*it); andrewm@0: } andrewm@0: andrewm@0: newPointsCopy.insert(*it); andrewm@0: } andrewm@0: andrewm@0: /*if(verbose_ >= 4) { andrewm@0: cout << " -> ["; andrewm@0: list::iterator it2; andrewm@0: andrewm@0: for(it2 = order.begin(); it2 != order.end(); ++it2) andrewm@0: cout << *it2 << ", "; andrewm@0: cout << "] (" << minVal << ")\n"; andrewm@0: }*/ andrewm@0: andrewm@0: return std::pair >(minVal, order); andrewm@0: } andrewm@0: andrewm@0: // A new touch was added from the last frame to this one andrewm@0: andrewm@0: void PianoKey::touchAdd(const KeyTouchFrame& frame, int index, timestamp_type timestamp) { andrewm@0: KeyTouchEvent event = { kTouchEventAdd, timestamp, frame }; andrewm@0: touchEvents_.insert(std::pair(frame.ids[index], event)); andrewm@0: #ifdef TOUCHKEYS_LEGACY_OSC andrewm@0: keyboard_.sendMessage("/touchkeys/add", "iiifff", noteNumber_, frame.ids[index], frame.count, andrewm@0: frame.locs[index], frame.sizes[index], frame.horizontal(index), andrewm@0: LO_ARGS_END); andrewm@0: #endif andrewm@0: } andrewm@0: andrewm@0: // A touch was removed from the last frame. The frame in this case is the last frame containing andrewm@0: // the touch in question (so we can find its ending position later). andrewm@0: andrewm@0: void PianoKey::touchRemove(const KeyTouchFrame& frame, int idRemoved, int remainingCount, timestamp_type timestamp) { andrewm@0: KeyTouchEvent event = { kTouchEventRemove, timestamp, frame }; andrewm@0: touchEvents_.insert(std::pair(idRemoved, event)); andrewm@0: #ifdef TOUCHKEYS_LEGACY_OSC andrewm@0: keyboard_.sendMessage("/touchkeys/remove", "iii", noteNumber_, idRemoved, andrewm@0: remainingCount, LO_ARGS_END); andrewm@0: #endif andrewm@0: } andrewm@0: andrewm@0: // Process multi-finger gestures (pinch and slide) based on previous and current frames andrewm@0: andrewm@0: void PianoKey::touchMultiFingerGestures(const KeyTouchFrame& lastFrame, const KeyTouchFrame& newFrame, timestamp_type timestamp) { andrewm@0: #ifdef TOUCHKEYS_LEGACY_OSC andrewm@0: if(newFrame.count == 2 && lastFrame.count == 2) { andrewm@0: float previousCentroid = (lastFrame.locs[0] + lastFrame.locs[1]) / 2.0; andrewm@0: float newCentroid = (newFrame.locs[0] + newFrame.locs[1]) / 2.0; andrewm@0: float previousWidth = lastFrame.locs[1] - lastFrame.locs[0]; andrewm@0: float newWidth = newFrame.locs[1] - newFrame.locs[0]; andrewm@0: andrewm@0: if(fabsf(newWidth - previousWidth) >= 0 /*pinchThreshold_*/) { andrewm@0: keyboard_.sendMessage("/touchkeys/twofinger/pinch", "iiif", andrewm@0: noteNumber_, newFrame.ids[0], newFrame.ids[1], newWidth, LO_ARGS_END); andrewm@0: } andrewm@0: if(fabsf(newCentroid - previousCentroid) >= 0 /*slideThreshold_*/) { andrewm@0: keyboard_.sendMessage("/touchkeys/twofinger/slide", "iiif", andrewm@0: noteNumber_, newFrame.ids[0], newFrame.ids[1], newCentroid, LO_ARGS_END); andrewm@0: } andrewm@0: } andrewm@0: else if(newFrame.count == 3 && lastFrame.count == 3) { andrewm@0: float previousCentroid = (lastFrame.locs[0] + lastFrame.locs[1] + lastFrame.locs[2]) / 3.0; andrewm@0: float newCentroid = (newFrame.locs[0] + newFrame.locs[1] + newFrame.locs[2]) / 3.0; andrewm@0: float previousWidth = lastFrame.locs[2] - lastFrame.locs[0]; andrewm@0: float newWidth = newFrame.locs[2] - newFrame.locs[0]; andrewm@0: andrewm@0: if(fabsf(newWidth - previousWidth) >= 0 /*pinchThreshold_*/) { andrewm@0: keyboard_.sendMessage("/touchkeys/threefinger/pinch", "iiiif", andrewm@0: noteNumber_, newFrame.ids[0], newFrame.ids[1], newFrame.ids[2], newWidth, LO_ARGS_END); andrewm@0: } andrewm@0: if(fabsf(newCentroid - previousCentroid) >= 0 /*slideThreshold_*/) { andrewm@0: keyboard_.sendMessage("/touchkeys/threefinger/slide", "iiiif", andrewm@0: noteNumber_, newFrame.ids[0], newFrame.ids[1], newFrame.ids[2], newCentroid, LO_ARGS_END); andrewm@0: } andrewm@0: } andrewm@0: #endif andrewm@0: } andrewm@0: andrewm@0: /* andrewm@0: * State Machine: andrewm@0: * andrewm@0: * Disabled andrewm@0: * --> Unknown: (by user) andrewm@0: * enable triggers on comparator andrewm@0: * Unknown andrewm@0: * --> Idle: activity <= X, pos < Y andrewm@0: * Idle andrewm@0: * --> Active: activity > X andrewm@0: * start looking for maxes and mins andrewm@0: * watch for key down andrewm@0: * Active andrewm@0: * --> Idle: activity <= X, pos < Y andrewm@0: * stop looking for maxes and mins andrewm@0: * --> Max: found maximum position > Z andrewm@0: * calculate features andrewm@0: * Max: andrewm@0: * --> Max: found maximum position greater than before; time from start < T andrewm@0: * recalculate features andrewm@0: * --> Idle: activity <= X, pos < Y andrewm@0: * Down: andrewm@0: * (just a special case of Max?) andrewm@0: * Release: andrewm@0: * (means the user is no longer playing the key, ignore its motion) andrewm@0: * --> Idle: activity <= X, pos < Y andrewm@0: * andrewm@0: */