view Source/Mappings/MRPMapping.cpp @ 20:dfff66c07936

Lots of minor changes to support building on Visual Studio. A few MSVC-specific #ifdefs to eliminate things Visual Studio doesn't like. This version now compiles on Windows (provided liblo, Juce and pthread are present) but the TouchKeys device support is not yet enabled. Also, the code now needs to be re-checked on Mac and Linux.
author Andrew McPherson <andrewm@eecs.qmul.ac.uk>
date Sun, 09 Feb 2014 18:40:51 +0000
parents 3580ffe87dc8
children
line wrap: on
line source
/*
  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/>.
 
  =====================================================================

  MRPMapping.cpp: mapping class for magnetic resonator piano using continuous
  key position.
*/

#include "MRPMapping.h"
#include <vector>

// Class constants
// Useful constants for mapping MRP messages
const int MRPMapping::kMIDINoteOnMessage = 0x90;
const int MRPMapping::kDefaultMIDIChannel = 15;
const float MRPMapping::kDefaultAftertouchScaler = 100.0;

// Parameters for vibrato detection and mapping
const key_velocity MRPMapping::kVibratoVelocityThreshold = scale_key_velocity(2.0);
const timestamp_diff_type MRPMapping::kVibratoMinimumPeakSpacing = microseconds_to_timestamp(60000);
const timestamp_diff_type MRPMapping::kVibratoTimeout = microseconds_to_timestamp(500000);
const int MRPMapping::kVibratoMinimumOscillations = 4;
const float MRPMapping::kVibratoRateScaler = 0.005;

// Main constructor takes references/pointers from objects which keep track
// of touch location, continuous key position and the state detected from that
// position. The PianoKeyboard object is strictly required as it gives access to
// Scheduler and OSC methods. The others are optional since any given system may
// contain only one of continuous key position or touch sensitivity
MRPMapping::MRPMapping(PianoKeyboard &keyboard, MappingFactory *factory, int noteNumber, Node<KeyTouchFrame>* touchBuffer,
                     Node<key_position>* positionBuffer, KeyPositionTracker* positionTracker)
: Mapping(keyboard, factory, noteNumber, touchBuffer, positionBuffer, positionTracker),
  noteIsOn_(false), lastIntensity_(missing_value<float>::missing()),
  lastBrightness_(missing_value<float>::missing()), lastPitch_(missing_value<float>::missing()),
  lastHarmonic_(missing_value<float>::missing()),
  shouldLookForPitchBends_(true), rawVelocity_(kMRPMappingVelocityBufferLength),
  filteredVelocity_(kMRPMappingVelocityBufferLength, rawVelocity_), lastCalculatedVelocityIndex_(0),
  vibratoActive_(false), vibratoVelocityPeakCount_(0), vibratoLastPeakTimestamp_(missing_value<timestamp_type>::missing())
{
    setAftertouchSensitivity(1.0);
    
    // Initialize the filter coefficients for filtered key velocity (used for vibrato detection)
    std::vector<double> bCoeffs, aCoeffs;
    designSecondOrderLowpass(bCoeffs, aCoeffs, 15.0, 0.707, 1000.0);
    std::vector<float> bCf(bCoeffs.begin(), bCoeffs.end()), aCf(aCoeffs.begin(), aCoeffs.end());
    filteredVelocity_.setCoefficients(bCf, aCf);
}

// Copy constructor
/*MRPMapping::MRPMapping(MRPMapping const& obj)
: Mapping(obj), lastIntensity_(obj.lastIntensity_), lastBrightness_(obj.lastBrightness_),
aftertouchScaler_(obj.aftertouchScaler_), noteIsOn_(obj.noteIsOn_), lastPitch_(obj.lastPitch_),
lastHarmonic_(obj.lastHarmonic_),
shouldLookForPitchBends_(obj.shouldLookForPitchBends_), activePitchBends_(obj.activePitchBends_),
rawVelocity_(obj.rawVelocity_), filteredVelocity_(obj.filteredVelocity_),
lastCalculatedVelocityIndex_(obj.lastCalculatedVelocityIndex_), vibratoActive_(obj.vibratoActive_),
vibratoVelocityPeakCount_(obj.vibratoVelocityPeakCount_), vibratoLastPeakTimestamp_(obj.vibratoLastPeakTimestamp_) {

}*/

