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