Mercurial > hg > touchkeys
diff Source/TouchKeys/KeyPositionTracker.cpp @ 0:3580ffe87dc8
First commit of TouchKeys public pre-release.
author | Andrew McPherson <andrewm@eecs.qmul.ac.uk> |
---|---|
date | Mon, 11 Nov 2013 18:19:35 +0000 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Source/TouchKeys/KeyPositionTracker.cpp Mon Nov 11 18:19:35 2013 +0000 @@ -0,0 +1,697 @@ +/* + TouchKeys: multi-touch musical keyboard control software + Copyright (c) 2013 Andrew McPherson + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + + ===================================================================== + + KeyPositionTracker.cpp: parses continuous key position and detects the + state of the key. +*/ + +#include "KeyPositionTracker.h" + +// Default constructor +KeyPositionTracker::KeyPositionTracker(capacity_type capacity, Node<key_position>& keyBuffer) +: Node<KeyPositionTrackerNotification>(capacity), keyBuffer_(keyBuffer), engaged_(false) { + reset(); +} + +// Copy constructor +/*KeyPositionTracker::KeyPositionTracker(KeyPositionTracker const& obj) +: Node<int>(obj), keyBuffer_(obj.keyBuffer_), engaged_(obj.engaged_) { + if(engaged_) + registerForTrigger(&keyBuffer_); +}*/ + +// Calculate (MIDI-style) key press velocity from continuous key position +std::pair<timestamp_type, key_velocity> KeyPositionTracker::pressVelocity() { + return pressVelocity(pressVelocityEscapementPosition_); +} + +std::pair<timestamp_type, key_velocity> KeyPositionTracker::pressVelocity(key_position escapementPosition) { + // Check that we have a valid start point from which to calculate + if(missing_value<timestamp_type>::isMissing(startTimestamp_)) { + return std::pair<timestamp_type, key_velocity>(missing_value<timestamp_type>::missing(), + missing_value<key_velocity>::missing()); + } + + // Find where the key position crosses the indicated level + key_buffer_index index = startIndex_; + if(index < keyBuffer_.beginIndex() + 2) + index = keyBuffer_.beginIndex() + 2; + + while(index < keyBuffer_.endIndex() - kPositionTrackerSamplesNeededForPressVelocityAfterEscapement) { + // If the key press has a defined end, make sure we don't go past it + if(pressIndex_ != 0 && index >= pressIndex_) + break; + + if(keyBuffer_[index] > escapementPosition) { + // Found the place the position crosses the indicated threshold + // Now find the exact (interpolated) timestamp and velocity + timestamp_type exactPressTimestamp = keyBuffer_.timestampAt(index); // TODO + + // Velocity is calculated by an average of 2 samples before and 1 after + key_position diffPosition = keyBuffer_[index + kPositionTrackerSamplesNeededForPressVelocityAfterEscapement] - keyBuffer_[index - 2]; + timestamp_diff_type diffTimestamp = keyBuffer_.timestampAt(index + kPositionTrackerSamplesNeededForPressVelocityAfterEscapement) - keyBuffer_.timestampAt(index - 2); + key_velocity velocity = calculate_key_velocity(diffPosition, diffTimestamp); + + return std::pair<timestamp_type, key_velocity>(exactPressTimestamp, velocity); + } + index++; + } + + // Didn't find anything matching that threshold + return std::pair<timestamp_type, key_velocity>(missing_value<timestamp_type>::missing(), + missing_value<key_velocity>::missing()); +} + +// Calculate (MIDI-style) key release velocity from continuous key position +std::pair<timestamp_type, key_velocity> KeyPositionTracker::releaseVelocity() { + return releaseVelocity(releaseVelocityEscapementPosition_); +} + +std::pair<timestamp_type, key_velocity> KeyPositionTracker::releaseVelocity(key_position returnPosition) { + // Check that we have a valid start point from which to calculate + if(missing_value<timestamp_type>::isMissing(releaseBeginTimestamp_)) { + return std::pair<timestamp_type, key_velocity>(missing_value<timestamp_type>::missing(), + missing_value<key_velocity>::missing()); + } + + // Find where the key position crosses the indicated level + key_buffer_index index = releaseBeginIndex_; + if(index < keyBuffer_.beginIndex() + 2) + index = keyBuffer_.beginIndex() + 2; + + while(index < keyBuffer_.endIndex() - kPositionTrackerSamplesNeededForReleaseVelocityAfterEscapement) { + // Check for whether we've hit the end of the release interval, assuming + // the interval exists yet + if(releaseEndIndex_ != 0 && index >= releaseEndIndex_) + break; + + if(keyBuffer_[index] < returnPosition) { + // Found the place the position crosses the indicated threshold + // Now find the exact (interpolated) timestamp and velocity + timestamp_type exactPressTimestamp = keyBuffer_.timestampAt(index); // TODO + + // Velocity is calculated by an average of 2 samples before and 1 after + key_position diffPosition = keyBuffer_[index + kPositionTrackerSamplesNeededForReleaseVelocityAfterEscapement] - keyBuffer_[index - 2]; + timestamp_diff_type diffTimestamp = keyBuffer_.timestampAt(index + kPositionTrackerSamplesNeededForReleaseVelocityAfterEscapement) - keyBuffer_.timestampAt(index - 2); + key_velocity velocity = calculate_key_velocity(diffPosition, diffTimestamp); + + std::cout << "found release velocity " << velocity << "(diffp " << diffPosition << ", diffT " << diffTimestamp << ")" << std::endl; + + return std::pair<timestamp_type, key_velocity>(exactPressTimestamp, velocity); + } + index++; + } + + // Didn't find anything matching that threshold + return std::pair<timestamp_type, key_velocity>(missing_value<timestamp_type>::missing(), + missing_value<key_velocity>::missing()); +} + +// Calculate and return features about the percussiveness of the key press +KeyPositionTracker::PercussivenessFeatures KeyPositionTracker::pressPercussiveness() { + PercussivenessFeatures features; + key_buffer_index index; + key_velocity maximumVelocity, largestVelocityDifference; + key_buffer_index maximumVelocityIndex, largestVelocityDifferenceIndex; + + // Check that we have a valid start point from which to calculate + if(missing_value<timestamp_type>::isMissing(startTimestamp_) || keyBuffer_.beginIndex() > startIndex_ - 1) { + std::cout << "*** no start time\n"; + features.percussiveness = missing_value<float>::missing(); + return features; + } + + // From the start of the key press, look for an initial maximum in velocity + index = startIndex_; + + maximumVelocity = scale_key_velocity(0); + maximumVelocityIndex = startIndex_; + largestVelocityDifference = scale_key_velocity(0); + largestVelocityDifferenceIndex = startIndex_; + + std::cout << "*** start index " << index << std::endl; + + while(index < keyBuffer_.endIndex()) { + if(pressIndex_ != 0 && index >= pressIndex_) + break; + + key_position diffPosition = keyBuffer_[index] - keyBuffer_[index - 1]; + timestamp_diff_type diffTimestamp = keyBuffer_.timestampAt(index) - keyBuffer_.timestampAt(index - 1); + key_velocity velocity = calculate_key_velocity(diffPosition, diffTimestamp); + + // Look for maximum of velocity + if(velocity > maximumVelocity) { + maximumVelocity = velocity; + maximumVelocityIndex = index; + std::cout << "*** found new max velocity " << maximumVelocity << " at index " << index << std::endl; + } + + // And given the difference between the max and the current sample, + // look for the largest rebound (velocity hitting a peak and falling) + if(maximumVelocity - velocity > largestVelocityDifference) { + largestVelocityDifference = maximumVelocity - velocity; + largestVelocityDifferenceIndex = index; + std::cout << "*** found new diff velocity " << largestVelocityDifference << " at index " << index << std::endl; + } + + // Only look at the early part of the key press: if the key position + // makes it more than a certain amount down, assume the initial spike + // has passed and finish up. But always allow at least 5 points for the + // fastest key presses to be considered. + if(index - startIndex_ >= 4 && keyBuffer_[index] > kPositionTrackerPositionThresholdForPercussivenessCalculation) { + break; + } + + index++; + } + + // Now transfer what we've found to the data structure + features.velocitySpikeMaximum = Event(maximumVelocityIndex, maximumVelocity, keyBuffer_.timestampAt(maximumVelocityIndex)); + features.velocitySpikeMinimum = Event(largestVelocityDifferenceIndex, maximumVelocity - largestVelocityDifference, + keyBuffer_.timestampAt(largestVelocityDifferenceIndex)); + features.timeFromStartToSpike = keyBuffer_.timestampAt(maximumVelocityIndex) - keyBuffer_.timestampAt(startIndex_); + + // Check if we found a meaningful difference. If not, percussiveness is set to 0 + if(largestVelocityDifference == scale_key_velocity(0)) { + features.percussiveness = 0.0; + features.areaPrecedingSpike = scale_key_velocity(0); + features.areaFollowingSpike = scale_key_velocity(0); + return features; + } + + // Calculate the area under the velocity curve before and after the maximum + features.areaPrecedingSpike = scale_key_velocity(0); + for(index = startIndex_; index < maximumVelocityIndex; index++) { + key_position diffPosition = keyBuffer_[index] - keyBuffer_[index - 1]; + timestamp_diff_type diffTimestamp = keyBuffer_.timestampAt(index) - keyBuffer_.timestampAt(index - 1); + features.areaPrecedingSpike += calculate_key_velocity(diffPosition, diffTimestamp); + } + features.areaFollowingSpike = scale_key_velocity(0); + for(index = maximumVelocityIndex; index < largestVelocityDifferenceIndex; index++) { + key_position diffPosition = keyBuffer_[index] - keyBuffer_[index - 1]; + timestamp_diff_type diffTimestamp = keyBuffer_.timestampAt(index) - keyBuffer_.timestampAt(index - 1); + features.areaFollowingSpike += calculate_key_velocity(diffPosition, diffTimestamp); + } + + std::cout << "area before = " << features.areaPrecedingSpike << " after = " << features.areaFollowingSpike << std::endl; + + features.percussiveness = features.velocitySpikeMaximum.position; + + return features; +} + +// Register to receive messages from the key buffer on each new sample +void KeyPositionTracker::engage() { + if(engaged_) + return; + + registerForTrigger(&keyBuffer_); + engaged_ = true; +} + +// Unregister from receiving message on new samples +void KeyPositionTracker::disengage() { + if(!engaged_) + return; + + unregisterForTrigger(&keyBuffer_); + engaged_ = false; +} + +// Clear current state and reset to unknown state +void KeyPositionTracker::reset() { + Node<KeyPositionTrackerNotification>::clear(); + + currentState_ = kPositionTrackerStateUnknown; + currentlyAvailableFeatures_ = KeyPositionTrackerNotification::kFeaturesNone; + currentMinIndex_ = currentMaxIndex_ = startIndex_ = pressIndex_ = 0; + releaseBeginIndex_ = releaseEndIndex_ = 0; + lastMinMaxPosition_ = startPosition_ = pressPosition_ = missing_value<key_position>::missing(); + releaseBeginPosition_ = releaseEndPosition_ = missing_value<key_position>::missing(); + currentMinPosition_ = currentMaxPosition_ = missing_value<key_position>::missing(); + startTimestamp_ = pressTimestamp_ = missing_value<timestamp_type>::missing(); + currentMinTimestamp_ = currentMaxTimestamp_ = missing_value<timestamp_type>::missing(); + releaseBeginTimestamp_ = releaseEndTimestamp_ = missing_value<timestamp_type>::missing(); + pressVelocityEscapementPosition_ = kPositionTrackerDefaultPositionForPressVelocityCalculation; + releaseVelocityEscapementPosition_ = kPositionTrackerDefaultPositionForReleaseVelocityCalculation; + pressVelocityAvailableIndex_ = releaseVelocityAvailableIndex_ = percussivenessAvailableIndex_ = 0; + releaseVelocityWaitingForThresholdCross_ = false; +} + +// Evaluator function. Update the current state +void KeyPositionTracker::triggerReceived(TriggerSource* who, timestamp_type timestamp) { + + if(who != &keyBuffer_) + return; + + // Always start in the partial press state after a reset, retroactively locating + // the start position for this key press + if(empty()) { + findKeyPressStart(timestamp); + changeState(kPositionTrackerStatePartialPressAwaitingMax, timestamp); + } + + key_position currentKeyPosition = keyBuffer_.latest(); + key_buffer_index currentBufferIndex = keyBuffer_.endIndex() - 1; + + // First, check queued actions to see if we can calculate a new feature + // ** Press Velocity ** + if(pressVelocityAvailableIndex_ != 0) { + if(currentBufferIndex >= pressVelocityAvailableIndex_) { + // Can now calculate press velocity + currentlyAvailableFeatures_ |= KeyPositionTrackerNotification::kFeaturePressVelocity; + notifyFeature(KeyPositionTrackerNotification::kNotificationTypeFeatureAvailableVelocity, timestamp); + pressVelocityAvailableIndex_ = 0; + } + } + // ** Release Velocity ** + if(releaseVelocityWaitingForThresholdCross_) { + if(currentKeyPosition < releaseVelocityEscapementPosition_) + prepareReleaseVelocityFeature(currentBufferIndex, timestamp); + } + else if(releaseVelocityAvailableIndex_ != 0) { + if(currentBufferIndex >= releaseVelocityAvailableIndex_) { + // Can now calculate release velocity + currentlyAvailableFeatures_ |= KeyPositionTrackerNotification::kFeatureReleaseVelocity; + notifyFeature(KeyPositionTrackerNotification::kNotificationTypeFeatureAvailableReleaseVelocity, timestamp); + releaseVelocityAvailableIndex_ = 0; + } + } + // ** Percussiveness ** + if(percussivenessAvailableIndex_ != 0) { + if(currentBufferIndex >= percussivenessAvailableIndex_) { + // Can now calculate percussiveness + currentlyAvailableFeatures_ |= KeyPositionTrackerNotification::kFeaturePercussiveness; + notifyFeature(KeyPositionTrackerNotification::kNotificationTypeFeatureAvailablePercussiveness, timestamp); + percussivenessAvailableIndex_ = 0; + } + } + + // Major state transitions next, centered on whether the key is pressed + // fully or partially + if(currentState_ == kPositionTrackerStatePartialPressAwaitingMax || + currentState_ == kPositionTrackerStatePartialPressFoundMax) { + // These are collectively the pre-press states + if(currentKeyPosition >= kPositionTrackerPressPosition + kPositionTrackerPressHysteresis) { + // Key has gone far enough down to be considered pressed, but hasn't necessarily + // made it down yet. + pressIndex_ = 0; + pressPosition_ = missing_value<key_position>::missing(); + pressTimestamp_ = missing_value<timestamp_type>::missing(); + + changeState(kPositionTrackerStatePressInProgress, timestamp); + } + } + else if(currentState_ == kPositionTrackerStateReleaseInProgress || + currentState_ == kPositionTrackerStateReleaseFinished) { + if(currentKeyPosition >= kPositionTrackerPressPosition + kPositionTrackerPressHysteresis) { + // Key was releasing but is now back down. Need to reprime the start + // position information, which will be taken as the last minimum. + startIndex_ = currentMinIndex_; + startPosition_ = currentMinPosition_; + startTimestamp_ = currentMinTimestamp_; + pressIndex_ = 0; + pressPosition_ = missing_value<key_position>::missing(); + pressTimestamp_ = missing_value<timestamp_type>::missing(); + + changeState(kPositionTrackerStatePressInProgress, timestamp); + } + } + else if(currentState_ == kPositionTrackerStatePressInProgress) { + // Press has started, wait to find its max position before labeling the key as "down" + if(currentKeyPosition < kPositionTrackerPressPosition - kPositionTrackerPressHysteresis) { + // Key is on its way back up: find where release began + findKeyReleaseStart(timestamp); + + changeState(kPositionTrackerStateReleaseInProgress, timestamp); + } + } + else if(currentState_ == kPositionTrackerStateDown) { + if(currentKeyPosition < kPositionTrackerPressPosition - kPositionTrackerPressHysteresis) { + // Key is on its way back up: find where release began + findKeyReleaseStart(timestamp); + + changeState(kPositionTrackerStateReleaseInProgress, timestamp); + } + } + + // Find the maxima and minima of the key motion + if(missing_value<key_position>::isMissing(currentMaxPosition_) || + currentKeyPosition > currentMaxPosition_) { + // Found a new local maximum + currentMaxIndex_ = currentBufferIndex; + currentMaxPosition_ = currentKeyPosition; + currentMaxTimestamp_ = timestamp; + + // If we previously found a maximum, go back to the original + // state so we can process the new max that is in progress + if(currentState_ == kPositionTrackerStatePartialPressFoundMax) + changeState(kPositionTrackerStatePartialPressAwaitingMax, timestamp); + } + else if(missing_value<key_position>::isMissing(currentMinPosition_) || + currentKeyPosition < currentMinPosition_) { + // Found a new local minimum + currentMinIndex_ = currentBufferIndex; + currentMinPosition_ = currentKeyPosition; + currentMinTimestamp_ = timestamp; + } + + // Check if the deviation between min and max exceeds the threshold of significance, + // and if so, figure out when a peak occurs + if(!missing_value<key_position>::isMissing(currentMaxPosition_) && + !missing_value<key_position>::isMissing(lastMinMaxPosition_)) { + if(currentMaxPosition_ - lastMinMaxPosition_ >= kPositionTrackerMinMaxSpacingThreshold && currentBufferIndex != currentMaxIndex_) { + // We need to come down off the current maximum before we can be sure that we've found the right location. + // Implement a sliding threshold that gets lower the farther away from the maximum we get + key_position triggerThreshold = kPositionTrackerMinMaxSpacingThreshold / (key_position)(currentBufferIndex - currentMaxIndex_); + + if(currentKeyPosition < currentMaxPosition_ - triggerThreshold) { + // Found the local maximum and the position has already retreated from it + lastMinMaxPosition_ = currentMaxPosition_; + + if(currentState_ == kPositionTrackerStatePressInProgress) { + // If we were waiting for a press to complete, this is it. + pressIndex_ = currentMaxIndex_; + pressPosition_ = currentMaxPosition_; + pressTimestamp_ = currentMaxTimestamp_; + + // Insert the state change into the buffer timestamped according to + // when the maximum arrived, unless that would put it earlier than what's already there + timestamp_type stateChangeTimestamp = latestTimestamp() > currentMaxTimestamp_ ? latestTimestamp() : currentMaxTimestamp_; + changeState(kPositionTrackerStateDown, stateChangeTimestamp); + } + else if(currentState_ == kPositionTrackerStatePartialPressAwaitingMax) { + // Otherwise if we were waiting for a maximum to occur that was + // short of a full press, this might be it if it is of sufficient size + if(currentMaxPosition_ >= kPositionTrackerFirstMaxThreshold) { + timestamp_type stateChangeTimestamp = latestTimestamp() > currentMaxTimestamp_ ? latestTimestamp() : currentMaxTimestamp_; + changeState(kPositionTrackerStatePartialPressFoundMax, stateChangeTimestamp); + } + } + + // Reinitialize the minimum value for the next search + currentMinIndex_ = currentBufferIndex; + currentMinPosition_ = currentKeyPosition; + currentMinTimestamp_ = timestamp; + } + } + } + if(!missing_value<key_position>::isMissing(currentMinPosition_) && + !missing_value<key_position>::isMissing(lastMinMaxPosition_)) { + if(lastMinMaxPosition_ - currentMinPosition_ >= kPositionTrackerMinMaxSpacingThreshold && currentBufferIndex != currentMinIndex_) { + // We need to come up from the current minimum before we can be sure that we've found the right location. + // Implement a sliding threshold that gets lower the farther away from the minimum we get + key_position triggerThreshold = kPositionTrackerMinMaxSpacingThreshold / (key_position)(currentBufferIndex - currentMinIndex_); + + if(currentKeyPosition > currentMinPosition_ + triggerThreshold) { + // Found the local minimum and the position has already retreated from it + lastMinMaxPosition_ = currentMinPosition_; + + // If in the middle of releasing, see whether this minimum appears to have completed the release + if(currentState_ == kPositionTrackerStateReleaseInProgress) { + if(currentMinPosition_ < kPositionTrackerReleaseFinishPosition) { + releaseEndIndex_ = currentMinIndex_; + releaseEndPosition_ = currentMinPosition_; + releaseEndTimestamp_ = currentMinTimestamp_; + + timestamp_type stateChangeTimestamp = latestTimestamp() > currentMinTimestamp_ ? latestTimestamp() : currentMinTimestamp_; + changeState(kPositionTrackerStateReleaseFinished, stateChangeTimestamp); + } + } + + // Reinitialize the maximum value for the next search + currentMaxIndex_ = currentBufferIndex; + currentMaxPosition_ = currentKeyPosition; + currentMaxTimestamp_ = timestamp; + } + } + } +} + +// Change the current state of the tracker and generate a notification +void KeyPositionTracker::changeState(int newState, timestamp_type timestamp) { + KeyPositionTracker::key_buffer_index index; + KeyPositionTracker::key_buffer_index mostRecentIndex = 0; + + if(keyBuffer_.empty()) + mostRecentIndex = keyBuffer_.endIndex() - 1; + + // Manage features based on state + switch(newState) { + case kPositionTrackerStatePressInProgress: + // Clear features for a retrigger + if(currentState_ == kPositionTrackerStateReleaseInProgress || + currentState_ == kPositionTrackerStateReleaseFinished) + currentlyAvailableFeatures_ = KeyPositionTrackerNotification::kFeaturesNone; + + // Look for percussiveness first since it will always be available by the time of + // key press. That means we can count on it arriving before velocity every time. + if((currentlyAvailableFeatures_ & KeyPositionTrackerNotification::kFeaturePercussiveness) == 0 + && percussivenessAvailableIndex_ == 0) { + currentlyAvailableFeatures_ |= KeyPositionTrackerNotification::kFeaturePercussiveness; + notifyFeature(KeyPositionTrackerNotification::kNotificationTypeFeatureAvailablePercussiveness, timestamp); + percussivenessAvailableIndex_ = 0; + } + + // Start looking for the data needed for MIDI onset velocity. + // Where did the key cross the escapement position? How many more samples do + // we need to calculate velocity? + index = findMostRecentKeyPositionCrossing(pressVelocityEscapementPosition_, false, 1000); + if(index + kPositionTrackerSamplesNeededForPressVelocityAfterEscapement <= mostRecentIndex) { + // Here, we already have the velocity information + currentlyAvailableFeatures_ |= KeyPositionTrackerNotification::kFeaturePressVelocity; + notifyFeature(KeyPositionTrackerNotification::kNotificationTypeFeatureAvailableVelocity, timestamp); + } + else { + // Otherwise, we need to send a notification when the information becomes available + pressVelocityAvailableIndex_ = index + kPositionTrackerSamplesNeededForPressVelocityAfterEscapement; + } + break; + case kPositionTrackerStateReleaseInProgress: + // Start looking for the data needed for MIDI release velocity. + // Where did the key cross the release escaoentb position? How many more samples do + // we need to calculate velocity? + prepareReleaseVelocityFeature(mostRecentIndex, timestamp); + break; + case kPositionTrackerStatePartialPressFoundMax: + // Also look for the percussiveness features, if not already present + if((currentlyAvailableFeatures_ & KeyPositionTrackerNotification::kFeaturePercussiveness) == 0 + && percussivenessAvailableIndex_ == 0) { + currentlyAvailableFeatures_ |= KeyPositionTrackerNotification::kFeaturePercussiveness; + notifyFeature(KeyPositionTrackerNotification::kNotificationTypeFeatureAvailablePercussiveness, timestamp); + percussivenessAvailableIndex_ = 0; + } + break; + case kPositionTrackerStatePartialPressAwaitingMax: + case kPositionTrackerStateUnknown: + // Reset all features + currentlyAvailableFeatures_ = KeyPositionTrackerNotification::kFeaturesNone; + break; + case kPositionTrackerStateDown: + case kPositionTrackerStateReleaseFinished: + default: + // Don't change features + break; + } + + currentState_ = newState; + + KeyPositionTrackerNotification notification; + notification.type = KeyPositionTrackerNotification::kNotificationTypeStateChange; + notification.state = newState; + notification.features = currentlyAvailableFeatures_; + + insert(notification, timestamp); +} + +// Notify listeners that a given feature has become available +void KeyPositionTracker::notifyFeature(int notificationType, timestamp_type timestamp) { + // Can now calculate press velocity + KeyPositionTrackerNotification notification; + + notification.state = currentState_; + notification.type = notificationType; + notification.features = currentlyAvailableFeatures_; + + insert(notification, timestamp); +} + +// When starting from a blank state, retroactively locate +// the start of the key press so it can be used to calculate +// features of key motion +void KeyPositionTracker::findKeyPressStart(timestamp_type timestamp) { + if(keyBuffer_.size() < kPositionTrackerSamplesToAverageForStartVelocity + 1) + return; + + key_buffer_index index = keyBuffer_.endIndex() - 1; + int searchBackCounter = 0; + + while(index >= keyBuffer_.beginIndex() + kPositionTrackerSamplesToAverageForStartVelocity && searchBackCounter <= kPositionTrackerSamplesToSearchForStartLocation) { + // Take the N-sample velocity average and compare to a minimum threshold + key_position diffPosition = keyBuffer_[index] - keyBuffer_[index - kPositionTrackerSamplesToAverageForStartVelocity]; + timestamp_diff_type diffTimestamp = keyBuffer_.timestampAt(index) - keyBuffer_.timestampAt(index - kPositionTrackerSamplesToAverageForStartVelocity); + key_velocity velocity = calculate_key_velocity(diffPosition, diffTimestamp); + + if(velocity < kPositionTrackerStartVelocityThreshold) { + break; + } + + searchBackCounter++; + index--; + } + + // Having either found the minimum velocity or reached the beginning of the search period, + // store the key start information. Since the velocity is calculated over a window, choose + // a start position in the middle of the window. + startIndex_ = index - kPositionTrackerSamplesToAverageForStartVelocity/2; + startPosition_ = keyBuffer_[index - kPositionTrackerSamplesToAverageForStartVelocity/2]; + startTimestamp_ = keyBuffer_.timestampAt(index - kPositionTrackerSamplesToAverageForStartVelocity/2); + lastMinMaxPosition_ = startPosition_; + + // After saving that information, look further back for a specified number of samples to see if there + // is another mini-spike at the beginning of the key press. This can happen with highly percussive presses. + // If so, the start is actually the earlier time. + + // Leave index where it was... + searchBackCounter = 0; + bool haveFoundVelocitySpike = false, haveFoundNewMinimum = false; + + while(index >= keyBuffer_.beginIndex() + kPositionTrackerSamplesToAverageForStartVelocity && searchBackCounter <= kPositionTrackerSamplesToSearchBeyondStartLocation) { + // Take the N-sample velocity average and compare to a minimum threshold + key_position diffPosition = keyBuffer_[index] - keyBuffer_[index - kPositionTrackerSamplesToAverageForStartVelocity]; + timestamp_diff_type diffTimestamp = keyBuffer_.timestampAt(index) - keyBuffer_.timestampAt(index - kPositionTrackerSamplesToAverageForStartVelocity); + key_velocity velocity = calculate_key_velocity(diffPosition, diffTimestamp); + + if(velocity > kPositionTrackerStartVelocitySpikeThreshold) { + std::cout << "At index " << index << ", velocity is " << velocity << std::endl; + haveFoundVelocitySpike = true; + } + + if(velocity < kPositionTrackerStartVelocityThreshold && haveFoundVelocitySpike) { + std::cout << "At index " << index << ", velocity is " << velocity << std::endl; + haveFoundNewMinimum = true; + break; + } + + searchBackCounter++; + index--; + } + + if(haveFoundNewMinimum) { + // Here we looked back beyond a small spike and found an earlier start time + startIndex_ = index - kPositionTrackerSamplesToAverageForStartVelocity/2; + startPosition_ = keyBuffer_[index - kPositionTrackerSamplesToAverageForStartVelocity/2]; + startTimestamp_ = keyBuffer_.timestampAt(index - kPositionTrackerSamplesToAverageForStartVelocity/2); + lastMinMaxPosition_ = startPosition_; + + std::cout << "Found previous location\n"; + } +} + +// When a key is released, retroactively locate where the release started +void KeyPositionTracker::findKeyReleaseStart(timestamp_type timestamp) { + if(keyBuffer_.size() < kPositionTrackerSamplesToAverageForStartVelocity + 1) + return; + + key_buffer_index index = keyBuffer_.endIndex() - 1; + int searchBackCounter = 0; + + while(index >= keyBuffer_.beginIndex() + kPositionTrackerSamplesToAverageForStartVelocity && searchBackCounter <= kPositionTrackerSamplesToSearchForReleaseLocation) { + // Take the N-sample velocity average and compare to a minimum threshold + key_position diffPosition = keyBuffer_[index] - keyBuffer_[index - kPositionTrackerSamplesToAverageForStartVelocity]; + timestamp_diff_type diffTimestamp = keyBuffer_.timestampAt(index) - keyBuffer_.timestampAt(index - kPositionTrackerSamplesToAverageForStartVelocity); + key_velocity velocity = calculate_key_velocity(diffPosition, diffTimestamp); + + if(velocity > kPositionTrackerReleaseVelocityThreshold) { + std::cout << "Found release at index " << index << " (vel = " << velocity << ")\n"; + break; + } + + searchBackCounter++; + index--; + } + + // Having either found the minimum velocity or reached the beginning of the search period, + // store the key release information. + releaseBeginIndex_ = index - kPositionTrackerSamplesToAverageForStartVelocity/2; + releaseBeginPosition_ = keyBuffer_[index - kPositionTrackerSamplesToAverageForStartVelocity/2]; + releaseBeginTimestamp_ = keyBuffer_.timestampAt(index - kPositionTrackerSamplesToAverageForStartVelocity/2); + lastMinMaxPosition_ = releaseBeginPosition_; + + // Clear the release end position so there's no possibility of an inconsistent state + releaseEndIndex_ = 0; + releaseEndPosition_ = missing_value<key_position>::missing(); + releaseEndTimestamp_ = missing_value<timestamp_type>::missing(); +} + +// Find the index at which the key position crosses the given threshold. Returns 0 if not found. +KeyPositionTracker::key_buffer_index KeyPositionTracker::findMostRecentKeyPositionCrossing(key_position threshold, bool greaterThan, int maxDistance) { + if(keyBuffer_.empty()) + return 0; + + key_buffer_index index = keyBuffer_.endIndex() - 1; + int searchBackCounter = 0; + + // Check if the most recent sample already meets the criterion. If so, + // there's no crossing yet. + if(keyBuffer_[index] >= threshold && greaterThan) + return 0; + if(keyBuffer_[index] <= threshold && !greaterThan) + return 0; + + while(index >= keyBuffer_.beginIndex() && searchBackCounter <= maxDistance) { + if(keyBuffer_[index] >= threshold && greaterThan) + return index; + else if(keyBuffer_[index] <= threshold && !greaterThan) + return index; + + searchBackCounter++; + index--; + } + + return 0; +} + +void KeyPositionTracker::prepareReleaseVelocityFeature(KeyPositionTracker::key_buffer_index mostRecentIndex, timestamp_type timestamp) { + KeyPositionTracker::key_buffer_index index; + + // Find the sample where the key position crosses the release threshold. What is returned + // will be the last sample which is above the threshold. What we need is the first sample + // below the threshold plus at least one more (SamplesNeededForReleaseVelocity...) to + // perform a local velocity calculation. + index = findMostRecentKeyPositionCrossing(releaseVelocityEscapementPosition_, true, 1000); + + if(index == 0) { + // Haven't crossed the threshold yet + releaseVelocityWaitingForThresholdCross_ = true; + } + else if(index + kPositionTrackerSamplesNeededForReleaseVelocityAfterEscapement + 1 <= mostRecentIndex) { + // Here, we already have the velocity information + std::cout << "release available, at index = " << keyBuffer_[index] << ", most recent position = " << keyBuffer_[mostRecentIndex] << std::endl; + currentlyAvailableFeatures_ |= KeyPositionTrackerNotification::kFeatureReleaseVelocity; + notifyFeature(KeyPositionTrackerNotification::kNotificationTypeFeatureAvailableReleaseVelocity, timestamp); + releaseVelocityWaitingForThresholdCross_ = false; + } + else { + // Otherwise, we need to send a notification when the information becomes available + std::cout << "release available at index " << index + kPositionTrackerSamplesNeededForReleaseVelocityAfterEscapement + 1 << std::endl; + releaseVelocityAvailableIndex_ = index + kPositionTrackerSamplesNeededForReleaseVelocityAfterEscapement + 1; + releaseVelocityWaitingForThresholdCross_ = false; + } +} \ No newline at end of file