From c3f05e4e96148bab942835ba84b7afa065de3f7e Mon Sep 17 00:00:00 2001 From: falkTX Date: Mon, 13 Jul 2020 12:17:56 +0100 Subject: [PATCH] Initial implementation of MIDI learn (per plugin, not global) Closes #26 Signed-off-by: falkTX --- source/backend/CarlaBackend.h | 7 +- source/backend/CarlaEngine.hpp | 5 +- source/backend/CarlaPlugin.hpp | 7 ++ source/backend/engine/CarlaEngineData.cpp | 5 ++ source/backend/engine/CarlaEngineJack.cpp | 10 +-- source/backend/engine/CarlaEnginePorts.cpp | 9 +-- source/backend/plugin/CarlaPlugin.cpp | 59 ++++++++++++++++ source/backend/plugin/CarlaPluginBridge.cpp | 11 +++ source/backend/plugin/CarlaPluginInternal.cpp | 1 + source/backend/plugin/CarlaPluginInternal.hpp | 8 ++- source/backend/plugin/CarlaPluginJuce.cpp | 18 +++-- .../backend/plugin/CarlaPluginLADSPADSSI.cpp | 18 +++-- source/backend/plugin/CarlaPluginLV2.cpp | 18 +++-- source/backend/plugin/CarlaPluginNative.cpp | 24 ++++--- source/backend/plugin/CarlaPluginVST2.cpp | 18 +++-- source/frontend/carla_backend.py | 14 ++-- source/frontend/carla_widgets.py | 68 ++++++++++++------- source/utils/CarlaStateUtils.cpp | 5 +- 18 files changed, 229 insertions(+), 76 deletions(-) diff --git a/source/backend/CarlaBackend.h b/source/backend/CarlaBackend.h index 1ee283bfa..1dea781a0 100644 --- a/source/backend/CarlaBackend.h +++ b/source/backend/CarlaBackend.h @@ -779,10 +779,15 @@ typedef enum { */ CONTROL_INDEX_MIDI_PITCHBEND = 131, + /*! + * Special value to indicate MIDI learn. + */ + CONTROL_INDEX_MIDI_LEARN = 132, + /*! * Highest index allowed for mappings. */ - CONTROL_INDEX_MAX_ALLOWED = CONTROL_INDEX_MIDI_PITCHBEND + CONTROL_INDEX_MAX_ALLOWED = CONTROL_INDEX_MIDI_LEARN } SpecialMappedControlIndex; diff --git a/source/backend/CarlaEngine.hpp b/source/backend/CarlaEngine.hpp index afcdfb40f..c6dac6f81 100644 --- a/source/backend/CarlaEngine.hpp +++ b/source/backend/CarlaEngine.hpp @@ -178,6 +178,7 @@ struct CARLA_API EngineControlEvent { EngineControlEventType type; //!< Control-Event type. uint16_t param; //!< Parameter Id, midi bank or midi program. float value; //!< Parameter value, normalized to 0.0f<->1.0f. + bool handled; //!< Indicates that event was handled/received at least once. /*! * Convert this control event into MIDI data. @@ -575,12 +576,12 @@ public: * Get the event at @a index. * @note You must only call this for input ports. */ - virtual const EngineEvent& getEvent(uint32_t index) const noexcept; + virtual EngineEvent& getEvent(uint32_t index) const noexcept; /*! * Get the event at @a index, faster unchecked version. */ - virtual const EngineEvent& getEventUnchecked(uint32_t index) const noexcept; + virtual EngineEvent& getEventUnchecked(uint32_t index) const noexcept; /*! * Write a control event into the buffer. diff --git a/source/backend/CarlaPlugin.hpp b/source/backend/CarlaPlugin.hpp index 7a63b9a32..55fdd5e45 100644 --- a/source/backend/CarlaPlugin.hpp +++ b/source/backend/CarlaPlugin.hpp @@ -51,6 +51,7 @@ class CarlaEngineEventPort; class CarlaEngineCVSourcePorts; class CarlaEngineBridge; struct CarlaStateSave; +struct EngineEvent; // ----------------------------------------------------------------------- @@ -891,6 +892,12 @@ public: */ CarlaEngineEventPort* getDefaultEventOutPort() const noexcept; + /*! + * Check if the plugin is interested on MIDI learn, and if so, map this event to the parameter that wants it. + * Event MUST be of control type and not have been handled before. + */ + void checkForMidiLearn(EngineEvent& event) noexcept; + /*! * Get the plugin's type native handle. * This will be LADSPA_Handle, LV2_Handle, etc. diff --git a/source/backend/engine/CarlaEngineData.cpp b/source/backend/engine/CarlaEngineData.cpp index e3b8f8bde..7a513ccfb 100644 --- a/source/backend/engine/CarlaEngineData.cpp +++ b/source/backend/engine/CarlaEngineData.cpp @@ -108,18 +108,21 @@ void EngineEvent::fillFromMidiData(const uint8_t size, const uint8_t* const data ctrl.type = kEngineControlEventTypeMidiBank; ctrl.param = midiBank; ctrl.value = 0.0f; + ctrl.handled = true; } else if (midiControl == MIDI_CONTROL_ALL_SOUND_OFF) { ctrl.type = kEngineControlEventTypeAllSoundOff; ctrl.param = 0; ctrl.value = 0.0f; + ctrl.handled = true; } else if (midiControl == MIDI_CONTROL_ALL_NOTES_OFF) { ctrl.type = kEngineControlEventTypeAllNotesOff; ctrl.param = 0; ctrl.value = 0.0f; + ctrl.handled = true; } else { @@ -130,6 +133,7 @@ void EngineEvent::fillFromMidiData(const uint8_t size, const uint8_t* const data ctrl.type = kEngineControlEventTypeParameter; ctrl.param = midiControl; ctrl.value = float(midiValue)/127.0f; + ctrl.handled = false; } } else if (midiStatus == MIDI_STATUS_PROGRAM_CHANGE) @@ -143,6 +147,7 @@ void EngineEvent::fillFromMidiData(const uint8_t size, const uint8_t* const data ctrl.type = kEngineControlEventTypeMidiProgram; ctrl.param = midiProgram; ctrl.value = 0.0f; + ctrl.handled = true; } else { diff --git a/source/backend/engine/CarlaEngineJack.cpp b/source/backend/engine/CarlaEngineJack.cpp index dbbdc0f8d..5b9962376 100644 --- a/source/backend/engine/CarlaEngineJack.cpp +++ b/source/backend/engine/CarlaEngineJack.cpp @@ -124,7 +124,9 @@ static /* */ PortNameToId kPortNameToIdFallbackNC = { 0, 0, { '\0' }, { '\0' static const PortNameToId kPortNameToIdFallback = { 0, 0, { '\0' }, { '\0' } }; static const ConnectionToId kConnectionToIdFallback = { 0, 0, 0, 0, 0 }; #endif -static const EngineEvent kFallbackJackEngineEvent = { kEngineEventTypeNull, 0, 0, {{ kEngineControlEventTypeNull, 0, 0.0f }} }; +static EngineEvent kFallbackJackEngineEvent = { + kEngineEventTypeNull, 0, 0, {{ kEngineControlEventTypeNull, 0, 0.0f, true }} +}; // ----------------------------------------------------------------------- // Carla Engine Port removal helper @@ -460,7 +462,7 @@ public: } CARLA_SAFE_EXCEPTION_RETURN("jack_midi_get_event_count", 0); } - const EngineEvent& getEvent(const uint32_t index) const noexcept override + EngineEvent& getEvent(const uint32_t index) const noexcept override { if (fJackPort == nullptr) return CarlaEngineEventPort::getEvent(index); @@ -471,7 +473,7 @@ public: return getEventUnchecked(index); } - const EngineEvent& getEventUnchecked(uint32_t index) const noexcept override + EngineEvent& getEventUnchecked(uint32_t index) const noexcept override { #ifndef BUILD_BRIDGE_ALTERNATIVE_ARCH if (index < fCvSourceEventCount) @@ -529,7 +531,7 @@ public: uint8_t data[3] = { 0, 0, 0 }; - EngineControlEvent ctrlEvent = { type, param, value }; + EngineControlEvent ctrlEvent = { type, param, value, false }; const uint8_t size = ctrlEvent.convertToMidiData(channel, data); if (size == 0) diff --git a/source/backend/engine/CarlaEnginePorts.cpp b/source/backend/engine/CarlaEnginePorts.cpp index 0f1812a42..597c5658d 100644 --- a/source/backend/engine/CarlaEnginePorts.cpp +++ b/source/backend/engine/CarlaEnginePorts.cpp @@ -31,8 +31,9 @@ CARLA_BACKEND_START_NAMESPACE // ----------------------------------------------------------------------- // Fallback data -static const EngineEvent kFallbackEngineEvent = { kEngineEventTypeNull, 0, 0, {{ kEngineControlEventTypeNull, 0, 0.0f }} }; -//static CarlaEngineEventCV kFallbackEngineEventCV = { nullptr, (uint32_t)-1, 0.0f }; +static EngineEvent kFallbackEngineEvent = { + kEngineEventTypeNull, 0, 0, {{ kEngineControlEventTypeNull, 0, 0.0f, true }} +}; // ----------------------------------------------------------------------- // Carla Engine port (Abstract) @@ -169,7 +170,7 @@ uint32_t CarlaEngineEventPort::getEventCount() const noexcept return i; } -const EngineEvent& CarlaEngineEventPort::getEvent(const uint32_t index) const noexcept +EngineEvent& CarlaEngineEventPort::getEvent(const uint32_t index) const noexcept { CARLA_SAFE_ASSERT_RETURN(kIsInput, kFallbackEngineEvent); CARLA_SAFE_ASSERT_RETURN(fBuffer != nullptr, kFallbackEngineEvent); @@ -179,7 +180,7 @@ const EngineEvent& CarlaEngineEventPort::getEvent(const uint32_t index) const no return fBuffer[index]; } -const EngineEvent& CarlaEngineEventPort::getEventUnchecked(const uint32_t index) const noexcept +EngineEvent& CarlaEngineEventPort::getEventUnchecked(const uint32_t index) const noexcept { return fBuffer[index]; } diff --git a/source/backend/plugin/CarlaPlugin.cpp b/source/backend/plugin/CarlaPlugin.cpp index da9ef05c1..0b484e2d9 100644 --- a/source/backend/plugin/CarlaPlugin.cpp +++ b/source/backend/plugin/CarlaPlugin.cpp @@ -21,6 +21,7 @@ #include "CarlaBackendUtils.hpp" #include "CarlaBase64Utils.hpp" #include "CarlaMathUtils.hpp" +#include "CarlaMIDI.h" #include "CarlaPluginUI.hpp" #include "CarlaScopeUtils.hpp" #include "CarlaStringList.hpp" @@ -1789,6 +1790,11 @@ void CarlaPlugin::setParameterMappedControlIndex(const uint32_t parameterId, con paramData.mappedControlIndex = index; #ifndef BUILD_BRIDGE_ALTERNATIVE_ARCH + if (index == CONTROL_INDEX_MIDI_LEARN) + pData->midiLearnParameterIndex = static_cast(parameterId); + else + pData->midiLearnParameterIndex = -1; + pData->engine->callback(sendCallback, sendOsc, ENGINE_CALLBACK_PARAMETER_MAPPED_CONTROL_INDEX_CHANGED, pData->id, @@ -2258,6 +2264,33 @@ void CarlaPlugin::idle() 0, 0.0f, nullptr); } } break; + + case kPluginPostRtEventMidiLearn: { + CARLA_SAFE_ASSERT_BREAK(event.value1 >= 0 && event.value1 < MAX_MIDI_CHANNELS); + CARLA_SAFE_ASSERT_BREAK(event.value2 >= 0 && event.value2 < MAX_MIDI_NOTE); + + if (event.sendCallback) + { + const int32_t parameterId = event.value1; + const int32_t midiCC = event.value2; + const int32_t midiChannel = event.value2; + + pData->engine->callback(true, true, + ENGINE_CALLBACK_PARAMETER_MAPPED_CONTROL_INDEX_CHANGED, + pData->id, + parameterId, + midiCC, + 0, 0.0f, nullptr); + + pData->engine->callback(true, true, + ENGINE_CALLBACK_PARAMETER_MIDI_CHANNEL_CHANGED, + pData->id, + parameterId, + midiChannel, + 0, 0.0f, nullptr); + + } + } break; } } } @@ -2405,6 +2438,7 @@ void CarlaPlugin::uiIdle() { case kPluginPostRtEventNull: case kPluginPostRtEventDebug: + case kPluginPostRtEventMidiLearn: break; case kPluginPostRtEventParameterChange: @@ -2538,6 +2572,31 @@ CarlaEngineEventPort* CarlaPlugin::getDefaultEventOutPort() const noexcept return pData->event.portOut; } +void CarlaPlugin::checkForMidiLearn(EngineEvent& event) noexcept +{ + if (pData->midiLearnParameterIndex < 0) + return; + if (event.ctrl.param == MIDI_CONTROL_BANK_SELECT || event.ctrl.param == MIDI_CONTROL_BANK_SELECT__LSB) + return; + if (event.ctrl.param >= MAX_MIDI_CONTROL) + return; + + const uint32_t parameterId = static_cast(pData->midiLearnParameterIndex); + CARLA_SAFE_ASSERT_UINT2_RETURN(parameterId < pData->param.count, parameterId, pData->param.count,); + + ParameterData& paramData(pData->param.data[parameterId]); + CARLA_SAFE_ASSERT_INT_RETURN(paramData.mappedControlIndex == CONTROL_INDEX_MIDI_LEARN, + paramData.mappedControlIndex,); + + event.ctrl.handled = true; + paramData.mappedControlIndex = static_cast(event.ctrl.param); + paramData.midiChannel = event.channel; + + pData->postponeRtEvent(kPluginPostRtEventMidiLearn, true, + pData->midiLearnParameterIndex, event.ctrl.param, event.channel, 0.0f); + pData->midiLearnParameterIndex = -1; +} + void* CarlaPlugin::getNativeHandle() const noexcept { return nullptr; diff --git a/source/backend/plugin/CarlaPluginBridge.cpp b/source/backend/plugin/CarlaPluginBridge.cpp index c80f9d358..d6959ea7a 100644 --- a/source/backend/plugin/CarlaPluginBridge.cpp +++ b/source/backend/plugin/CarlaPluginBridge.cpp @@ -1365,6 +1365,17 @@ public: case kEngineControlEventTypeParameter: #ifndef BUILD_BRIDGE_ALTERNATIVE_ARCH + // non-midi + if (event.channel == kEngineEventNonMidiChannel) + { + const uint32_t k = ctrlEvent.param; + CARLA_SAFE_ASSERT_CONTINUE(k < pData->param.count); + + // value = pData->param.getFinalUnnormalizedValue(k, ctrlEvent.value); + // setParameterValueRT(k, value, true); + continue; + } + // Control backend stuff if (event.channel == pData->ctrlChannel) { diff --git a/source/backend/plugin/CarlaPluginInternal.cpp b/source/backend/plugin/CarlaPluginInternal.cpp index 880b15800..b530e3095 100644 --- a/source/backend/plugin/CarlaPluginInternal.cpp +++ b/source/backend/plugin/CarlaPluginInternal.cpp @@ -673,6 +673,7 @@ CarlaPlugin::ProtectedData::ProtectedData(CarlaEngine* const eng, const uint idx ctrlChannel(0), extraHints(0x0), #ifndef BUILD_BRIDGE_ALTERNATIVE_ARCH + midiLearnParameterIndex(-1), transientTryCounter(0), transientFirstTry(true), #endif diff --git a/source/backend/plugin/CarlaPluginInternal.hpp b/source/backend/plugin/CarlaPluginInternal.hpp index a319c40a8..4e93a2363 100644 --- a/source/backend/plugin/CarlaPluginInternal.hpp +++ b/source/backend/plugin/CarlaPluginInternal.hpp @@ -86,7 +86,8 @@ enum PluginPostRtEventType { kPluginPostRtEventProgramChange, // index kPluginPostRtEventMidiProgramChange, // index kPluginPostRtEventNoteOn, // channel, note, velo - kPluginPostRtEventNoteOff // channel, note + kPluginPostRtEventNoteOff, // channel, note + kPluginPostRtEventMidiLearn // param, cc, channel }; /*! @@ -244,8 +245,9 @@ struct CarlaPlugin::ProtectedData { int8_t ctrlChannel; uint extraHints; #ifndef BUILD_BRIDGE_ALTERNATIVE_ARCH - uint transientTryCounter; - bool transientFirstTry; + int32_t midiLearnParameterIndex; + uint transientTryCounter; + bool transientFirstTry; #endif // data 1 diff --git a/source/backend/plugin/CarlaPluginJuce.cpp b/source/backend/plugin/CarlaPluginJuce.cpp index c8de902be..0dca13306 100644 --- a/source/backend/plugin/CarlaPluginJuce.cpp +++ b/source/backend/plugin/CarlaPluginJuce.cpp @@ -864,7 +864,7 @@ public: for (uint32_t i=0, numEvents=pData->event.portIn->getEventCount(); i < numEvents; ++i) { - const EngineEvent& event(pData->event.portIn->getEvent(i)); + EngineEvent& event(pData->event.portIn->getEvent(i)); if (event.time >= frames) continue; @@ -875,7 +875,7 @@ public: break; case kEngineEventTypeControl: { - const EngineControlEvent& ctrlEvent(event.ctrl); + EngineControlEvent& ctrlEvent(event.ctrl); switch (ctrlEvent.type) { @@ -892,6 +892,7 @@ public: const uint32_t k = ctrlEvent.param; CARLA_SAFE_ASSERT_CONTINUE(k < pData->param.count); + ctrlEvent.handled = true; value = pData->param.getFinalUnnormalizedValue(k, ctrlEvent.value); setParameterValueRT(k, value, true); continue; @@ -902,17 +903,17 @@ public: { if (MIDI_IS_CONTROL_BREATH_CONTROLLER(ctrlEvent.param) && (pData->hints & PLUGIN_CAN_DRYWET) != 0) { + ctrlEvent.handled = true; value = ctrlEvent.value; setDryWetRT(value, true); } - - if (MIDI_IS_CONTROL_CHANNEL_VOLUME(ctrlEvent.param) && (pData->hints & PLUGIN_CAN_VOLUME) != 0) + else if (MIDI_IS_CONTROL_CHANNEL_VOLUME(ctrlEvent.param) && (pData->hints & PLUGIN_CAN_VOLUME) != 0) { + ctrlEvent.handled = true; value = ctrlEvent.value*127.0f/100.0f; setVolumeRT(value, true); } - - if (MIDI_IS_CONTROL_BALANCE(ctrlEvent.param) && (pData->hints & PLUGIN_CAN_BALANCE) != 0) + else if (MIDI_IS_CONTROL_BALANCE(ctrlEvent.param) && (pData->hints & PLUGIN_CAN_BALANCE) != 0) { float left, right; value = ctrlEvent.value/0.5f - 1.0f; @@ -933,6 +934,7 @@ public: right = 1.0f; } + ctrlEvent.handled = true; setBalanceLeftRT(left, true); setBalanceRightRT(right, true); } @@ -951,6 +953,7 @@ public: if ((pData->param.data[k].hints & PARAMETER_IS_AUTOMABLE) == 0) continue; + ctrlEvent.handled = true; value = pData->param.getFinalUnnormalizedValue(k, ctrlEvent.value); setParameterValueRT(k, value, true); } @@ -965,6 +968,9 @@ public: fMidiBuffer.addEvent(midiData, 3, static_cast(event.time)); } + if (! ctrlEvent.handled) + checkForMidiLearn(event); + break; } // case kEngineControlEventTypeParameter diff --git a/source/backend/plugin/CarlaPluginLADSPADSSI.cpp b/source/backend/plugin/CarlaPluginLADSPADSSI.cpp index da6bc0f7f..9bcec0928 100644 --- a/source/backend/plugin/CarlaPluginLADSPADSSI.cpp +++ b/source/backend/plugin/CarlaPluginLADSPADSSI.cpp @@ -1602,7 +1602,7 @@ public: for (uint32_t i=0, numEvents=pData->event.portIn->getEventCount(); i < numEvents; ++i) { - const EngineEvent& event(pData->event.portIn->getEvent(i)); + EngineEvent& event(pData->event.portIn->getEvent(i)); uint32_t eventTime = event.time; CARLA_SAFE_ASSERT_UINT2_CONTINUE(eventTime < frames, eventTime, frames); @@ -1637,7 +1637,7 @@ public: break; case kEngineEventTypeControl: { - const EngineControlEvent& ctrlEvent(event.ctrl); + EngineControlEvent& ctrlEvent(event.ctrl); switch (ctrlEvent.type) { @@ -1654,6 +1654,7 @@ public: const uint32_t k = ctrlEvent.param; CARLA_SAFE_ASSERT_CONTINUE(k < pData->param.count); + ctrlEvent.handled = true; value = pData->param.getFinalUnnormalizedValue(k, ctrlEvent.value); setParameterValueRT(k, value, true); continue; @@ -1664,17 +1665,17 @@ public: { if (MIDI_IS_CONTROL_BREATH_CONTROLLER(ctrlEvent.param) && (pData->hints & PLUGIN_CAN_DRYWET) != 0) { + ctrlEvent.handled = true; value = ctrlEvent.value; setDryWetRT(value, true); } - - if (MIDI_IS_CONTROL_CHANNEL_VOLUME(ctrlEvent.param) && (pData->hints & PLUGIN_CAN_VOLUME) != 0) + else if (MIDI_IS_CONTROL_CHANNEL_VOLUME(ctrlEvent.param) && (pData->hints & PLUGIN_CAN_VOLUME) != 0) { + ctrlEvent.handled = true; value = ctrlEvent.value*127.0f/100.0f; setVolumeRT(value, true); } - - if (MIDI_IS_CONTROL_BALANCE(ctrlEvent.param) && (pData->hints & PLUGIN_CAN_BALANCE) != 0) + else if (MIDI_IS_CONTROL_BALANCE(ctrlEvent.param) && (pData->hints & PLUGIN_CAN_BALANCE) != 0) { float left, right; value = ctrlEvent.value/0.5f - 1.0f; @@ -1695,6 +1696,7 @@ public: right = 1.0f; } + ctrlEvent.handled = true; setBalanceLeftRT(left, true); setBalanceRightRT(right, true); } @@ -1712,6 +1714,7 @@ public: if ((pData->param.data[k].hints & PARAMETER_IS_AUTOMABLE) == 0) continue; + ctrlEvent.handled = true; value = pData->param.getFinalUnnormalizedValue(k, ctrlEvent.value); setParameterValueRT(k, value, true); } @@ -1731,6 +1734,9 @@ public: seqEvent.data.control.value = int8_t(ctrlEvent.value*127.0f); } + if (! ctrlEvent.handled) + checkForMidiLearn(event); + break; } // case kEngineControlEventTypeParameter diff --git a/source/backend/plugin/CarlaPluginLV2.cpp b/source/backend/plugin/CarlaPluginLV2.cpp index f2e3b8f00..ed8ce8cb0 100644 --- a/source/backend/plugin/CarlaPluginLV2.cpp +++ b/source/backend/plugin/CarlaPluginLV2.cpp @@ -3749,7 +3749,7 @@ public: for (uint32_t i=0; i < numEvents; ++i) { - const EngineEvent& event(fEventsIn.ctrl->port->getEvent(i)); + EngineEvent& event(fEventsIn.ctrl->port->getEvent(i)); uint32_t eventTime = event.time; CARLA_SAFE_ASSERT_UINT2_CONTINUE(eventTime < frames, eventTime, frames); @@ -3821,7 +3821,7 @@ public: break; case kEngineEventTypeControl: { - const EngineControlEvent& ctrlEvent(event.ctrl); + EngineControlEvent& ctrlEvent(event.ctrl); switch (ctrlEvent.type) { @@ -3838,6 +3838,7 @@ public: const uint32_t k = ctrlEvent.param; CARLA_SAFE_ASSERT_CONTINUE(k < pData->param.count); + ctrlEvent.handled = true; value = pData->param.getFinalUnnormalizedValue(k, ctrlEvent.value); setParameterValueRT(k, value, true); continue; @@ -3848,17 +3849,17 @@ public: { if (MIDI_IS_CONTROL_BREATH_CONTROLLER(ctrlEvent.param) && (pData->hints & PLUGIN_CAN_DRYWET) != 0) { + ctrlEvent.handled = true; value = ctrlEvent.value; setDryWetRT(value, true); } - - if (MIDI_IS_CONTROL_CHANNEL_VOLUME(ctrlEvent.param) && (pData->hints & PLUGIN_CAN_VOLUME) != 0) + else if (MIDI_IS_CONTROL_CHANNEL_VOLUME(ctrlEvent.param) && (pData->hints & PLUGIN_CAN_VOLUME) != 0) { + ctrlEvent.handled = true; value = ctrlEvent.value*127.0f/100.0f; setVolumeRT(value, true); } - - if (MIDI_IS_CONTROL_BALANCE(ctrlEvent.param) && (pData->hints & PLUGIN_CAN_BALANCE) != 0) + else if (MIDI_IS_CONTROL_BALANCE(ctrlEvent.param) && (pData->hints & PLUGIN_CAN_BALANCE) != 0) { float left, right; value = ctrlEvent.value/0.5f - 1.0f; @@ -3879,6 +3880,7 @@ public: right = 1.0f; } + ctrlEvent.handled = true; setBalanceLeftRT(left, true); setBalanceRightRT(right, true); } @@ -3897,6 +3899,7 @@ public: if ((pData->param.data[k].hints & PARAMETER_IS_AUTOMABLE) == 0) continue; + ctrlEvent.handled = true; value = pData->param.getFinalUnnormalizedValue(k, ctrlEvent.value); setParameterValueRT(k, value, true); } @@ -3920,6 +3923,9 @@ public: lv2midi_put_event(&evInMidiStates[fEventsIn.ctrlIndex], mtime, 3, midiData); } + if (! ctrlEvent.handled) + checkForMidiLearn(event); + break; } // case kEngineControlEventTypeParameter diff --git a/source/backend/plugin/CarlaPluginNative.cpp b/source/backend/plugin/CarlaPluginNative.cpp index 92593f1c4..9493811c3 100644 --- a/source/backend/plugin/CarlaPluginNative.cpp +++ b/source/backend/plugin/CarlaPluginNative.cpp @@ -97,7 +97,9 @@ CARLA_BACKEND_START_NAMESPACE // Fallback data static const CustomData kCustomDataFallback = { nullptr, nullptr, nullptr }; -static const EngineEvent kNullEngineEvent = { kEngineEventTypeNull, 0, 0, {} }; +static EngineEvent kNullEngineEvent = { + kEngineEventTypeNull, 0, 0, {{ kEngineControlEventTypeNull, 0, 0.0f, true }} +}; // ----------------------------------------------------------------------- @@ -1684,7 +1686,7 @@ public: } } - const EngineEvent& findNextEvent() + EngineEvent& findNextEvent() { if (fMidiIn.count == 1) { @@ -1909,7 +1911,7 @@ public: for (;;) { - const EngineEvent& event(findNextEvent()); + EngineEvent& event(findNextEvent()); if (event.type == kEngineEventTypeNull) break; @@ -1959,7 +1961,7 @@ public: break; case kEngineEventTypeControl: { - const EngineControlEvent& ctrlEvent = event.ctrl; + EngineControlEvent& ctrlEvent(event.ctrl); switch (ctrlEvent.type) { @@ -1976,6 +1978,7 @@ public: const uint32_t k = ctrlEvent.param; CARLA_SAFE_ASSERT_CONTINUE(k < pData->param.count); + ctrlEvent.handled = true; value = pData->param.getFinalUnnormalizedValue(k, ctrlEvent.value); setParameterValueRT(k, value, true); continue; @@ -1986,17 +1989,17 @@ public: { if (MIDI_IS_CONTROL_BREATH_CONTROLLER(ctrlEvent.param) && (pData->hints & PLUGIN_CAN_DRYWET) > 0) { + ctrlEvent.handled = true; value = ctrlEvent.value; setDryWetRT(value, true); } - - if (MIDI_IS_CONTROL_CHANNEL_VOLUME(ctrlEvent.param) && (pData->hints & PLUGIN_CAN_VOLUME) > 0) + else if (MIDI_IS_CONTROL_CHANNEL_VOLUME(ctrlEvent.param) && (pData->hints & PLUGIN_CAN_VOLUME) > 0) { + ctrlEvent.handled = true; value = ctrlEvent.value*127.0f/100.0f; setVolumeRT(value, true); } - - if (MIDI_IS_CONTROL_BALANCE(ctrlEvent.param) && (pData->hints & PLUGIN_CAN_BALANCE) > 0) + else if (MIDI_IS_CONTROL_BALANCE(ctrlEvent.param) && (pData->hints & PLUGIN_CAN_BALANCE) > 0) { float left, right; value = ctrlEvent.value/0.5f - 1.0f; @@ -2017,6 +2020,7 @@ public: right = 1.0f; } + ctrlEvent.handled = true; setBalanceLeftRT(left, true); setBalanceRightRT(right, true); } @@ -2034,6 +2038,7 @@ public: if ((pData->param.data[k].hints & PARAMETER_IS_AUTOMABLE) == 0) continue; + ctrlEvent.handled = true; value = pData->param.getFinalUnnormalizedValue(k, ctrlEvent.value); setParameterValueRT(k, value, true); } @@ -2053,6 +2058,9 @@ public: nativeEvent.size = 3; } + if (! ctrlEvent.handled) + checkForMidiLearn(event); + break; } // case kEngineControlEventTypeParameter diff --git a/source/backend/plugin/CarlaPluginVST2.cpp b/source/backend/plugin/CarlaPluginVST2.cpp index c921a019c..5cfd654a8 100644 --- a/source/backend/plugin/CarlaPluginVST2.cpp +++ b/source/backend/plugin/CarlaPluginVST2.cpp @@ -1302,7 +1302,7 @@ public: for (uint32_t i=0, numEvents = pData->event.portIn->getEventCount(); i < numEvents; ++i) { - const EngineEvent& event(pData->event.portIn->getEvent(i)); + EngineEvent& event(pData->event.portIn->getEvent(i)); uint32_t eventTime = event.time; CARLA_SAFE_ASSERT_UINT2_CONTINUE(eventTime < frames, eventTime, frames); @@ -1337,7 +1337,7 @@ public: break; case kEngineEventTypeControl: { - const EngineControlEvent& ctrlEvent(event.ctrl); + EngineControlEvent& ctrlEvent(event.ctrl); switch (ctrlEvent.type) { @@ -1354,6 +1354,7 @@ public: const uint32_t k = ctrlEvent.param; CARLA_SAFE_ASSERT_CONTINUE(k < pData->param.count); + ctrlEvent.handled = true; value = pData->param.getFinalUnnormalizedValue(k, ctrlEvent.value); setParameterValueRT(k, value, true); continue; @@ -1364,17 +1365,17 @@ public: { if (MIDI_IS_CONTROL_BREATH_CONTROLLER(ctrlEvent.param) && (pData->hints & PLUGIN_CAN_DRYWET) != 0) { + ctrlEvent.handled = true; value = ctrlEvent.value; setDryWetRT(value, true); } - - if (MIDI_IS_CONTROL_CHANNEL_VOLUME(ctrlEvent.param) && (pData->hints & PLUGIN_CAN_VOLUME) != 0) + else if (MIDI_IS_CONTROL_CHANNEL_VOLUME(ctrlEvent.param) && (pData->hints & PLUGIN_CAN_VOLUME) != 0) { + ctrlEvent.handled = true; value = ctrlEvent.value*127.0f/100.0f; setVolumeRT(value, true); } - - if (MIDI_IS_CONTROL_BALANCE(ctrlEvent.param) && (pData->hints & PLUGIN_CAN_BALANCE) != 0) + else if (MIDI_IS_CONTROL_BALANCE(ctrlEvent.param) && (pData->hints & PLUGIN_CAN_BALANCE) != 0) { float left, right; value = ctrlEvent.value/0.5f - 1.0f; @@ -1395,6 +1396,7 @@ public: right = 1.0f; } + ctrlEvent.handled = true; setBalanceLeftRT(left, true); setBalanceRightRT(right, true); } @@ -1413,6 +1415,7 @@ public: if ((pData->param.data[k].hints & PARAMETER_IS_AUTOMABLE) == 0) continue; + ctrlEvent.handled = true; value = pData->param.getFinalUnnormalizedValue(k, ctrlEvent.value); setParameterValueRT(k, value, true); } @@ -1433,6 +1436,9 @@ public: vstMidiEvent.midiData[2] = char(ctrlEvent.value*127.0f); } + if (! ctrlEvent.handled) + checkForMidiLearn(event); + break; } // case kEngineControlEventTypeParameter diff --git a/source/frontend/carla_backend.py b/source/frontend/carla_backend.py index 565983135..b09c14649 100644 --- a/source/frontend/carla_backend.py +++ b/source/frontend/carla_backend.py @@ -562,13 +562,19 @@ PARAMETER_MAX = -9 # @see ParameterData::mappedControlIndex # Unused control index, meaning no mapping is enabled. -CONTROL_VALUE_NONE = -1 +CONTROL_INDEX_NONE = -1 # CV control index, meaning the parameter is exposed as CV port. -CONTROL_VALUE_CV = 130 +CONTROL_INDEX_CV = 130 # Special value to indicate MIDI pitchbend. -CONTROL_VALUE_MIDI_PITCHBEND = 131 +CONTROL_INDEX_MIDI_PITCHBEND = 131 + +# Special value to indicate MIDI learn. +CONTROL_INDEX_MIDI_LEARN = 132 + +# Special value to indicate MIDI pitchbend. +CONTROL_INDEX_MAX_ALLOWED = CONTROL_INDEX_MIDI_LEARN # --------------------------------------------------------------------------------------------------------------------- # Engine Callback Opcode @@ -1227,7 +1233,7 @@ PyParameterData = { 'index': PARAMETER_NULL, 'rindex': -1, 'midiChannel': 0, - 'mappedControlIndex': CONTROL_VALUE_NONE, + 'mappedControlIndex': CONTROL_INDEX_NONE, 'mappedMinimum': 0.0, 'mappedMaximum': 0.0, } diff --git a/source/frontend/carla_widgets.py b/source/frontend/carla_widgets.py index 992471980..5951d230f 100755 --- a/source/frontend/carla_widgets.py +++ b/source/frontend/carla_widgets.py @@ -73,9 +73,10 @@ from carla_backend import ( PARAMETER_USES_CUSTOM_TEXT, PARAMETER_CAN_BE_CV_CONTROLLED, PARAMETER_INPUT, PARAMETER_OUTPUT, - CONTROL_VALUE_NONE, - CONTROL_VALUE_MIDI_PITCHBEND, - CONTROL_VALUE_CV + CONTROL_INDEX_NONE, + CONTROL_INDEX_MIDI_PITCHBEND, + CONTROL_INDEX_MIDI_LEARN, + CONTROL_INDEX_CV ) from carla_shared import ( @@ -390,10 +391,14 @@ class PluginParameter(QWidget): self.ui.l_name.setFixedWidth(width) def updateStatusLabel(self): - if self.fMappedCtrl == CONTROL_VALUE_NONE: + if self.fMappedCtrl == CONTROL_INDEX_NONE: text = self.tr("Unmapped") - elif self.fMappedCtrl == CONTROL_VALUE_CV: + elif self.fMappedCtrl == CONTROL_INDEX_CV: text = self.tr("CV export") + elif self.fMappedCtrl == CONTROL_INDEX_MIDI_PITCHBEND: + text = self.tr("PBend Ch%i" % (self.fMidiChannel,)) + elif self.fMappedCtrl == CONTROL_INDEX_MIDI_LEARN: + text = self.tr("MIDI Learn") else: text = self.tr("CC%i Ch%i" % (self.fMappedCtrl, self.fMidiChannel)) @@ -403,14 +408,18 @@ class PluginParameter(QWidget): def slot_optionsCustomMenu(self): menu = QMenu(self) - if self.fMappedCtrl == CONTROL_VALUE_NONE: + if self.fMappedCtrl == CONTROL_INDEX_NONE: title = self.tr("Unmapped") - elif self.fMappedCtrl == CONTROL_VALUE_CV: + elif self.fMappedCtrl == CONTROL_INDEX_CV: title = self.tr("Exposed as CV port") + elif self.fMappedCtrl == CONTROL_INDEX_MIDI_PITCHBEND: + title = self.tr("Mapped to MIDI Pitchbend, channel %i" % (self.fMidiChannel,)) + elif self.fMappedCtrl == CONTROL_INDEX_MIDI_LEARN: + title = self.tr("MIDI Learn active") else: title = self.tr("Mapped to MIDI control %i, channel %i" % (self.fMappedCtrl, self.fMidiChannel)) - if self.fMappedCtrl != CONTROL_VALUE_NONE: + if self.fMappedCtrl != CONTROL_INDEX_NONE: title += " (range: %g-%g)" % (self.fMappedMinimum, self.fMappedMaximum) actTitle = menu.addAction(title) @@ -420,14 +429,14 @@ class PluginParameter(QWidget): actUnmap = menu.addAction(self.tr("Unmap")) - if self.fMappedCtrl == CONTROL_VALUE_NONE: + if self.fMappedCtrl == CONTROL_INDEX_NONE: actUnmap.setCheckable(True) actUnmap.setChecked(True) if self.fCanBeInCV: menu.addSection("CV") actCV = menu.addAction(self.tr("Expose as CV port")) - if self.fMappedCtrl == CONTROL_VALUE_CV: + if self.fMappedCtrl == CONTROL_INDEX_CV: actCV.setCheckable(True) actCV.setChecked(True) else: @@ -435,9 +444,18 @@ class PluginParameter(QWidget): menu.addSection("MIDI") + actLearn = menu.addAction(self.tr("MIDI Learn")) + + if self.fMappedCtrl == CONTROL_INDEX_MIDI_LEARN: + actLearn.setCheckable(True) + actLearn.setChecked(True) + menuMIDI = menu.addMenu(self.tr("MIDI Control")) - if self.fMappedCtrl not in (CONTROL_VALUE_NONE, CONTROL_VALUE_CV, CONTROL_VALUE_MIDI_PITCHBEND): + if self.fMappedCtrl not in (CONTROL_INDEX_NONE, + CONTROL_INDEX_CV, + CONTROL_INDEX_MIDI_PITCHBEND, + CONTROL_INDEX_MIDI_LEARN): action = menuMIDI.menuAction() action.setCheckable(True) action.setChecked(True) @@ -475,7 +493,7 @@ class PluginParameter(QWidget): # TODO #actPitchbend = menu.addAction(self.tr("MIDI Pitchbend")) - #if self.fMappedCtrl == CONTROL_VALUE_MIDI_PITCHBEND: + #if self.fMappedCtrl == CONTROL_INDEX_MIDI_PITCHBEND: #actPitchbend.setCheckable(True) #actPitchbend.setChecked(True) @@ -490,8 +508,8 @@ class PluginParameter(QWidget): action.setCheckable(True) action.setChecked(True) - if self.fMappedCtrl != CONTROL_VALUE_NONE: - if self.fMappedCtrl == CONTROL_VALUE_CV: + if self.fMappedCtrl != CONTROL_INDEX_NONE: + if self.fMappedCtrl == CONTROL_INDEX_CV: menu.addSection("Range (Scaled CV input)") else: menu.addSection("Range (MIDI bounds)") @@ -517,8 +535,8 @@ class PluginParameter(QWidget): self.tr("Custom Minimum"), "Custom minimum value to use:", self.fMappedMinimum, - self.fMinimum if self.fMappedCtrl != CONTROL_VALUE_CV else -9e6, - self.fMaximum if self.fMappedCtrl != CONTROL_VALUE_CV else 9e6, + self.fMinimum if self.fMappedCtrl != CONTROL_INDEX_CV else -9e6, + self.fMaximum if self.fMappedCtrl != CONTROL_INDEX_CV else 9e6, self.fDecimalPoints) if not ok: return @@ -532,8 +550,8 @@ class PluginParameter(QWidget): self.tr("Custom Maximum"), "Custom maximum value to use:", self.fMappedMaximum, - self.fMinimum if self.fMappedCtrl != CONTROL_VALUE_CV else -9e6, - self.fMaximum if self.fMappedCtrl != CONTROL_VALUE_CV else 9e6, + self.fMinimum if self.fMappedCtrl != CONTROL_INDEX_CV else -9e6, + self.fMaximum if self.fMappedCtrl != CONTROL_INDEX_CV else 9e6, self.fDecimalPoints) if not ok: return @@ -543,11 +561,9 @@ class PluginParameter(QWidget): return if actSel == actUnmap: - ctrl = CONTROL_VALUE_NONE + ctrl = CONTROL_INDEX_NONE elif actSel == actCV: - ctrl = CONTROL_VALUE_CV - elif actSel in actCCs: - ctrl = int(actSel.text().split(" ", 1)[0].replace("&",""), 10) + ctrl = CONTROL_INDEX_CV elif actSel == actCustomCC: value = self.fMappedCtrl if self.fMappedCtrl >= 0x01 and self.fMappedCtrl <= 0x77 else 1 ctrl, ok = QInputDialog.getInt(self, @@ -557,8 +573,12 @@ class PluginParameter(QWidget): 0x01, 0x77, 1) if not ok: return - #elif actSel in actPitchbend: - #ctrl = CONTROL_VALUE_MIDI_PITCHBEND + #elif actSel == actPitchbend: + #ctrl = CONTROL_INDEX_MIDI_PITCHBEND + elif actSel == actLearn: + ctrl = CONTROL_INDEX_MIDI_LEARN + elif actSel in actCCs: + ctrl = int(actSel.text().split(" ", 1)[0].replace("&",""), 10) else: return diff --git a/source/utils/CarlaStateUtils.cpp b/source/utils/CarlaStateUtils.cpp index e0ad2c239..34e00fe81 100644 --- a/source/utils/CarlaStateUtils.cpp +++ b/source/utils/CarlaStateUtils.cpp @@ -1,6 +1,6 @@ /* * Carla State utils - * Copyright (C) 2012-2018 Filipe Coelho + * Copyright (C) 2012-2020 Filipe Coelho * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -455,7 +455,8 @@ bool CarlaStateSave::fillFromXmlElement(const XmlElement* const xmlElement) { const int ctrl(pText.getIntValue()); if (ctrl > CONTROL_INDEX_NONE && ctrl <= CONTROL_INDEX_MAX_ALLOWED) - stateParameter->mappedControlIndex = static_cast(ctrl); + if (ctrl != CONTROL_INDEX_MIDI_LEARN) + stateParameter->mappedControlIndex = static_cast(ctrl); } else if (pTag == "MappedMinimum") {