view Source/TouchKeys/OscMidiConverter.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 353276611036
children cfbcd31a54e7
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/>.
 
  =====================================================================
 
  OscMidiConverter.cpp: converts incoming OSC messages to outgoing MIDI
  messages with adjustable ranges and parameters.
*/
#include "OscMidiConverter.h"
#include "MidiKeyboardSegment.h"

#undef DEBUG_OSC_MIDI_CONVERTER

// Main constructor: set up OSC reception from the keyboard
OscMidiConverter::OscMidiConverter(PianoKeyboard& keyboard, MidiKeyboardSegment& segment, int controllerId) :
  keyboard_(keyboard), keyboardSegment_(segment), midiOutputController_(0),
  controller_(controllerId), lastUniqueId_(0),
  incomingController_(MidiKeyboardSegment::kControlDisabled)
{
	setOscController(&keyboard_);
    
    for(int i = 0; i < 16; i++) {
        currentValue_[i] = 0;
        lastOutputValue_[i] = -1;
    }
}

// Set the type of MIDI message (CC, Pitch Wheel, Aftertouch)
// that this particular object is in charge of controlling.
// Optionally specify the default, range, and whether to use a
// 14 bit controller.
void OscMidiConverter::setMidiMessageType(int defaultValue, int minValue,
                                          int maxValue, int centerValue, bool use14BitControl) {
    /*if(controller < 0 || controller >= MidiKeyboardSegment::kControlMax) {
        controller_ = MidiKeyboardSegment::kControlDisabled;
        return;
    }
    
    keyboardSegment_ = segment;*/
    
    // Otherwise, send defaults on the current controller to all channels and update to the new values
    //for(int i = 0; i < 16; i++)
    //    sendDefaultValue(i);
    
    // Clear any existing active inputs, but not the mappings themselves
    lastValues_.clear();
    
    //controller_ = controller;
    if(defaultValue >= 0)
        controlDefaultValue_ = defaultValue;
    else {
        // Default value for MIDI CCs and aftertouch is 0
        // Default value for MIDI pitch wheel is 8192
        if(controller_ == MidiKeyboardSegment::kControlPitchWheel)
            controlDefaultValue_ = 8192;
        else
            controlDefaultValue_ = 0;
    }
    
    if(centerValue >= 0)
        controlCenterValue_ = centerValue;
    else
        controlCenterValue_ = controlDefaultValue_;
    
    // Pitch wheel is always 14 bit. Aftertouch is always 7 bit.
    // Other CCs are selectable (though according to MIDI spec not every
    // controller has an LSB defined). And above 96 using "control+32"
    // for fine adjust no longer makes sense
    if(controller_ < 96)
        controllerIs14Bit_ = use14BitControl;
    else if(controller_ == MidiKeyboardSegment::kControlPitchWheel)
        controllerIs14Bit_ = true;
    else
        controllerIs14Bit_ = false;
   
    // Handle the outer ranges of the control, providing defaults if unspecified
    if(controllerIs14Bit_) {
        if(maxValue < 0 || maxValue > 16383)
            controlMaxValue_ = 16383;
        else
            controlMaxValue_ = maxValue;
        if(minValue < 0 || minValue > 16383)
            controlMinValue_ = 0;
        else
            controlMinValue_ = minValue;
    }
    else {
        if(maxValue < 0 || maxValue > 127)
            controlMaxValue_ = 127;
        else
            controlMaxValue_ = maxValue;
        if(minValue < 0 || minValue > 127)
            controlMinValue_ = 0;
        else
            controlMinValue_ = minValue;
    }
}

