view Source/Mappings/OnsetAngle/TouchkeyOnsetAngleMapping.cpp @ 56:b4a2d2ae43cf tip

merge
author Andrew McPherson <andrewm@eecs.qmul.ac.uk>
date Fri, 23 Nov 2018 15:48:14 +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/>.
 
  =====================================================================

  TouchkeyOnsetAngleMapping.cpp: per-note mapping for the onset angle mapping,
  which measures the speed of finger motion along the key surface at the
  time of MIDI note onset.
*/

#include "TouchkeyOnsetAngleMapping.h"
#include "../MappingFactory.h"

#define DEBUG_NOTE_ONSET_MAPPING

// Class constants
const int TouchkeyOnsetAngleMapping::kDefaultFilterBufferLength = 30;
const timestamp_diff_type TouchkeyOnsetAngleMapping::kDefaultMaxLookbackTime = milliseconds_to_timestamp(100);
const int TouchkeyOnsetAngleMapping::kDefaultMaxLookbackSamples = 3;

// 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
TouchkeyOnsetAngleMapping::TouchkeyOnsetAngleMapping(PianoKeyboard &keyboard, MappingFactory *factory, int noteNumber, Node<KeyTouchFrame>* touchBuffer,
                                                         Node<key_position>* positionBuffer, KeyPositionTracker* positionTracker)
: TouchkeyBaseMapping(keyboard, factory, noteNumber, touchBuffer, positionBuffer, positionTracker),
pastSamples_(kDefaultFilterBufferLength), maxLookbackTime_(kDefaultMaxLookbackTime),
startingPitchBendSemitones_(0), lastPitchBendSemitones_(0),
rampBeginTime_(missing_value<timestamp_type>::missing()), rampLength_(0)
{
}

// Reset state back to defaults
void TouchkeyOnsetAngleMapping::reset() {
    ScopedLock sl(sampleBufferMutex_);
    
    TouchkeyBaseMapping::reset();
    pastSamples_.clear();
}

// Resend all current parameters
void TouchkeyOnsetAngleMapping::resend() {
    // Message is only sent at release; resend may not apply here.
}

// This method receives data from the touch buffer or possibly the continuous key angle (not used here)
void TouchkeyOnsetAngleMapping::triggerReceived(TriggerSource* who, timestamp_type timestamp) {
    if(who == touchBuffer_) {
        ScopedLock sl(sampleBufferMutex_);
        
        // Save the latest frame, even if it is an empty touch (we need to know what happened even
        // after the touch ends since the MIDI off may come later)
        if(!touchBuffer_->empty())
            pastSamples_.insert(touchBuffer_->latest(), touchBuffer_->latestTimestamp());
    }
}

// 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 TouchkeyOnsetAngleMapping::performMapping() {
    timestamp_type currentTimestamp = keyboard_.schedulerCurrentTimestamp();
    
    if(rampLength_ != 0 && currentTimestamp <= rampBeginTime_ + rampLength_) {
        float rampValue = 1.0 - (float)(currentTimestamp - rampBeginTime_)/(float)rampLength_;
        
        lastPitchBendSemitones_ = startingPitchBendSemitones_ * rampValue;
#ifdef DEBUG_NOTE_ONSET_MAPPING
        std::cout << "onset pitch = " << lastPitchBendSemitones_ << endl;
#endif
        sendPitchBendMessage(lastPitchBendSemitones_);
    }
    else if(lastPitchBendSemitones_ != 0) {
        lastPitchBendSemitones_ = 0;
#ifdef DEBUG_NOTE_ONSET_MAPPING
        std::cout << "onset pitch = " << lastPitchBendSemitones_ << endl;
#endif
        sendPitchBendMessage(lastPitchBendSemitones_);
    }
        
    // Register for the next update by returning its timestamp
    nextScheduledTimestamp_ = currentTimestamp + updateInterval_;
    return nextScheduledTimestamp_;
}

