view Source/Mappings/Control/TouchkeyControlMapping.cpp @ 33:e8965409903e

First (incomplete) start on save/load presets.
author Andrew McPherson <andrewm@eecs.qmul.ac.uk>
date Thu, 20 Mar 2014 23:18:41 +0000
parents c6f30c1e2bda
children 85577160a0d4
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/>.
 
  =====================================================================

  TouchkeyControlMapping.cpp: per-note mapping for the TouchKeys control
  mapping, which converts an arbitrary touch parameter into a MIDI or
  OSC control message.
*/

#include "TouchkeyControlMapping.h"
#include <vector>
#include <climits>
#include <cmath>
#include "../MappingScheduler.h"

#undef DEBUG_CONTROL_MAPPING

// Class constants
const int TouchkeyControlMapping::kDefaultMIDIChannel = 0;
const int TouchkeyControlMapping::kDefaultFilterBufferLength = 300;

const bool TouchkeyControlMapping::kDefaultIgnoresTwoFingers = false;
const bool TouchkeyControlMapping::kDefaultIgnoresThreeFingers = false;
const int TouchkeyControlMapping::kDefaultDirection = TouchkeyControlMapping::kDirectionPositive;

// 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
TouchkeyControlMapping::TouchkeyControlMapping(PianoKeyboard &keyboard, MappingFactory *factory, int noteNumber, Node<KeyTouchFrame>* touchBuffer,
                                                   Node<key_position>* positionBuffer, KeyPositionTracker* positionTracker)
: TouchkeyBaseMapping(keyboard, factory, noteNumber, touchBuffer, positionBuffer, positionTracker),
controlIsEngaged_(false),
inputMin_(0.0), inputMax_(1.0), outputMin_(0.0), outputMax_(1.0), outputDefault_(0.0),
inputParameter_(kInputParameterYPosition), inputType_(kTypeAbsolute),
threshold_(0.0), ignoresTwoFingers_(kDefaultIgnoresTwoFingers),
ignoresThreeFingers_(kDefaultIgnoresThreeFingers), direction_(kDefaultDirection),
touchOnsetValue_(missing_value<float>::missing()),
midiOnsetValue_(missing_value<float>::missing()),
lastValue_(missing_value<float>::missing()),
lastTimestamp_(missing_value<timestamp_type>::missing()), lastProcessedIndex_(0),
controlEngageLocation_(missing_value<float>::missing()),
controlScalerPositive_(missing_value<float>::missing()),
controlScalerNegative_(missing_value<float>::missing()),
lastControlValue_(outputDefault_),
rawValues_(kDefaultFilterBufferLength)
{
    resetDetectionState();
}

TouchkeyControlMapping::~TouchkeyControlMapping() {
#if 0
#ifndef NEW_MAPPING_SCHEDULER
    try {
        disengage();
    }
    catch(...) {
        std::cerr << "~TouchkeyControlMapping(): exception during disengage()\n";
    }
#endif
#endif
}

// Turn on mapping of data.
/*void TouchkeyControlMapping::engage() {
    Mapping::engage();
    
    // Register for OSC callbacks on MIDI note on/off
    addOscListener("/midi/noteon");
	addOscListener("/midi/noteoff");
}

// Turn off mapping of data. Remove our callback from the scheduler
void TouchkeyControlMapping::disengage(bool shouldDelete) {
    // Remove OSC listeners first
    removeOscListener("/midi/noteon");
	removeOscListener("/midi/noteoff");
    
    // Don't send any separate message here, leave it where it was
    
    Mapping::disengage(shouldDelete);
    
    if(noteIsOn_) {
        // TODO
    }
    noteIsOn_ = false;
}*/

// Reset state back to defaults
void TouchkeyControlMapping::reset() {
    TouchkeyBaseMapping::reset();
    sendControlMessage(outputDefault_);
    resetDetectionState();
    //noteIsOn_ = false;
}

// Resend all current parameters
void TouchkeyControlMapping::resend() {
    sendControlMessage(lastControlValue_, true);
}

