view Source/Mappings/KeyDivision/TouchkeyKeyDivisionMapping.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 78b9808a2c65
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/>.
 
  =====================================================================

  TouchkeyKeyDivisionMapping.cpp: per-note mapping for the split-key mapping
  which triggers different actions or pitches depending on where the key
  was struck.
*/

#include "TouchkeyKeyDivisionMapping.h"
#include "TouchkeyKeyDivisionMappingFactory.h"

#define DEBUG_KEY_DIVISION_MAPPING

const int TouchkeyKeyDivisionMapping::kDefaultNumberOfSegments = 2;
const timestamp_diff_type TouchkeyKeyDivisionMapping::kDefaultDetectionTimeout = milliseconds_to_timestamp(25.0);
const int TouchkeyKeyDivisionMapping::kDefaultDetectionParameter = kDetectionParameterYPosition;
const int TouchkeyKeyDivisionMapping::kDefaultRetriggerNumFrames = 2;

// 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
TouchkeyKeyDivisionMapping::TouchkeyKeyDivisionMapping(PianoKeyboard &keyboard, MappingFactory *factory, int noteNumber, Node<KeyTouchFrame>* touchBuffer,
                                                     Node<key_position>* positionBuffer, KeyPositionTracker* positionTracker)
: TouchkeyBaseMapping(keyboard, factory, noteNumber, touchBuffer, positionBuffer, positionTracker),
numberOfSegments_(kDefaultNumberOfSegments), candidateSegment_(-1), detectedSegment_(-1), defaultSegment_(0),
detectionParameter_(kDefaultDetectionParameter), retriggerable_(false), retriggerNumFrames_(kDefaultRetriggerNumFrames),
retriggerKeepsVelocity_(true),
midiNoteOnTimestamp_(missing_value<timestamp_type>::missing()), timeout_(kDefaultDetectionTimeout),
lastNumActiveTouches_(-1)
{
}

// Reset state back to defaults
void TouchkeyKeyDivisionMapping::reset() {
    TouchkeyBaseMapping::reset();
    
    candidateSegment_ = detectedSegment_ = -1;
    midiNoteOnTimestamp_ = missing_value<timestamp_type>::missing();
}

// Resend all current parameters
void TouchkeyKeyDivisionMapping::resend() {
    if(detectedSegment_ >= 0)
        sendSegmentMessage(detectedSegment_, true);
}

// Set the pitch bend values (in semitones) for each segment. These
// values are in relation to the pitch of this note
void TouchkeyKeyDivisionMapping::setSegmentPitchBends(const float *bendsInSemitones, int numBends) {
    // Clear old values and refill the vector
    segmentBends_.clear();
    for(int i = 0; i < numBends; i++)
        segmentBends_.push_back(bendsInSemitones[i]);
}

// This method receives data from the touch buffer or possibly the continuous key angle (not used here)
void TouchkeyKeyDivisionMapping::triggerReceived(TriggerSource* who, timestamp_type timestamp) {
    if(who == touchBuffer_) {
        // If we get here, a new touch frame has been received and there is no segment detected
        // yet. We should come up with a candidate segment. If the MIDI note is on, activate this
        // segment right away. Otherwise, save it for later so when the MIDI note begins, we have
        // it ready to go.
        if(!touchBuffer_->empty()) {
            const KeyTouchFrame& frame = touchBuffer_->latest();
            
            if(detectedSegment_ < 0) {
                int candidateBasedOnYPosition = -1, candidateBasedOnNumberOfTouches = -1;
                
                // Find the first touch. TODO: eventually look for the largest touch
                float yPosition = frame.locs[0];
                
                // Calculate two possible segments based on touch location and based on
                // number of touches.
                candidateBasedOnYPosition = segmentForLocation(yPosition);
                candidateBasedOnNumberOfTouches = segmentForNumTouches(frame.count);
                
                if(detectionParameter_ == kDetectionParameterYPosition)
                    candidateSegment_ = candidateBasedOnYPosition;
                else if(detectionParameter_ == kDetectionParameterNumberOfTouches)
                    candidateSegment_ = candidateBasedOnNumberOfTouches;
                else if(detectionParameter_ == kDetectionParameterYPositionAndNumberOfTouches) {
                    // Choose the maximum segment specified by the other two methods
                    candidateSegment_ = candidateBasedOnNumberOfTouches > candidateBasedOnYPosition ? candidateBasedOnNumberOfTouches : candidateBasedOnYPosition;
                }
                else // Shouldn't happen
                    candidateSegment_ = -1;
                
                if(noteIsOn_) {
                    detectedSegment_ = candidateSegment_;
#ifdef DEBUG_KEY_DIVISION_MAPPING
                    cout << "TouchkeyKeyDivisionMapping::triggerReceived(): detectedSegment_ = " << detectedSegment_ << endl;
#endif
                    sendSegmentMessage(detectedSegment_);
                }
            }
            else if(retriggerable_ &&
                    (lastNumActiveTouches_ == 1 &&
                    frame.count >= 2) && noteIsOn_) {
                // Here, there was one touch active before and now there are two. Look for the
                // location of the most recently added touch, and determine whether it matches a
                // segment different from the one we're in. If so, retrigger the MIDI note
                // with a different pitch bend
                
                int newCandidate = segmentForLocation(locationOfNewestTouch(frame));
                
#ifdef DEBUG_KEY_DIVIOSION_MAPPING
                cout << "TouchkeyKeyDivisionMapping: touch added with candidate segment " << newCandidate << " (current is " << detectedSegment_ << ")\n";
#endif
                if(newCandidate != detectedSegment_) {
                    // Set up a new segment to retrigger and tell the scheduler to insert the mapping
                    detectedSegment_ = newCandidate;
                    
                    // Find the keyboard segment, which gives us the output port
                    int outputPort = static_cast<TouchkeyKeyDivisionMappingFactory*>(factory_)->segment().outputPort();
                    
                    // Send MIDI note-on on the same channel as previously
                    int ch = keyboard_.key(noteNumber_)->midiChannel();
                    int vel = 64;
                    if(retriggerKeepsVelocity_)
                        vel = keyboard_.key(noteNumber_)->midiVelocity();
                    keyboard_.midiOutputController()->sendNoteOn(outputPort, ch, noteNumber_, vel);
                    sendSegmentMessage(detectedSegment_);
                }
            }
            
            // Save the number of active touches for next time
            lastNumActiveTouches_ = frame.count;
        }
    }
}