MRPMapping::~MRPMapping() {
    //std::cerr << "~MRPMapping(): " << this << std::endl;
    
    try {
        disengage();
    }
    catch(...) {
        std::cerr << "~MRPMapping(): exception during disengage()\n";
    }
    
    //std::cerr << "~MRPMapping(): done\n";
}

// Turn off mapping of data. Remove our callback from the scheduler
void MRPMapping::disengage() {
    Mapping::disengage();
    if(noteIsOn_) {
        int newNoteNumber = noteNumber_;
        //int newNoteNumber = ((noteNumber_ - 21) * 25)%88 + 21;
        keyboard_.sendMessage("/mrp/midi",
                              "iii", (int)(kMIDINoteOnMessage + kDefaultMIDIChannel), (int)newNoteNumber, (int)0, LO_ARGS_END);
       // if(!touchBuffer_->empty())
       //     keyboard_.testLog_ << touchBuffer_->latestTimestamp() << " /mrp/midi iii " << (kMIDINoteOnMessage + kDefaultMIDIChannel) << " " << newNoteNumber << " " << 0 << endl;
        
        // Reset qualities
        lastPitch_ = lastHarmonic_ = lastBrightness_ = lastIntensity_ = missing_value<float>::missing();
    }
    noteIsOn_ = false;
    shouldLookForPitchBends_ = true;
}

// Reset state back to defaults
void MRPMapping::reset() {
    Mapping::reset();
    noteIsOn_ = false;
    shouldLookForPitchBends_ = true;
}

// Set the aftertouch sensitivity on continuous key position
// 0 means no aftertouch, 1 means default sensitivity, upward
// from there
void MRPMapping::setAftertouchSensitivity(float sensitivity) {
    if(sensitivity <= 0)
        aftertouchScaler_ = 0;
    else
        aftertouchScaler_ = kDefaultAftertouchScaler * sensitivity;
}

// This is called by another MRPMapping when it finds a pitch bend starting.
// Add the sending note to our list of bends, with the sending note marked
// as controlling the bend
void MRPMapping::enablePitchBend(int toNote, Node<key_position>* toPositionBuffer,
                                KeyPositionTracker *toPositionTracker) {
    if(toPositionBuffer == 0 || toPositionTracker == 0)
        return;
    
    std::cout << "enablePitchBend(): this note = " << noteNumber_ << " note = " << toNote << " posBuf = " << toPositionBuffer << " posTrack = " << toPositionTracker << "\n";
    PitchBend newBend = {toNote, true, false, toPositionBuffer, toPositionTracker};
    activePitchBends_.push_back(newBend);
}

// Trigger method. This receives updates from the TouchKey data or from state changes in
// the continuous key position (KeyPositionTracker). It will potentially change the scheduled
// behavior of future mapping calls, but the actual OSC messages should be transmitted in a different
// thread.
void MRPMapping::triggerReceived(TriggerSource* who, timestamp_type timestamp) {
    if(who == 0)
        return;
    if(who == positionTracker_) {
        // The state of the key (based on continuous position) just changed.
        // Might want to alter our mapping strategy.
    }
    else if(who == touchBuffer_) {
        // TouchKey data is available
    }
}

