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: */