void OscMidiConverter::listenToIncomingControl(int controller, int centerValue, bool use14BitControl) {
    if(controller < 0 || controller >= MidiKeyboardSegment::kControlMax) {
        incomingController_ = MidiKeyboardSegment::kControlDisabled;
        return;
    }
    
    incomingController_ = controller;
    
    // Assign the center value that will be subtacted from the incoming controller
    if(centerValue >= 0)
        incomingControllerCenterValue_ = centerValue;
    else if(controller == MidiKeyboardSegment::kControlPitchWheel)
        incomingControllerCenterValue_ = 8192; // Pitch wheel is centered at 8192
    else
        incomingControllerCenterValue_ = 0;
    
    // Pitch wheel is always 14 bit. Aftertouch is always 7 bit.
    // Other CCs are selectable (though according to MIDI spec not every
    // controller has an LSB defined). And above 96 using "control+32"
    // for fine adjust no longer makes sense
    if(controller < 96)
        incomingControllerIs14Bit_ = use14BitControl;
    else if(controller == MidiKeyboardSegment::kControlPitchWheel)
        incomingControllerIs14Bit_ = true;
    else
        incomingControllerIs14Bit_ = false;
}

// Resend the most recent value
void OscMidiConverter::resend(int channel) {
    sendCurrentValue(keyboardSegment_.outputPort(), channel, -1, true);
}

// Send the default value on the specified channel.
void OscMidiConverter::sendDefaultValue(int channel) {    
    if(midiOutputController_ == 0 || controller_ == MidiKeyboardSegment::kControlDisabled)
        return;
    
    // Modulate the default value by the value of the incoming controller,
    // if one is enabled.
    int defaultValue = controlDefaultValue_;
    if(incomingController_ != MidiKeyboardSegment::kControlDisabled)
        defaultValue += keyboardSegment_.controllerValue(incomingController_) - incomingControllerCenterValue_;
    
    if(controller_ == MidiKeyboardSegment::kControlPitchWheel) {
        //sendPitchWheelRange(keyboardSegment_.outputPort(), channel);
        midiOutputController_->sendPitchWheel(keyboardSegment_.outputPort(), channel, defaultValue);
    }
    else if(controller_ == MidiKeyboardSegment::kControlChannelAftertouch) {
        midiOutputController_->sendAftertouchChannel(keyboardSegment_.outputPort(), channel, defaultValue);
    }
    else if(controller_ == MidiKeyboardSegment::kControlPolyphonicAftertouch) {
        // TODO
    }
    else if(controllerIs14Bit_) {
        midiOutputController_->sendControlChange(keyboardSegment_.outputPort(), channel, controller_, defaultValue >> 7);
        midiOutputController_->sendControlChange(keyboardSegment_.outputPort(), channel, controller_ + 32, defaultValue & 0x7F);
    }
    else
        midiOutputController_->sendControlChange(keyboardSegment_.outputPort(), channel, controller_, defaultValue);
}

int OscMidiConverter::currentControllerValue(int channel) {
    float controlValue = (float)controlCenterValue_ + (float)controlMinValue_
    + currentValue_[channel]*(float)(controlMaxValue_ - controlMinValue_);
    
    if(incomingController_ != MidiKeyboardSegment::kControlDisabled) {
        controlValue += keyboardSegment_.controllerValue(incomingController_) - incomingControllerCenterValue_;
#ifdef DEBUG_OSC_MIDI_CONVERTER
        std::cout << "current value " << currentValue_[channel] << " corresponds to " << controlValue;
        std::cout << " including incoming value " << (keyboardSegment_.controllerValue(incomingController_) - incomingControllerCenterValue_) << std::endl;
#endif
    }
    else {
#ifdef DEBUG_OSC_MIDI_CONVERTER
        std::cout << "current value " << currentValue_[channel] << " corresponds to " << controlValue << std::endl;
#endif
    }
    
    int roundedControlValue = (int)floorf(controlValue + 0.5f);
    if(roundedControlValue > controlMaxValue_)
        roundedControlValue = controlMaxValue_;
    if(roundedControlValue < controlMinValue_)
        roundedControlValue = controlMinValue_;
    
    return roundedControlValue;
}