// Name for this control, used in the OSC path
/*void TouchkeyControlMapping::setName(const std::string& name) {
    controlName_ = name;
}*/

// Parameters for the controller handling
// Input parameter to use for this control mapping and whether it is absolute or relative
void TouchkeyControlMapping::setInputParameter(int parameter, int type) {
    if(inputParameter_ >= 0 && inputParameter_ < kInputParameterMaxValue)
        inputParameter_ = parameter;
    if(type >= 0 && type < kTypeMaxValue)
        inputType_ = type;
}

// Input/output range for this parameter
void TouchkeyControlMapping::setRange(float inputMin, float inputMax, float outputMin, float outputMax, float outputDefault) {
    inputMin_ = inputMin;
    inputMax_ = inputMax;
    outputMin_ = outputMin;
    outputMax_ = outputMax;
    outputDefault_ = outputDefault;
}

// Threshold which must be exceeded for the control to engage (for relative position), or 0 if not used
void TouchkeyControlMapping::setThreshold(float threshold) {
    threshold_ = threshold;
}

void TouchkeyControlMapping::setIgnoresMultipleFingers(bool ignoresTwo, bool ignoresThree) {
    ignoresTwoFingers_ = ignoresTwo;
    ignoresThreeFingers_ = ignoresThree;
}

void TouchkeyControlMapping::setDirection(int direction) {
    if(direction >= 0 && direction < kDirectionMaxValue)
        direction_ = direction;
}

// OSC handler method. Called from PianoKeyboard when MIDI data comes in.
/*bool TouchkeyControlMapping::oscHandlerMethod(const char *path, const char *types, int numValues, lo_arg **values, void *data) {
    if(!strcmp(path, "/midi/noteon") && !noteIsOn_ && numValues >= 1) {
        if(types[0] == 'i' && values[0]->i == noteNumber_) {
            // MIDI note has gone on. Set the starting location to be most recent
            // location. It's possible there has been no touch data before this,
            // in which case lastX and lastY will hold missing values.
            midiOnsetValue_ = lastValue_;
            if(!missing_value<float>::isMissing(midiOnsetValue_)) {
                if(inputType_ == kTypeNoteOnsetRelative) {
                    // Already have touch data. Clear the buffer here.
                    // Clear buffer and start with default value for this point
                    clearBuffers();
                    
#ifdef DEBUG_CONTROL_MAPPING
                    std::cout << "MIDI on: starting at (" << midiOnsetValue_ << ")\n";
#endif
                }
            }
            else {
#ifdef DEBUG_CONTROL_MAPPING
                std::cout << "MIDI on but no touch\n";
#endif
            }
            
            noteIsOn_ = true;
            return false;
        }
    }
    else if(!strcmp(path, "/midi/noteoff") && noteIsOn_ && numValues >= 1) {
        if(types[0] == 'i' && values[0]->i == noteNumber_) {
            // MIDI note goes off
            noteIsOn_ = false;
            if(controlIsEngaged_) {
                // TODO: should anything happen here?
            }
#ifdef DEBUG_CONTROL_MAPPING
            std::cout << "MIDI off\n";
#endif
            return false;
        }
    }
    
    return false;
}*/