// Mapping method. This actually does the real work of sending OSC data in response to the
// latest information from the touch sensors or continuous key angle
timestamp_type MRPMapping::performMapping() {
    if(!engaged_)
        return 0;
    
    timestamp_type currentTimestamp = keyboard_.schedulerCurrentTimestamp();
    float intensity = 0;
    float brightness = 0;
    float pitch = 0;
    float harmonic = 0;

    // Calculate the output features as a function of input sensor data
    if(positionBuffer_ == 0) {
        // No buffer -> all 0
    }
    else if(positionBuffer_->empty()) {
        // No samples -> all 0
    }
    else {
        // TODO: IIR filter on the position data before mapping it
        key_position latestPosition = positionBuffer_->latest();
        int trackerState = kPositionTrackerStateUnknown;
        if(positionTracker_ != 0)
            trackerState = positionTracker_->currentState();
        
        // Get the latest velocity measurements
        key_velocity latestVelocity = updateVelocityMeasurements();
        
        // Every time we enter a state of PartialPress, check whether this key
        // is part of a multi-key pitch bend gesture with another key that's already
        // down. Only do this once, though, since keys that go down after we enter
        // PartialPress state are not part of such a gesture.
        if(shouldLookForPitchBends_) {
            if(trackerState == kPositionTrackerStatePartialPressAwaitingMax ||
               trackerState == kPositionTrackerStatePartialPressFoundMax) {
                // Look for a pitch bend gesture by searching for neighboring
                // keys which are in the Down state and reached that state before
                // this one reached PartialPress state.
                for(int neighborNote = noteNumber_ - 2; neighborNote < noteNumber_; neighborNote++) {
                    // If one of the lower keys is in the Down state, then this note should bend it up
                    MRPMapping *neighborMapper = dynamic_cast<MRPMapping*>(keyboard_.mapping(neighborNote));
                    if(neighborMapper == 0)
                        continue;
                    if(neighborMapper->positionTracker_ != 0) {
                        int neighborState = neighborMapper->positionTracker_->currentState();
                        if(neighborState == kPositionTrackerStateDown) {
                            // Here we've found a neighboring note in the Down state. But did it precede our transition?
                            timestamp_type timeOfDownTransition = neighborMapper->positionTracker_->latestTimestamp();
                            timestamp_type timeOfOurPartialActivation = findTimestampOfPartialPress();
                            
                            cout << "Found key " << neighborNote << " in Down state\n";
                            
                            if(!missing_value<timestamp_type>::isMissing(timeOfOurPartialActivation)) {
                                if(timeOfOurPartialActivation > timeOfDownTransition) {
                                    // The neighbor note went down before us; pitch bend should engage
                                    cout << "Found pitch bend: " << noteNumber_ << " to " << neighborNote << endl;
                                    
                                    // Insert the details for the neighboring note into our buffer. The bend
                                    // is controlled by our own key, and the target is the neighbor note.
                                    PitchBend newBend = {neighborNote, false, false, neighborMapper->positionBuffer_,
                                        neighborMapper->positionTracker_};
                                    activePitchBends_.push_back(newBend);
                                
                                    // Tell the other note to bend its pitch based on our position
                                    neighborMapper->enablePitchBend(noteNumber_, positionBuffer_, positionTracker_);
                                }
                            }
                        }
                    }
                }
                for(int neighborNote = noteNumber_ + 1; neighborNote < noteNumber_ + 3; neighborNote++) {
                    // If one of the upper keys is in the Down state, then this note should bend it down
                    MRPMapping *neighborMapper = dynamic_cast<MRPMapping*>(keyboard_.mapping(neighborNote));
                    if(neighborMapper == 0)
                        continue;
                    if(neighborMapper->positionTracker_ != 0) {
                        int neighborState = neighborMapper->positionTracker_->currentState();
                        if(neighborState == kPositionTrackerStateDown) {
                            // Here we've found a neighboring note in the Down state. But did it precede our transition?
                            timestamp_type timeOfDownTransition = neighborMapper->positionTracker_->latestTimestamp();
                            timestamp_type timeOfOurPartialActivation = findTimestampOfPartialPress();
                            
                            cout << "Found key " << neighborNote << " in Down state\n";
                            
                            if(!missing_value<timestamp_type>::isMissing(timeOfOurPartialActivation)) {
                                if(timeOfOurPartialActivation > timeOfDownTransition) {
                                    // The neighbor note went down before us; pitch bend should engage
                                    cout << "Found pitch bend: " << noteNumber_ << " to " << neighborNote << endl;
                                    
                                    // Insert the details for the neighboring note into our buffer. The bend
                                    // is controlled by our own key, and the target is the neighbor note.
                                    PitchBend newBend = {neighborNote, false, false, neighborMapper->positionBuffer_,
                                        neighborMapper->positionTracker_};
                                    activePitchBends_.push_back(newBend);
                                    
                                    // Tell the other note to bend its pitch based on our position
                                    neighborMapper->enablePitchBend(noteNumber_, positionBuffer_, positionTracker_);
                                }
                            }
                        }
                    }
                }
                
                shouldLookForPitchBends_ = false;
            }
        }
        
        if(trackerState == kPositionTrackerStatePartialPressAwaitingMax ||
           trackerState == kPositionTrackerStatePartialPressFoundMax) {
            // Look for active vibrato gestures which are defined as oscillating
            // motion in the key velocity. They could conceivably occur at a variety
            // of raw key positions, as long as the key is not yet down
            
            if(missing_value<timestamp_type>::isMissing(vibratoLastPeakTimestamp_))
                vibratoLastPeakTimestamp_ = currentTimestamp;
            
            if(vibratoVelocityPeakCount_ % 2 == 0) {
                if(latestVelocity > kVibratoVelocityThreshold && currentTimestamp - vibratoLastPeakTimestamp_ > kVibratoMinimumPeakSpacing) {
                    std::cout << "Vibrato count = " << vibratoVelocityPeakCount_ << std::endl;
                    vibratoVelocityPeakCount_++;
                    vibratoLastPeakTimestamp_ = currentTimestamp;
                }
            }
            else {
                if(latestVelocity < -kVibratoVelocityThreshold && currentTimestamp - vibratoLastPeakTimestamp_ > kVibratoMinimumPeakSpacing) {
                    std::cout << "Vibrato count = " << vibratoVelocityPeakCount_ << std::endl;
                    vibratoVelocityPeakCount_++;
                    vibratoLastPeakTimestamp_ = currentTimestamp;
                }
            }
            
            if(vibratoVelocityPeakCount_ >= kVibratoMinimumOscillations) {
                vibratoActive_ = true;
            }
            
            
            if(vibratoActive_) {
                // Update the harmonic parameter, which increases linearly with the absolute
                // value of velocity. The value will accumulate over the course of a vibrato
                // gesture and retain its value when the vibrato finishes. It reverts to minimum
                // when the note finishes.
                if(missing_value<float>::isMissing(lastHarmonic_))
                    lastHarmonic_ = 0.0;
                harmonic = lastHarmonic_ + fabsf(latestVelocity) * kVibratoRateScaler;
                std::cout << "harmonic = " << harmonic << std::endl;
                
                // Check whether the current vibrato has timed out
                if(currentTimestamp - vibratoLastPeakTimestamp_ > kVibratoTimeout) {
                    std::cout << "Vibrato timed out\n";
                    vibratoActive_ = false;
                    vibratoVelocityPeakCount_ = 0;
                    vibratoLastPeakTimestamp_ = currentTimestamp;
                }
            }
        }
        else {
            // Vibrato can't be active in these states
            //std::cout << "Vibrato finished from state change\n";
            vibratoActive_ = false;
            vibratoVelocityPeakCount_ = 0;
            vibratoLastPeakTimestamp_ = currentTimestamp;
        }
        
        if(trackerState != kPositionTrackerStateReleaseFinished) {
            // For all active states except post-release, calculate
            // Intensity and Brightness parameters based on key position
            
            if(latestPosition > 1.0) {
                intensity = 1.0;
                brightness = (latestPosition - 1.0) * aftertouchScaler_;
            }
            else if(latestPosition < 0.0) {
                intensity = 0.0;
                brightness = 0.0;
            }
            else {
                intensity = latestPosition;
                brightness = 0.0;
            }
            
            if(!activePitchBends_.empty()) {
                // Look for active multi-key pitch bend gestures
                std::vector<PitchBend>::iterator it = activePitchBends_.begin();
                pitch = 0.0;
                
                for(it = activePitchBends_.begin(); it != activePitchBends_.end(); it++) {
                    PitchBend& bend(*it);

                    if(bend.isControllingBend) {
                        // First find out of the bending key is still in a PartialPress state
                        // If not, remove it and move on
                        if((bend.positionTracker->currentState() != kPositionTrackerStatePartialPressAwaitingMax &&
                           bend.positionTracker->currentState() != kPositionTrackerStatePartialPressFoundMax)
                           || !bend.positionTracker->engaged()) {
                            cout << "Removing bend from note " << bend.note << endl;
                            bend.isFinished = true;
                            continue;
                        }
                        
                        // This is the case where the other note is controlling our pitch
                        if(bend.positionBuffer->empty()) {
                            continue;
                        }
                        
                        float noteDifference = (float)(bend.note - noteNumber_);
                        key_position latestBenderPosition = bend.positionBuffer->latest();
                        
                        // Key position at 0 = 0 pitch bend; key position at max = most pitch bend
                        float bendAmount = key_position_to_float(latestBenderPosition - kPianoKeyDefaultIdlePositionThreshold*2) /
                                                key_position_to_float(1.0 - kPianoKeyDefaultIdlePositionThreshold*2);
                        if(bendAmount < 0)
                            bendAmount = 0;
                        pitch += noteDifference * bendAmount;
                    }
                    else {
                        // This is the case where we're controlling the other note's pitch. Our own
                        // pitch is the inverse of what we're sending to the neighboring note.
                        // Compared to the above case, we know a few things since we're using our own
                        // position: the buffer isn't empty and the tracker is engaged.
                        
                        if(trackerState != kPositionTrackerStatePartialPressAwaitingMax &&
                           trackerState != kPositionTrackerStatePartialPressFoundMax) {
                            cout << "Removing our bend on note " << bend.note << endl;
                            bend.isFinished = true;
                            continue;
                        }
  
                        float noteDifference = (float)(bend.note - noteNumber_);
                        
                        // Key position at 0 = 0 pitch bend; key position at max = most pitch bend
                        float bendAmount = key_position_to_float(latestPosition - kPianoKeyDefaultIdlePositionThreshold*2) /
                                            key_position_to_float(1.0 - kPianoKeyDefaultIdlePositionThreshold*2);
                        if(bendAmount < 0)
                            bendAmount = 0;
                        pitch += noteDifference * (1.0 - bendAmount);
                    }
                }
                
                // Now reiterate to remove any of them that have finished
                it = activePitchBends_.begin();
                
                while(it != activePitchBends_.end()) {
                    if(it->isFinished) {
                        // Go back to beginning and look again after erasing each one
                        // This isn't very efficient but there will never be more than 4 elements anyway
                        activePitchBends_.erase(it);
                        it = activePitchBends_.begin();
                    }
                    else
                        it++;
                }
                
                std::cout << "pitch = " << pitch << std::endl;
            }
            else
                pitch = 0.0;
        }
        else {
            intensity = 0.0;
            brightness = 0.0;
            if(noteIsOn_) {
                        int newNoteNumber = noteNumber_;
                //int newNoteNumber = ((noteNumber_ - 21) * 25)%88 + 21;
                keyboard_.sendMessage("/mrp/midi",
                                      "iii", (int)(kMIDINoteOnMessage + kDefaultMIDIChannel), (int)newNoteNumber, (int)0, LO_ARGS_END);
                //keyboard_.testLog_ << currentTimestamp << " /mrp/midi iii " << (kMIDINoteOnMessage + kDefaultMIDIChannel) << " " << newNoteNumber << " " << 0 << endl;
            }
            noteIsOn_ = false;
            shouldLookForPitchBends_ = true;
        }
    }
    
    // TODO: TouchKeys mapping
    
    // Send OSC message with these parameters unless they are unchanged from before
    if(!noteIsOn_ && intensity > 0.0) {
                int newNoteNumber = noteNumber_;
        //int newNoteNumber = ((noteNumber_ - 21) * 25)%88 + 21;
        keyboard_.sendMessage("/mrp/midi",
                              "iii", (int)(kMIDINoteOnMessage + kDefaultMIDIChannel), (int)newNoteNumber, (int)127, LO_ARGS_END);
        //keyboard_.testLog_ << currentTimestamp << " /mrp/midi iii " << (kMIDINoteOnMessage + kDefaultMIDIChannel) << " " << newNoteNumber << " " << 127 << endl;
        noteIsOn_ = true;
    }
    
    // Set key LED color according to key parameters
    // Partial press --> green of varying intensity
    // Aftertouch (brightness) --> green moving to red depending on brightness parameter
    // Pitch bend --> note bends toward blue as pitch value departs from center
    // Harmonic glissando --> cycle through hues with whitish tint (lower saturation)
    if(intensity != lastIntensity_ || brightness != lastBrightness_ || pitch != lastPitch_ || harmonic != lastHarmonic_) {
        if(harmonic != 0.0) {
            float hue = fmodf(harmonic, 1.0);
            keyboard_.setKeyLEDColorHSV(noteNumber_, hue, 0.25, 0.5);
        }
        else if(intensity >= 1.0) {
            if(pitch != 0.0)
                keyboard_.setKeyLEDColorHSV(noteNumber_, 0.33 + 0.33 * fabsf(pitch) - (brightness * 0.2), 1.0, intensity);
            else
                keyboard_.setKeyLEDColorHSV(noteNumber_, 0.33 - (brightness * 0.2), 1.0, 1.0);
        }
        else {
            if(pitch != 0.0)
                keyboard_.setKeyLEDColorHSV(noteNumber_, 0.33 + 0.33 * fabsf(pitch), 1.0, intensity);
            else
                keyboard_.setKeyLEDColorHSV(noteNumber_, 0.33, 1.0, intensity);
        }
    }
        
    if(intensity != lastIntensity_) {
                int newNoteNumber = noteNumber_;
        //int newNoteNumber = ((noteNumber_ - 21) * 25)%88 + 21;
        keyboard_.sendMessage("/mrp/quality/intensity",
                              "iif", (int)kDefaultMIDIChannel, (int)newNoteNumber, (float)intensity, LO_ARGS_END);
        //keyboard_.testLog_ << currentTimestamp << " /mrp/quality/intensity iif " << kDefaultMIDIChannel << " " << newNoteNumber << " " << intensity << endl;
    }
    if(brightness != lastBrightness_) {
                int newNoteNumber = noteNumber_;
        //int newNoteNumber = ((noteNumber_ - 21) * 25)%88 + 21;
        keyboard_.sendMessage("/mrp/quality/brightness",
                              "iif", (int)kDefaultMIDIChannel, (int)newNoteNumber, (float)brightness, LO_ARGS_END);
        //keyboard_.testLog_ << currentTimestamp << " /mrp/quality/brightness iif " << kDefaultMIDIChannel << " " << newNoteNumber << " " << brightness << endl;
    }
    if(pitch != lastPitch_) {
                int newNoteNumber = noteNumber_;
        //int newNoteNumber = ((noteNumber_ - 21) * 25)%88 + 21;
        keyboard_.sendMessage("/mrp/quality/pitch",
                              "iif", (int)kDefaultMIDIChannel, (int)newNoteNumber, (float)pitch, LO_ARGS_END);
        //keyboard_.testLog_ << currentTimestamp << " /mrp/quality/pitch iif " << kDefaultMIDIChannel << " " << newNoteNumber << " " << pitch << endl;
    }
    if(harmonic != lastHarmonic_) {
        int newNoteNumber = noteNumber_;
        //int newNoteNumber = ((noteNumber_ - 21) * 25)%88 + 21;
        keyboard_.sendMessage("/mrp/quality/harmonic",
                              "iif", (int)kDefaultMIDIChannel, (int)newNoteNumber, (float)harmonic, LO_ARGS_END);
        //keyboard_.testLog_ << currentTimestamp << " /mrp/quality/harmonic iif " << kDefaultMIDIChannel << " " << newNoteNumber << " " << harmonic << endl;
    }
    
    lastIntensity_ = intensity;
    lastBrightness_ = brightness;
    lastPitch_ = pitch;
    lastHarmonic_ = harmonic;
    
    // Register for the next update by returning its timestamp
    nextScheduledTimestamp_ = currentTimestamp + updateInterval_;
    return nextScheduledTimestamp_;
}