// Add a new OSC input to this MIDI control
void OscMidiConverter::addControl(const string& oscPath, int oscParamNumber, float oscMinValue,
                                  float oscMaxValue, float oscCenterValue, int outOfRangeBehavior) {
	// First remove any existing mapping with these exact parameters
	removeControl(oscPath);

#ifdef DEBUG_OSC_MIDI_CONVERTER
    std::cout << "OscMidiConverter: adding path " << oscPath << std::endl;
#endif
    
	// Insert the mapping
	OscInput input;
    
    input.oscParamNumber = oscParamNumber;
    input.oscMinValue = oscMinValue;
    input.oscMaxValue = oscMaxValue;
    
    // Calculate the normalized center value which will be subtracted
    // from the scaled input. Do this once now to save computation.
    if(oscMinValue == oscMaxValue)
        input.oscScaledCenterValue = 0.5;
    else {
        input.oscScaledCenterValue = (oscCenterValue - oscMinValue) / (oscMaxValue - oscMinValue);
        if(input.oscScaledCenterValue < 0)
            input.oscScaledCenterValue = 0;
        if(input.oscScaledCenterValue > 1.0)
            input.oscScaledCenterValue = 1.0;
    }
    input.outOfRangeBehavior = outOfRangeBehavior;
    input.uniqueId = lastUniqueId_++;
    inputs_[oscPath] = input;
	
	// Register for the relevant OSC message
	addOscListener(oscPath);
}

// Remove an existing OSC input
void OscMidiConverter::removeControl(const string& oscPath) {
    // Find the affected control and its ID
    if(inputs_.count(oscPath) == 0)
        return;
    int controlId = inputs_[oscPath].uniqueId;
    
#ifdef DEBUG_OSC_MIDI_CONVERTER
    std::cout << "OscMidiConverter: removing path " << oscPath << std::endl;
#endif
    
    // Look for any active inputs on this channel
    for(int i = 0; i < 16; i++) {
        int channelModulatedId = idWithChannel(i, controlId);
        if(lastValues_.count(channelModulatedId) == 0)
            continue;
        
        // Found a last value. Subtract it off and get new value
        float lastValueThisChannel = lastValues_[channelModulatedId];
        currentValue_[i] -= lastValueThisChannel;
        
        // Remove this value from the set of active inputs
        lastValues_.erase(channelModulatedId);
        
        // Send the new value after removing this one
        sendCurrentValue(keyboardSegment_.outputPort(), i, -1, true);
    }
    
    // Having removed any active inputs, now remove the control itself
    // TODO: mutex protection
    inputs_.erase(oscPath);
    
    removeOscListener(oscPath);
}

void OscMidiConverter::removeAllControls() {
    // Clear all active inputs and send default values to all channels
    for(int i = 0; i < 16; i++)
        sendDefaultValue(i);
    lastValues_.clear();
    inputs_.clear();
    lastUniqueId_ = 0;
    removeAllOscListeners();
}

// Update the minimum input value of an existing path
void OscMidiConverter::setControlMinValue(const string& oscPath, float newValue) {
    if(inputs_.count(oscPath) == 0)
        return;
    inputs_[oscPath].oscMinValue = newValue;
}

// Update the maximum input value of an existing path
void OscMidiConverter::setControlMaxValue(const string& oscPath, float newValue) {
    if(inputs_.count(oscPath) == 0)
        return;
    inputs_[oscPath].oscMaxValue = newValue;
}

// Update the center input value of an existing path
void OscMidiConverter::setControlCenterValue(const string& oscPath, float newValue) {
    if(inputs_.count(oscPath) == 0)
        return;
    float minValue, maxValue, scaledCenterValue;
    minValue = inputs_[oscPath].oscMinValue;
    maxValue = inputs_[oscPath].oscMaxValue;
    
    if(minValue == maxValue)
        scaledCenterValue = 0.0;
    else
        scaledCenterValue = (newValue - minValue) / (maxValue - minValue);
    if(scaledCenterValue < 0)
        scaledCenterValue = 0;
    if(scaledCenterValue > 1.0)
        scaledCenterValue = 1.0;
    
    inputs_[oscPath].oscScaledCenterValue = scaledCenterValue;
}

// Update the out of range behavior for an existing path
void OscMidiConverter::setControlOutOfRangeBehavior(const string& oscPath, int newBehavior) {
    if(inputs_.count(oscPath) == 0)
        return;
    inputs_[oscPath].outOfRangeBehavior = newBehavior;
}