// 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 TouchkeyControlMapping::triggerReceived(TriggerSource* who, timestamp_type timestamp) {
    if(who == 0)
        return;
    
    if(who == touchBuffer_) {
        if(!touchBuffer_->empty()) {
            // New touch data is available. Find the distance from the onset location.
            KeyTouchFrame frame = touchBuffer_->latest();
            lastTimestamp_ = timestamp;
            
            if(frame.count == 0) {
                // No touches. Last values are "missing", and we're not tracking any
                // particular touch ID
                lastValue_ = missing_value<float>::missing();
                idsOfCurrentTouches_[0] = idsOfCurrentTouches_[1] = idsOfCurrentTouches_[2] = -1;
                
#ifdef DEBUG_CONTROL_MAPPING
                std::cout << "Touch off\n";
#endif
            }
            else {
                //ScopedLock sl(rawValueAccessMutex_);
                
                // At least one touch. Check if we are already tracking an ID and, if so,
                // use its coordinates. Otherwise grab the lowest current ID.
                lastValue_ = getValue(frame);
                
                // Check that the value actually exists
                if(!missing_value<float>::isMissing(lastValue_)) {
                    // If we have no onset value, this is it
                    if(missing_value<float>::isMissing(touchOnsetValue_)) {
                        touchOnsetValue_ = lastValue_;
                        if(inputType_ == kTypeFirstTouchRelative) {
                            clearBuffers();
#ifdef DEBUG_CONTROL_MAPPING
                            std::cout << "Starting at " << lastValue_ << std::endl;
#endif
                        }
                    }
                    
                    // If MIDI note is on and we don't previously have a value, this is it
                    if(noteIsOn_ && missing_value<float>::isMissing(midiOnsetValue_)) {
                        midiOnsetValue_ = lastValue_;
                        if(inputType_ == kTypeNoteOnsetRelative) {
                            clearBuffers();
#ifdef DEBUG_CONTROL_MAPPING
                            std::cout << "Starting at " << lastValue_ << std::endl;
#endif
                        }
                    }
                    
                    if(noteIsOn_) {
                        // Insert the latest sample into the buffer depending on how the data should be processed
                        if(inputType_ == kTypeAbsolute) {
                            rawValues_.insert(lastValue_, timestamp);
                        }
                        else if(inputType_ == kTypeFirstTouchRelative) {
                            rawValues_.insert(lastValue_ - touchOnsetValue_, timestamp);
                        }
                        else if(inputType_ == kTypeNoteOnsetRelative) {
                            rawValues_.insert(lastValue_ - midiOnsetValue_, timestamp);
                        }
                        
                        // Move the current scheduled event up to the present time.
                        // FIXME: this may be more inefficient than just doing everything in the current thread!
#ifdef NEW_MAPPING_SCHEDULER
                        keyboard_.mappingScheduler().scheduleNow(this);
#else
                        keyboard_.unscheduleEvent(this);
                        keyboard_.scheduleEvent(this, mappingAction_, keyboard_.schedulerCurrentTimestamp());
#endif
                    }
                }
            }
        }
    }
}

