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.h: parses continuous key position and detects the
andrewm@0: state of the key.
andrewm@0: */
andrewm@0:
andrewm@0:
andrewm@0: #ifndef __touchkeys__KeyPositionTracker__
andrewm@0: #define __touchkeys__KeyPositionTracker__
andrewm@0:
andrewm@0: #include
andrewm@0: #include "../Utility/Node.h"
andrewm@0: #include "../Utility/Accumulator.h"
andrewm@0: #include "PianoTypes.h"
andrewm@0:
andrewm@0: // Three states of idle detector
andrewm@0: enum {
andrewm@0: kPositionTrackerStateUnknown = 0,
andrewm@0: kPositionTrackerStatePartialPressAwaitingMax,
andrewm@0: kPositionTrackerStatePartialPressFoundMax,
andrewm@0: kPositionTrackerStatePressInProgress,
andrewm@0: kPositionTrackerStateDown,
andrewm@0: kPositionTrackerStateReleaseInProgress,
andrewm@0: kPositionTrackerStateReleaseFinished
andrewm@0: };
andrewm@0:
andrewm@0: // Constants for key state detection
andrewm@0: const key_position kPositionTrackerPressPosition = scale_key_position(0.75);
andrewm@0: const key_position kPositionTrackerPressHysteresis = scale_key_position(0.05);
andrewm@0: const key_position kPositionTrackerMinMaxSpacingThreshold = scale_key_position(0.02);
andrewm@0: const key_position kPositionTrackerFirstMaxThreshold = scale_key_position(0.075);
andrewm@0: const key_position kPositionTrackerReleaseFinishPosition = scale_key_position(0.2);
andrewm@0:
andrewm@0: // How far back to search at the beginning to find the real start or release of a key press
andrewm@0: const int kPositionTrackerSamplesToSearchForStartLocation = 50;
andrewm@0: const int kPositionTrackerSamplesToSearchBeyondStartLocation = 20;
andrewm@0: const int kPositionTrackerSamplesToSearchForReleaseLocation = 100;
andrewm@0: const int kPositionTrackerSamplesToAverageForStartVelocity = 3;
andrewm@0: const key_velocity kPositionTrackerStartVelocityThreshold = scale_key_velocity(0.5);
andrewm@0: const key_velocity kPositionTrackerStartVelocitySpikeThreshold = scale_key_velocity(2.5);
andrewm@0: const key_velocity kPositionTrackerReleaseVelocityThreshold = scale_key_velocity(-0.2);
andrewm@0:
andrewm@0: // Constants for feature calculations. The first one is the approximate location of the escapement
andrewm@0: // (empirically measured on one piano, so only approximate), used for velocity calculations
andrewm@0: const key_position kPositionTrackerDefaultPositionForPressVelocityCalculation = scale_key_position(0.65);
andrewm@0: const key_position kPositionTrackerDefaultPositionForReleaseVelocityCalculation = scale_key_position(0.5);
andrewm@0: const key_position kPositionTrackerPositionThresholdForPercussivenessCalculation = scale_key_position(0.4);
andrewm@0: const int kPositionTrackerSamplesNeededForPressVelocityAfterEscapement = 1;
andrewm@0: const int kPositionTrackerSamplesNeededForReleaseVelocityAfterEscapement = 1;
andrewm@0:
andrewm@0: // KeyPositionTrackerNotification
andrewm@0: //
andrewm@0: // This class contains information on the notifications sent and stored by
andrewm@0: // KeyPositionTracker. Includes state changes and feature available.
andrewm@0:
andrewm@0: class KeyPositionTrackerNotification {
andrewm@0: public:
andrewm@0: enum {
andrewm@0: kNotificationTypeStateChange = 1,
andrewm@0: kNotificationTypeFeatureAvailableVelocity,
andrewm@0: kNotificationTypeFeatureAvailableReleaseVelocity,
andrewm@0: kNotificationTypeFeatureAvailablePercussiveness,
andrewm@0: kNotificationTypeNewMinimum,
andrewm@0: kNotificationTypeNewMaximum
andrewm@0: };
andrewm@0:
andrewm@0: enum {
andrewm@0: kFeaturesNone = 0,
andrewm@0: kFeaturePressVelocity = 0x0001,
andrewm@0: kFeatureReleaseVelocity = 0x0002,
andrewm@0: kFeaturePercussiveness = 0x0004
andrewm@0: };
andrewm@0:
andrewm@0: int type;
andrewm@0: int state;
andrewm@0: int features;
andrewm@0: };
andrewm@0:
andrewm@0: // KeyPositionTracker
andrewm@0: //
andrewm@0: // This class implements a state machine for a key that is currently active (not idle),
andrewm@0: // using continuous key position data to determine the location and parameters of key
andrewm@0: // presses and other events. It includes management of partial press patterns and detection
andrewm@0: // of percussiveness as well as velocity features.
andrewm@0: //
andrewm@0: // This class is triggered by new data points in the key position buffer. Its output is
andrewm@0: // a series of state changes which indicate what the key is doing.
andrewm@0:
andrewm@0: class KeyPositionTracker : public Node {
andrewm@0: public:
andrewm@0: typedef Node::size_type key_buffer_index;
andrewm@0: //typedef void (*KeyActionFunction)(KeyPositionTracker *object, void *userData);
andrewm@0:
andrewm@0: // Simple class to hold index/position/timestamp triads
andrewm@0: class Event {
andrewm@0: public:
andrewm@0: Event() : index(0), position(missing_value::missing()),
andrewm@0: timestamp(missing_value::missing()) {}
andrewm@0:
andrewm@0: Event(key_buffer_index i, key_position p, timestamp_type t)
andrewm@0: : index(i), position(p), timestamp(t) {}
andrewm@0:
andrewm@0: Event(const Event& obj)
andrewm@0: : index(obj.index), position(obj.position), timestamp(obj.timestamp) {}
andrewm@0:
andrewm@0: Event& operator=(const Event& obj) {
andrewm@0: index = obj.index;
andrewm@0: position = obj.position;
andrewm@0: timestamp = obj.timestamp;
andrewm@0: return *this;
andrewm@0: }
andrewm@0:
andrewm@0: key_buffer_index index;
andrewm@0: key_position position;
andrewm@0: timestamp_type timestamp;
andrewm@0: };
andrewm@0:
andrewm@0: // Collection of features related to whether a key is percussively played or not
andrewm@0: struct PercussivenessFeatures {
andrewm@0: float percussiveness; // Calculated single feature based on everything below
andrewm@0: Event velocitySpikeMaximum; // Maximum and minimum points of the initial
andrewm@0: Event velocitySpikeMinimum; // velocity spike on a percussive press
andrewm@0: timestamp_type timeFromStartToSpike; // How long it took to reach the velocity spike
andrewm@0: key_velocity areaPrecedingSpike; // Total sum of velocity values from start to max
andrewm@0: key_velocity areaFollowingSpike; // Total sum of velocity values from max to min
andrewm@0: };
andrewm@0:
andrewm@0: public:
andrewm@0: // ***** Constructors *****
andrewm@0:
andrewm@0: // Default constructor, passing the buffer on which to trigger
andrewm@0: KeyPositionTracker(capacity_type capacity, Node& keyBuffer);
andrewm@0:
andrewm@0: // Copy constructor
andrewm@0: //KeyPositionTracker(KeyPositionTracker const& obj);
andrewm@0:
andrewm@0: // ***** State Access *****
andrewm@0:
andrewm@0: // Whether this object is currently tracking states
andrewm@0: bool engaged() {
andrewm@0: return engaged_;
andrewm@0: }
andrewm@0:
andrewm@0: // Return the current state (unknown if nothing is in the buffer)
andrewm@0: int currentState() {
andrewm@0: return currentState_;
andrewm@0: }
andrewm@0:
andrewm@0: // Information about important recent points
andrewm@0: Event currentMax() {
andrewm@0: return Event(currentMaxIndex_, currentMaxPosition_, currentMaxTimestamp_);
andrewm@0: }
andrewm@0: Event currentMin() {
andrewm@0: return Event(currentMinIndex_, currentMinPosition_, currentMinTimestamp_);
andrewm@0: }
andrewm@0: Event pressStart() {
andrewm@0: return Event(startIndex_, startPosition_, startTimestamp_);
andrewm@0: }
andrewm@0: Event pressFinish() {
andrewm@0: return Event(pressIndex_, pressPosition_, pressTimestamp_);
andrewm@0: }
andrewm@0: Event releaseStart() {
andrewm@0: return Event(releaseBeginIndex_, releaseBeginPosition_, releaseBeginTimestamp_);
andrewm@0: }
andrewm@0: Event releaseFinish() {
andrewm@0: return Event(releaseEndIndex_, releaseEndPosition_, releaseEndTimestamp_);
andrewm@0: }
andrewm@0:
andrewm@0: // ***** Key Press Features *****
andrewm@0:
andrewm@0: // Velocity for onset and release. The values without an argument use the stored
andrewm@0: // current escapement point (which is also used for notification of availability).
andrewm@0: std::pair pressVelocity();
andrewm@0: std::pair releaseVelocity();
andrewm@0:
andrewm@0: std::pair pressVelocity(key_position escapementPosition);
andrewm@0: std::pair releaseVelocity(key_position returnPosition);
andrewm@0:
andrewm@0: // Set the threshold where we look for press velocity calculations. It
andrewm@0: // can be anything up to the press position threshold on the upward side
andrewm@0: // and anything down to the final release position on the downward side.
andrewm@0: void setPressVelocityEscapementPosition(key_position pos) {
andrewm@0: if(pos > kPositionTrackerPressPosition + kPositionTrackerPressHysteresis)
andrewm@0: pressVelocityEscapementPosition_ = kPositionTrackerPressPosition + kPositionTrackerPressHysteresis;
andrewm@0: else
andrewm@0: pressVelocityEscapementPosition_ = pos;
andrewm@0: }
andrewm@0: void setReleaseVelocityEscapementPosition(key_position pos) {
andrewm@0: if(pos < kPositionTrackerReleaseFinishPosition)
andrewm@0: releaseVelocityEscapementPosition_ = kPositionTrackerReleaseFinishPosition;
andrewm@0: else
andrewm@0: releaseVelocityEscapementPosition_ = pos;
andrewm@0: }
andrewm@0:
andrewm@0:
andrewm@0: // Percussiveness (struck vs. pressed keys)
andrewm@0: PercussivenessFeatures pressPercussiveness();
andrewm@0:
andrewm@0: // ***** Modifiers *****
andrewm@0:
andrewm@0: // Register for updates from the key positon buffer
andrewm@0: void engage();
andrewm@0:
andrewm@0: // Unregister for updates from the key position buffer
andrewm@0: void disengage();
andrewm@0:
andrewm@0: // Reset the state back initial values
andrewm@0: void reset();
andrewm@0:
andrewm@0: // ***** Evaluator *****
andrewm@0:
andrewm@0: // This method receives triggers whenever a new sample enters the buffer. It updates
andrewm@0: // the state depending on the profile of the key position.
andrewm@0: void triggerReceived(TriggerSource* who, timestamp_type timestamp);
andrewm@0:
andrewm@0: private:
andrewm@0: // ***** Internal Helper Methods *****
andrewm@0:
andrewm@0: // Change the current state
andrewm@0: void changeState(int newState, timestamp_type timestamp);
andrewm@0:
andrewm@0: // Insert a new feature notification
andrewm@0: void notifyFeature(int notificationType, timestamp_type timestamp);
andrewm@0:
andrewm@0: // Work backwards in the key position buffer to find the start/release of a press
andrewm@0: void findKeyPressStart(timestamp_type timestamp);
andrewm@0: void findKeyReleaseStart(timestamp_type timestamp);
andrewm@0:
andrewm@0: // Generic method to find the most recent crossing of a given point
andrewm@0: key_buffer_index findMostRecentKeyPositionCrossing(key_position threshold, bool greaterThan, int maxDistance);
andrewm@0:
andrewm@0: // Look for the crossing of the release velocity threshold to prepare to send the feature
andrewm@0: void prepareReleaseVelocityFeature(KeyPositionTracker::key_buffer_index mostRecentIndex, timestamp_type timestamp);
andrewm@0:
andrewm@0: // ***** Member Variables *****
andrewm@0:
andrewm@0: Node& keyBuffer_; // Raw key position data
andrewm@0: bool engaged_; // Whether we're actively listening to incoming updates
andrewm@0: int currentState_; // Our current state
andrewm@0: int currentlyAvailableFeatures_; // Which features can be calculated for the current press
andrewm@0:
andrewm@0: // Position tracking information for significant points (minima and maxima)
andrewm@0: key_position startPosition_; // Position of where the key press started
andrewm@0: timestamp_type startTimestamp_; // Timestamp of where the key press started
andrewm@0: key_buffer_index startIndex_; // Index in the buffer where the start occurred
andrewm@0: key_position pressPosition_; // Position of where the key is fully pressed
andrewm@0: timestamp_type pressTimestamp_; // Timestamp of where the key is fully pressed
andrewm@0: key_buffer_index pressIndex_; // Index in the buffer where the press occurred
andrewm@0: key_position releaseBeginPosition_; // Position of where the key release began
andrewm@0: timestamp_type releaseBeginTimestamp_; // Timestamp of where the key release began
andrewm@0: key_buffer_index releaseBeginIndex_; // Index in the buffer of where the key release began
andrewm@0: key_position releaseEndPosition_; // Position of where the key release ended
andrewm@0: timestamp_type releaseEndTimestamp_; // Timestamp of where the key release ended
andrewm@0: key_buffer_index releaseEndIndex_; // Index in the buffer of where the key release ended
andrewm@0: key_position currentMinPosition_, currentMaxPosition_; // Running min and max key position
andrewm@0: timestamp_type currentMinTimestamp_, currentMaxTimestamp_; // Times for the above positions
andrewm@0: key_buffer_index currentMinIndex_, currentMaxIndex_; // Indices in the buffer for the recent min/max
andrewm@0: key_position lastMinMaxPosition_; // Position of the last significant point
andrewm@0:
andrewm@0: // Persistent parameters relating to feature calculation
andrewm@0: key_position pressVelocityEscapementPosition_; // Position at which onset velocity is calculated
andrewm@0: key_position releaseVelocityEscapementPosition_; // Position at which release velocity is calculate
andrewm@0: key_buffer_index pressVelocityAvailableIndex_; // When we can calculate press velocity
andrewm@0: key_buffer_index releaseVelocityAvailableIndex_; // When we can calculate release velocity
andrewm@0: bool releaseVelocityWaitingForThresholdCross_; // Set to true if we need to look for release escapement cross
andrewm@0: key_buffer_index percussivenessAvailableIndex_; // When we can calculate percussiveness features
andrewm@0:
andrewm@0: /*
andrewm@0: typedef struct {
andrewm@0: int runningSum; // sum of last N points (i.e. mean * N)
andrewm@0: int runningSumMaxLength; // the value of N above
andrewm@0: int runningSumCurrentLength; // How many values are actually part of the sum right now (transient condition)
andrewm@0: int startValuesSum; // sum of the last N start values (to calculate returning quiescent position)
andrewm@0: int startValuesSumMaxLength;
andrewm@0: int startValuesSumCurrentLength;
andrewm@0:
andrewm@0: int maxVariation; // The maximum deviation from mean of the last group of samples
andrewm@0: int flatCounter; // how many successive samples have been "flat" (minimal change)
andrewm@0: int currentStartValue; // values and positions of several key points for active keys
andrewm@0: int currentStartPosition;
andrewm@0: int currentMinValue;
andrewm@0: int currentMinPosition;
andrewm@0: int currentMaxValue;
andrewm@0: int currentMaxPosition;
andrewm@0: int lastKeyPointValue; // the value of the last important point {start, max, min}
andrewm@0:
andrewm@0: deque recentKeyPoints; // the minima and maxima since the key started
andrewm@0: bool sentPercussiveMidiOn; // HACK: whether we've sent the percussive MIDI event
andrewm@0:
andrewm@0: int pressValue; // the value at the maximum corresponding to the end of the key press motion
andrewm@0: int pressPosition; // the location in the buffer of this event (note: not the timestamp)
andrewm@0: int releaseValue; // the value the key held right before release
andrewm@0: int releasePosition; // the location in the buffer of the release corner
andrewm@0: } keyParameters;
andrewm@0: */
andrewm@0: };
andrewm@0:
andrewm@0:
andrewm@0: #endif /* defined(__touchkeys__KeyPositionTracker__) */