// Helper function that brings the velocity buffer up to date with the latest
// samples. Velocity is not updated on every new position sample since it's not
// efficient to run that many triggers all the time. Instead, it's brought up to
// date on an as-needed basis during performMapping().
key_velocity MRPMapping::updateVelocityMeasurements() {
    positionBuffer_->lock_mutex();
    
    // Need at least 2 samples to calculate velocity (first difference)
    if(positionBuffer_->size() < 2) {
        positionBuffer_->unlock_mutex();
        return missing_value<key_velocity>::missing();
    }
    
    if(lastCalculatedVelocityIndex_ < positionBuffer_->beginIndex() + 1) {
        // Fell off the beginning of the position buffer. Reset calculations.
        filteredVelocity_.clear();
        rawVelocity_.clear();
        lastCalculatedVelocityIndex_ = positionBuffer_->beginIndex() + 1;
    }
    
    while(lastCalculatedVelocityIndex_ < positionBuffer_->endIndex()) {
        // Calculate the velocity and add to buffer
        key_position diffPosition = (*positionBuffer_)[lastCalculatedVelocityIndex_] - (*positionBuffer_)[lastCalculatedVelocityIndex_ - 1];
        timestamp_diff_type diffTimestamp = positionBuffer_->timestampAt(lastCalculatedVelocityIndex_) - positionBuffer_->timestampAt(lastCalculatedVelocityIndex_ - 1);
        key_velocity vel;
        
        if(diffTimestamp != 0)
            vel = calculate_key_velocity(diffPosition, diffTimestamp);
        else
            vel = 0; // Bad measurement: replace with 0 so as not to mess up IIR calculations
        
        // Add the raw velocity to the buffer
        rawVelocity_.insert(vel, positionBuffer_->timestampAt(lastCalculatedVelocityIndex_));
        lastCalculatedVelocityIndex_++;
    }
    
    positionBuffer_->unlock_mutex();
    
    // Bring the filtered velocity up to date
    key_velocity filteredVel = filteredVelocity_.calculate();
    //std::cout << "Key " << noteNumber_ << " velocity " << filteredVel << std::endl;
    return filteredVel;
}