// 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 TouchkeyControlMapping::performMapping() {
    //ScopedLock sl(rawValueAccessMutex_);
    
    timestamp_type currentTimestamp = keyboard_.schedulerCurrentTimestamp();
    bool newSamplePresent = false;
    
    // Go through the filtered distance samples that are remaining to process.
    if(lastProcessedIndex_ < rawValues_.beginIndex() + 1) {
        // Fell off the beginning of the position buffer. Skip to the samples we have
        // (shouldn't happen except in cases of exceptional system load, and not too
        // consequential if it does happen).
        lastProcessedIndex_ = rawValues_.beginIndex() + 1;
    }
    
    while(lastProcessedIndex_ < rawValues_.endIndex()) {
        float value = rawValues_[lastProcessedIndex_];
        //timestamp_type timestamp = rawValues_.timestampAt(lastProcessedIndex_);
        newSamplePresent = true;
        
        if(inputType_ == kTypeAbsolute) {
            controlIsEngaged_ = true;
        }
        else if(!controlIsEngaged_) {
            // Compare value against threshold to see if the control should engage
            if(fabsf(value) > threshold_) {
                float startingValue;
                
                controlIsEngaged_ = true;
                controlEngageLocation_ = (value > 0 ? threshold_ : -threshold_);
                
#ifdef DEBUG_CONTROL_MAPPING
                std::cout << "engaging control at distance " << controlEngageLocation_ << std::endl;
#endif
                
                if(inputType_ == kTypeFirstTouchRelative)
                    startingValue = touchOnsetValue_;
                else
                    startingValue = midiOnsetValue_;
                
                // This is how much range we would have had without the threshold
                float distanceToPositiveEdgeWithoutThreshold = 1.0 - startingValue;
                float distanceToNegativeEdgeWithoutThreshold = 0.0 + startingValue;
                
                // This is how much range we actually have with the threshold
                float actualDistanceToPositiveEdge = 1.0 - (startingValue + controlEngageLocation_);
                float actualDistanceToNegativeEdge = 0.0 + startingValue + controlEngageLocation_;
                
                // Make it so moving toward edge of key gets as far as it would have without
                // the distance lost by the threshold
                if(actualDistanceToPositiveEdge > 0.0)
                    controlScalerPositive_ = (outputMax_ - outputDefault_) * distanceToPositiveEdgeWithoutThreshold / actualDistanceToPositiveEdge;
                else
                    controlScalerPositive_ = (outputMax_ - outputDefault_); // Sanity check
                if(actualDistanceToNegativeEdge > 0.0)
                    controlScalerNegative_ = (outputDefault_ - outputMin_) * distanceToNegativeEdgeWithoutThreshold / actualDistanceToNegativeEdge;
                else
                    controlScalerNegative_ = (outputDefault_ - outputMin_); // Sanity check
            }
        }
        
        lastProcessedIndex_++;
    }
    
    if(controlIsEngaged_) {
        // Having processed every sample individually for any detection/filtering, now send
        // the most recent output as an OSC message
        if(newSamplePresent) {
            float latestValue = rawValues_.latest();

            // In cases of relative values, the place the control engages will actually be where it crosses
            // the threshold, not the onset location itself. Need to update the value accordingly.
            if(inputType_ == kTypeFirstTouchRelative ||
               inputType_ == kTypeNoteOnsetRelative) {
                if(latestValue > 0) {
                    latestValue -= threshold_;
                    if(latestValue < 0)
                        latestValue = 0;
                }
                else if(latestValue < 0) {
                    latestValue += threshold_;
                    if(latestValue > 0)
                        latestValue = 0;
                }
            }

            if(direction_ == kDirectionNegative)
                latestValue = -latestValue;
            else if((direction_ == kDirectionBoth) && latestValue < 0)
                latestValue = -latestValue;
            
            sendControlMessage(latestValue);
            lastControlValue_ = latestValue;
        }
    }
    
    // Register for the next update by returning its timestamp
    nextScheduledTimestamp_ = 0; //currentTimestamp + updateInterval_;
    return nextScheduledTimestamp_;
}

// MIDI note-on message received
void TouchkeyControlMapping::midiNoteOnReceived(int channel, int velocity) {
    // MIDI note has gone on. Set the starting location to be most recent
    // location. It's possible there has been no touch data before this,
    // in which case lastX and lastY will hold missing values.
    midiOnsetValue_ = lastValue_;
    if(!missing_value<float>::isMissing(midiOnsetValue_)) {
        if(inputType_ == kTypeNoteOnsetRelative) {
            // Already have touch data. Clear the buffer here.
            // Clear buffer and start with default value for this point
            clearBuffers();
            
#ifdef DEBUG_CONTROL_MAPPING
            std::cout << "MIDI on: starting at (" << midiOnsetValue_ << ")\n";
#endif
        }
    }
    else {
#ifdef DEBUG_CONTROL_MAPPING
        std::cout << "MIDI on but no touch\n";
#endif
    }
}

// MIDI note-off message received
void TouchkeyControlMapping::midiNoteOffReceived(int channel) {
    if(controlIsEngaged_) {
        // TODO: should anything happen here?
    }
}

// Reset variables involved in detecting a pitch bend gesture
void TouchkeyControlMapping::resetDetectionState() {
    controlIsEngaged_ = false;
    controlEngageLocation_ = missing_value<float>::missing();
    idsOfCurrentTouches_[0] = idsOfCurrentTouches_[1] = idsOfCurrentTouches_[2] = -1;
}

// Clear the buffers that hold distance measurements
void TouchkeyControlMapping::clearBuffers() {
    rawValues_.clear();
    rawValues_.insert(0.0, lastTimestamp_);
    lastProcessedIndex_ = 0;
}