// Reset any active previous values on the given channel
// 'send' indicated whether to send the value when finished
// even if it hasn't changed
void OscMidiConverter::clearLastValues(int channel, bool send) {
    std::map<int, float>::iterator it = lastValues_.begin();
    
    bool erased = false;
    
    while(it != lastValues_.end()) {
        // Look for any last values matching this channel
        if((it->first & 0x0F) == channel) {
            lastValues_.erase(it++);
            erased = true;
        }
        else
            it++;
    }
 
    currentValue_[channel] = 0;
    lastOutputValue_[channel] = -1;
    
    // If any last values were erased and send is enabled,
    // resend the current values
    if(erased || send)
        sendDefaultValue(channel);
}

// OSC Handler, called by the data source (PianoKeyboard in this case). Check path against stored
// inputs and map to MIDI accordingly

bool OscMidiConverter::oscHandlerMethod(const char *path, const char *types, int numValues, lo_arg **values, void *data) {
#ifdef DEBUG_OSC_MIDI_CONVERTER
    std::cout << "OscMidiConverter: received path " << path << std::endl;
#endif
	if(midiOutputController_ == 0 || controller_ == MidiKeyboardSegment::kControlDisabled)
		return false;
    
    //double before = Time::getMillisecondCounterHiRes();  // DEBUG
    
	// First value should always be MIDI note number (integer type) so we can retrieve
	// information from the PianoKeyboard class
	if(numValues < 1)
		return false;
	if(types[0] != 'i')
		return false;
	int midiNoteNumber = values[0]->i;
    
	// Get the MIDI retransmission channel from the note number
	if(keyboard_.key(midiNoteNumber) == 0)
		return false;
	int midiChannel = keyboard_.key(midiNoteNumber)->midiChannel();
	if(midiChannel < 0 || midiChannel > 15) {
#ifdef DEBUG_OSC_MIDI_CONVERTER
        std::cout << "OscMidiConverter: no retransmission channel on note " << midiNoteNumber << std::endl;
#endif
		return false;
    }
    
    // Find the relevant input and make sure this OSC message has enough parameters
    if(inputs_.count(path) == 0)
        return false;
    OscInput const& input = inputs_[path];
    if(input.oscParamNumber >= numValues)
        return false;
    
    // Find the relevant input value from this OSC message
    float oscParamValue;
    if(types[input.oscParamNumber] == 'f')
        oscParamValue = values[input.oscParamNumber]->f;
    else if(types[input.oscParamNumber] == 'i')
        oscParamValue = (float)values[input.oscParamNumber]->i;
    else
        return false;
    
    // Scale input to a 0-1 range, then to the output range. There's a special case for MIDI pitch wheel,
    // where if the range is set to 0, it means to use the segment-wide pitch wheel range. This is done so
    // we don't have to cache multiple copies of the pitch wheel range in every OSC-MIDI converter.
    float scaledValue;
    if(controller_ == MidiKeyboardSegment::kControlPitchWheel &&
       input.oscMaxValue == 0 && input.oscMinValue == 0) {
        float pitchWheelRange = keyboardSegment_.midiPitchWheelRange();
        scaledValue = (oscParamValue + pitchWheelRange) / (2.0 * pitchWheelRange);
    }
    else
        scaledValue = (oscParamValue - input.oscMinValue) / (input.oscMaxValue - input.oscMinValue);

#ifdef DEBUG_OSC_MIDI_CONVERTER
    std::cout << "port " << keyboardSegment_.outputPort() << " received input " << oscParamValue << " which scales to " << scaledValue << std::endl;
#endif
    
    // Figure out what to do with an out of range value...
    if(scaledValue < 0.0 || scaledValue > 1.0) {
        if(input.outOfRangeBehavior == kOutOfRangeClip) {
            if(scaledValue < 0.0)
                scaledValue = 0.0;
            if(scaledValue > 1.0)
                scaledValue = 1.0;
        }
        else if(input.outOfRangeBehavior == kOutOfRangeExtrapolate) {
            // Do nothing
        }
        else	// Ignore or unknown behavior
            return false;
    }
    
    //double midpoint = Time::getMillisecondCounterHiRes(); // DEBUG
    
    // Now subtract the normalized center value, which may put the range outside 0-1
    // but this is expected. For example, in a pitch wheel situation, we might move
    // to a range of -0.5 to 0.5.
    scaledValue -= input.oscScaledCenterValue;

    // Look for previous input with this path and channel and remove it
    int channelModulatedId = idWithChannel(midiChannel, input.uniqueId);
    if(lastValues_.count(channelModulatedId) > 0) {
        // Found a last value. Subtract it off and replace with our current value
        float lastValueThisChannel = lastValues_[channelModulatedId];
        currentValue_[midiChannel] -= lastValueThisChannel;
#ifdef DEBUG_OSC_MIDI_CONVERTER
        std::cout << "found and removed " << lastValueThisChannel << ", now have " << currentValue_[midiChannel] << std::endl;
#endif
    }
    
    lastValues_[channelModulatedId] = scaledValue;
    currentValue_[midiChannel] += scaledValue;

    // Send the total current value as a MIDI controller
    sendCurrentValue(keyboardSegment_.outputPort(), midiChannel, midiNoteNumber, false);
    
    //double after = Time::getMillisecondCounterHiRes(); // DEBUG
    
    //std::cout << "OscMidiConverter: total " << after - before << "ms, of which " << after - midpoint << " in 2nd half\n";
    
	return true;
}