// Helper function that locates the timestamp at which this key entered the
// PartialPress (i.e. first non-idle) state. Returns missing value if the
// state can't be located.
timestamp_type MRPMapping::findTimestampOfPartialPress() {
    if(positionTracker_ == 0)
        return missing_value<timestamp_type>::missing();
    if(positionTracker_->empty())
        return missing_value<timestamp_type>::missing();
    //Node<int>::reverse_iterator it = positionTracker_->rbegin();
    Node<int>::size_type index = positionTracker_->endIndex() - 1;
    bool foundPartialPressState = false;
    timestamp_type earliestPartialPressTimestamp;

    // Search backwards from present
    while(index >= positionTracker_->beginIndex()/*it != positionTracker_->rend()*/) {
        if((*positionTracker_)[index].state == kPositionTrackerStatePartialPressAwaitingMax ||
           (*positionTracker_)[index].state == kPositionTrackerStatePartialPressFoundMax) {
            cout << "index " << index << " state " << (*positionTracker_)[index].state << endl;
            foundPartialPressState = true;
            earliestPartialPressTimestamp = positionTracker_->timestampAt(index);
        }
        else {
            // This state is not a PartialPress state. Two cases: either
            // we haven't yet encountered a partial press or we have found
            // a state before the partial press, in which case the previous
            // state we found was the first.
                        cout << "index " << index << " state " << (*positionTracker_)[index].state << endl;
            if(foundPartialPressState) {
                return earliestPartialPressTimestamp;
            }
        }
        
        // Step backwards one sample, but stop if we hit the beginning index
        if(index == 0)
            break;
        index--;
    }
    
    if(foundPartialPressState)
        return earliestPartialPressTimestamp;
    
    // Didn't find anything if we get here
    return missing_value<timestamp_type>::missing();
}