// Return the current parameter value depending on which one we are listening to
float TouchkeyControlMapping::getValue(const KeyTouchFrame& frame) {
    if(inputParameter_ == kInputParameterXPosition)
        return frame.locH;
    /*else if(inputParameter_ == kInputParameter2FingerMean ||
            inputParameter_ == kInputParameter2FingerDistance) {
        if(frame.count < 2)
            return missing_value<float>::missing();
        if(frame.count == 3 && ignoresThreeFingers_)
            return missing_value<float>::missing();
        
        bool foundCurrentTouch = false;
        float currentValue;
        
        // Look for the touches we were tracking last frame
        if(idsOfCurrentTouches_[0] >= 0) {
            for(int i = 0; i < frame.count; i++) {
                if(frame.ids[i] == idsOfCurrentTouches_[0]) {
                    if(inputParameter_ == kInputParameterYPosition)
                        currentValue = frame.locs[i];
                    else // kInputParameterTouchSize
                        currentValue = frame.sizes[i];
                    foundCurrentTouch = true;
                    break;
                }
            }
        }
        
        if(!foundCurrentTouch) {
            // Assign a new touch to be tracked
            int lowestRemainingId = INT_MAX;
            int lowestIndex = 0;
            
            for(int i = 0; i < frame.count; i++) {
                if(frame.ids[i] < lowestRemainingId) {
                    lowestRemainingId = frame.ids[i];
                    lowestIndex = i;
                }
            }
            
            idsOfCurrentTouches_[0] = lowestRemainingId;
            if(inputParameter_ == kInputParameterYPosition)
                currentValue = frame.locs[lowestIndex];
            else if(inputParameter_ == kInputParameterTouchSize)
                currentValue = frame.sizes[lowestIndex];
            else // Shouldn't happen
                currentValue = missing_value<float>::missing();
            
#ifdef DEBUG_CONTROL_MAPPING
            std::cout << "Previous touch stopped; now ID " << idsOfCurrentTouches_[0] << " at (" << currentValue << ")\n";
#endif
        }
        
    }*/
    else {
        if(frame.count == 0)
            return missing_value<float>::missing();
        if((inputParameter_ == kInputParameter2FingerMean ||
           inputParameter_ == kInputParameter2FingerDistance) &&
           frame.count < 2)
            return missing_value<float>::missing();
        if(frame.count == 2 && ignoresTwoFingers_)
            return missing_value<float>::missing();
        if(frame.count == 3 && ignoresThreeFingers_)
            return missing_value<float>::missing();
        /*
        // The other values are dependent on individual touches
        bool foundCurrentTouch = false;
        float currentValue;
        
        // Look for the touch we were tracking last frame
        if(idsOfCurrentTouches_[0] >= 0) {
            for(int i = 0; i < frame.count; i++) {
                if(frame.ids[i] == idsOfCurrentTouches_[0]) {
                    if(inputParameter_ == kInputParameterYPosition)
                        currentValue = frame.locs[i];
                    else // kInputParameterTouchSize
                        currentValue = frame.sizes[i];
                    foundCurrentTouch = true;
                    break;
                }
            }
        }
        
        if(!foundCurrentTouch) {
            // Assign a new touch to be tracked
            int lowestRemainingId = INT_MAX;
            int lowestIndex = 0;
            
            for(int i = 0; i < frame.count; i++) {
                if(frame.ids[i] < lowestRemainingId) {
                    lowestRemainingId = frame.ids[i];
                    lowestIndex = i;
                }
            }
            
            idsOfCurrentTouches_[0] = lowestRemainingId;
            if(inputParameter_ == kInputParameterYPosition)
                currentValue = frame.locs[lowestIndex];
            else if(inputParameter_ == kInputParameterTouchSize)
                currentValue = frame.sizes[lowestIndex];
            else // Shouldn't happen
                currentValue = missing_value<float>::missing();
            
#ifdef DEBUG_CONTROL_MAPPING
            std::cout << "Previous touch stopped; now ID " << idsOfCurrentTouches_[0] << " at (" << currentValue << ")\n";
#endif
        }*/
        
        float currentValue = 0;
        
        int idWithinFrame0 = locateTouchId(frame, 0);
        if(idWithinFrame0 < 0) { 
            // Touch ID not found, start a new value
            idsOfCurrentTouches_[0] = lowestUnassignedTouch(frame, &idWithinFrame0);
#ifdef DEBUG_CONTROL_MAPPING
            std::cout << "Previous touch stopped (0); now ID " << idsOfCurrentTouches_[0] << endl;
#endif
            if(idsOfCurrentTouches_[0] < 0) {
                cout << "BUG: didn't find any unassigned touch!\n";
            }
        }
        
        if(inputParameter_ == kInputParameterYPosition)
            currentValue = frame.locs[idWithinFrame0];
        else if(inputParameter_ == kInputParameterTouchSize) // kInputParameterTouchSize
            currentValue = frame.sizes[idWithinFrame0];
        else if(inputParameter_ == kInputParameter2FingerMean ||
           inputParameter_ == kInputParameter2FingerDistance) {
            int idWithinFrame1 = locateTouchId(frame, 1);
            if(idWithinFrame1 < 0) {
                // Touch ID not found, start a new value
                idsOfCurrentTouches_[1] = lowestUnassignedTouch(frame, &idWithinFrame1);
#ifdef DEBUG_CONTROL_MAPPING
                std::cout << "Previous touch stopped (1); now ID " << idsOfCurrentTouches_[1] << endl;
#endif
                if(idsOfCurrentTouches_[1] < 0) {
                    cout << "BUG: didn't find any unassigned touch for second finger!\n";
                }
            }
            
            if(inputParameter_ == kInputParameter2FingerMean)
                currentValue = (frame.locs[idWithinFrame0] + frame.locs[idWithinFrame1]) * 0.5;
            else
                currentValue = fabsf(frame.locs[idWithinFrame1] - frame.locs[idWithinFrame0]);
        }
        
        return currentValue;
    }
}