// Send the current sum value of all OSC inputs as a MIDI message
void OscMidiConverter::sendCurrentValue(int port, int channel, int note, bool force) {
    if(midiOutputController_ == 0 || channel < 0 || channel > 15)
        return;
    
    // TODO: what about values that are centered by default e.g. pitch?
    /*float controlValue = (float)controlCenterValue_ + (float)controlMinValue_
                            + currentValue_[channel]*(float)(controlMaxValue_ - controlMinValue_);
    
    if(incomingController_ != MidiKeyboardSegment::kControlDisabled)
        controlValue += keyboardSegment_.controllerValue(incomingController_) - incomingControllerCenterValue_;
    
    std::cout << "sending value " << currentValue_[channel] << " as " << controlValue << std::endl;
    int roundedControlValue = (int)roundf(controlValue);
    if(roundedControlValue > controlMaxValue_)
        roundedControlValue = controlMaxValue_;
    if(roundedControlValue < controlMinValue_)
        roundedControlValue = controlMinValue_;*/
    
    int roundedControlValue = currentControllerValue(channel);
    
    // If this is the same ultimate CC value as before, don't resend unless forced
    if(roundedControlValue == lastOutputValue_[channel] && !force)
        return;
    lastOutputValue_[channel] = roundedControlValue;
    
    // Four cases: Pitch Wheel messages, aftertouch, 14-bit controls (major and minor controllers), ordinary 7-bit controls
    if(controller_ == MidiKeyboardSegment::kControlPitchWheel) {
        midiOutputController_->sendPitchWheel(port, channel, roundedControlValue);
    }
    else if(controller_ == MidiKeyboardSegment::kControlChannelAftertouch) {
        midiOutputController_->sendAftertouchChannel(port, channel, roundedControlValue);
    }
    else if(controller_ == MidiKeyboardSegment::kControlPolyphonicAftertouch && note >= 0) {
        midiOutputController_->sendAftertouchPoly(port, channel, note, roundedControlValue);
    }
    else if(controllerIs14Bit_) {
        // LSB for controllers 0-31 are found on controllers 32-63
        int lsb = roundedControlValue & 0x007F;
        int msb = (roundedControlValue >> 7) & 0x007F;
        
        midiOutputController_->sendControlChange(port, channel, controller_, msb);
        midiOutputController_->sendControlChange(port, channel, controller_ + 32, lsb);
    }
    else {	// 7 bit
        midiOutputController_->sendControlChange(port, channel, controller_, roundedControlValue);
    }
}