// 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 TouchkeyKeyDivisionMapping::performMapping() {
    timestamp_type currentTimestamp = keyboard_.schedulerCurrentTimestamp();
    
    if(detectedSegment_ >= 0) {
        // Found segment; no need to keep sending mapping callbacks
        nextScheduledTimestamp_ = 0;
        return 0;
    }
    
    if(currentTimestamp - midiNoteOnTimestamp_ > timeout_) {
        // Timeout occurred. Activate default segment
#ifdef DEBUG_KEY_DIVISION_MAPPING
        cout << "TouchkeyKeyDivisionMapping: timeout\n";
#endif
        detectedSegment_ = defaultSegment_;
        sendSegmentMessage(detectedSegment_);
        nextScheduledTimestamp_ = 0;
        return 0;
    }
    
    // Register for the next update by returning its timestamp
    nextScheduledTimestamp_ = currentTimestamp + updateInterval_;
    return nextScheduledTimestamp_;
}

// MIDI note-on received. If we have a candidate segment, activate it as the actual segment
void TouchkeyKeyDivisionMapping::midiNoteOnReceived(int channel, int velocity) {
    midiNoteOnTimestamp_ = keyboard_.schedulerCurrentTimestamp();
    
    if(detectedSegment_ < 0) {
#ifdef DEBUG_KEY_DIVISION_MAPPING
        cout << "TouchkeyKeyDivisionMapping::midiNoteOnReceived(): candidateSegment_ = " << candidateSegment_ << endl;
#endif
        detectedSegment_ = candidateSegment_;
        if(detectedSegment_ >= 0) {
            sendSegmentMessage(detectedSegment_);
        }
    }
}

// MIDI note-off received. Reset back to the detecting state so we can assign the next note to a segment
void TouchkeyKeyDivisionMapping::midiNoteOffReceived(int channel) {
    detectedSegment_ = candidateSegment_ = -1;
}

void TouchkeyKeyDivisionMapping::sendSegmentMessage(int segment, bool force) {
    if(force || !suspended_) {
        keyboard_.sendMessage("/touchkeys/keysegment", "ii", noteNumber_, segment, LO_ARGS_END);
        if(segment < segmentBends_.size() && segment >= 0) {
#ifdef DEBUG_KEY_DIVISION_MAPPING
            cout << "TouchkeyKeyDivisionMapping::sendSegmentMessage(): pitch bend = " << segmentBends_[segment] << endl;
#endif
            sendPitchBendMessage(segmentBends_[segment], force);
        }
        else {
#ifdef DEBUG_KEY_DIVISION_MAPPING
            cout << "TouchkeyKeyDivisionMapping::sendSegmentMessage(): no bend for segment " << segment << endl;
#endif
        }
    }
    else {
#ifdef DEBUG_KEY_DIVISION_MAPPING
        cout << "TouchkeyKeyDivisionMapping::sendSegmentMessage(): suspended, not sending segment " << segment << endl;
#endif
    }
}

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

// Find the segment corresponding to a (Y) touch location
int TouchkeyKeyDivisionMapping::segmentForLocation(float location) {
    // Divide the key into evenly-spaced regions, and identify and candidate segment.
    // Since the location can go up to 1.0, make sure the top value doesn't overflow
    // the number of segments
    int segment = floorf(location * (float)numberOfSegments_);
    if(segment >= numberOfSegments_)
        segment = numberOfSegments_ - 1;
    return segment;
}

// Find the segment corresponding to a number of touches
int TouchkeyKeyDivisionMapping::segmentForNumTouches(int numTouches) {
    // Check the number of touches, which could divide the note into as many
    // as three segments.
    if(numTouches <= 0)
        return -1;
    
    int segment = numTouches - 1;
    if(segment >= numberOfSegments_)
        segment = numberOfSegments_ - 1;
    return segment;
}

// Return the location of the most recently added touch (indicated by the highest ID)
float TouchkeyKeyDivisionMapping::locationOfNewestTouch(KeyTouchFrame const& frame) {
    if(frame.count == 0)
        return -1.0;
    
    // Go through the active touches and find the one with the highest id
    int highestId = -1;
    float location = -1.0;
    for(int i = 0; i < frame.count; i++) {
        if(frame.ids[i] > highestId) {
            highestId = frame.ids[i];
            location = frame.locs[i];
        }
    }
    
    return location;
}