// Look for a touch index in the frame matching the given value of idsOfCurrentTouches[index]
// Returns -1 if not found
int TouchkeyControlMapping::locateTouchId(KeyTouchFrame const& frame, int index) {
    if(idsOfCurrentTouches_[index] < 0)
        return -1;
    
    for(int i = 0; i < frame.count; i++) {
        if(frame.ids[i] == idsOfCurrentTouches_[index]) {
            return i;
        }
    }
    
    return -1;
}

// Locates the lowest touch ID that is not assigned to a current touch
// Returns -1 if no unassigned touches were found
int TouchkeyControlMapping::lowestUnassignedTouch(KeyTouchFrame const& frame, int *indexWithinFrame) {
    int lowestRemainingId = INT_MAX;
    int lowestIndex = -1;
    
    for(int i = 0; i < frame.count; i++) {
        if(frame.ids[i] < lowestRemainingId) {
            bool alreadyAssigned = false;
            for(int j = 0; j < 3; j++) {
                if(idsOfCurrentTouches_[j] == frame.ids[i])
                    alreadyAssigned = true;
            }
            
            if(!alreadyAssigned) {
                lowestRemainingId = frame.ids[i];
                lowestIndex = i;

            }
        }
    }
    
    if(indexWithinFrame != 0)
        *indexWithinFrame = lowestIndex;
    return lowestRemainingId;
}

// Send the pitch bend message of a given number of a semitones. Send by OSC,
// which can be mapped to MIDI CC externally
void TouchkeyControlMapping::sendControlMessage(float value, bool force) {
    if(force || !suspended_) {
#ifdef DEBUG_CONTROL_MAPPING
        std::cout << "TouchkeyControlMapping: sending " << value << " for note " << noteNumber_ << std::endl;
#endif
        keyboard_.sendMessage(controlName_.c_str(), "if", noteNumber_, value, LO_ARGS_END);
    }
}