void TouchkeyOnsetAngleMapping::processOnset(timestamp_type timestamp) {

    sampleBufferMutex_.enter();
    
    // Look backwards from the current timestamp to find the velocity
    float calculatedVelocity = missing_value<float>::missing();
    bool touchWasOn = false;
    int sampleCount = 0;
    
#ifdef DEBUG_NOTE_ONSET_MAPPING
    std::cout << "processOnset begin = " << pastSamples_.beginIndex() << " end = " << pastSamples_.endIndex() << "\n";
#endif
    
    if(!pastSamples_.empty()) {
        Node<KeyTouchFrame>::size_type index = pastSamples_.endIndex() - 1;
        Node<KeyTouchFrame>::size_type mostRecentTouchPresentIndex = pastSamples_.endIndex() - 1;
        while(index >= pastSamples_.beginIndex()) {
#ifdef DEBUG_NOTE_ONSET_MAPPING
            std::cout << "examining sample " << index << " with " << pastSamples_[index].count << " touches and time diff " << timestamp - pastSamples_.timestampAt(index) << "\n";
#endif
            if(timestamp - pastSamples_.timestampAt(index) >= maxLookbackTime_)
                break;
            if(pastSamples_[index].count == 0) {
                if(touchWasOn) {
                    // We found a break in the touch; stop here. But don't stop
                    // if the first frames we consider have no touches.
                    if(index < pastSamples_.endIndex() - 1)
                        index++;
                    break;
                }
            }
            else if(!touchWasOn) {
                mostRecentTouchPresentIndex = index;
                touchWasOn = true;
            }
            if(sampleCount++ >= kDefaultMaxLookbackSamples)
                break;
            
            // Can't decrement past 0 in an unsigned type
            if(index == 0)
                break;
            index--;
        }
        
        // If we fell off the beginning of the buffer, back up.
        if(index < pastSamples_.beginIndex())
            index =  pastSamples_.beginIndex();
        
        // Need at least two points for this calculation to work
        timestamp_type endingTimestamp = pastSamples_.timestampAt(mostRecentTouchPresentIndex);
        timestamp_type startingTimestamp = pastSamples_.timestampAt(index);
        if(endingTimestamp - startingTimestamp > 0) {
            float endingPosition = pastSamples_[mostRecentTouchPresentIndex].locs[0];
            float startingPosition = pastSamples_[index].locs[0];
            calculatedVelocity = (endingPosition - startingPosition) / (endingTimestamp - startingTimestamp);
        }
        else { // DEBUG
#ifdef DEBUG_NOTE_ONSET_MAPPING
            std::cout << "Found 0 timestamp difference on key onset (indices " << index << " and " << pastSamples_.endIndex() - 1 << "\n";
#endif
        }
    }
    else {
#ifdef DEBUG_NOTE_ONSET_MAPPING
        std::cout << "Found empty touch buffer on key onset\n";
#endif
    }
    
    sampleBufferMutex_.exit();
    
    if(!missing_value<float>::isMissing(calculatedVelocity)) {
#ifdef DEBUG_NOTE_ONSET_MAPPING
        std::cout << "Found onset velocity " << calculatedVelocity << " on note " << noteNumber_ << std::endl;
#endif
        if(calculatedVelocity > 6.0)
            calculatedVelocity = 6.0;
        
        if(calculatedVelocity > 1.5) {
            startingPitchBendSemitones_ = -1.0 * (calculatedVelocity / 5.0);
            rampLength_ = milliseconds_to_timestamp((50.0 + calculatedVelocity*25.0));
            rampBeginTime_ = keyboard_.schedulerCurrentTimestamp();
        }
        else
            rampLength_ = 0;
        
        sendOnsetAngleMessage(calculatedVelocity);
    }
}

void TouchkeyOnsetAngleMapping::sendOnsetAngleMessage(float onsetAngle, bool force) {
    if(force || !suspended_) {
        keyboard_.sendMessage("/touchkeys/onsetangle", "if", noteNumber_, onsetAngle, LO_ARGS_END);
    }
}

// Send the pitch bend message of a given number of a semitones. Send by OSC,
// which can be mapped to MIDI CC externally
void TouchkeyOnsetAngleMapping::sendPitchBendMessage(float pitchBendSemitones, bool force) {
    if(force || !suspended_)
        keyboard_.sendMessage("/touchkeys/scoop", "if", noteNumber_, pitchBendSemitones, LO_ARGS_END);
}