From 530bbc07c50e3a1ab22eb7dcb4965b2af80d02df Mon Sep 17 00:00:00 2001 From: WarmUpTill Date: Wed, 12 Apr 2023 16:18:00 +0200 Subject: [PATCH] Add MIDI condition and action This new action will allow you to send MIDI messages to the selected device. The condition type will allow you to perform actions based on the MIDI messages received from the selected device. --- .gitmodules | 3 + data/locale/en-US.ini | 45 +- data/res/images/DarkNote.svg | 24 + data/res/images/LightNote.svg | 24 + deps/libremidi | 1 + src/macro-external/CMakeLists.txt | 1 + src/macro-external/midi/CMakeLists.txt | 25 + src/macro-external/midi/macro-action-midi.cpp | 206 ++++ src/macro-external/midi/macro-action-midi.hpp | 75 ++ .../midi/macro-condition-midi.cpp | 196 ++++ .../midi/macro-condition-midi.hpp | 71 ++ src/macro-external/midi/midi-helpers.cpp | 930 ++++++++++++++++++ src/macro-external/midi/midi-helpers.hpp | 172 ++++ 13 files changed, 1772 insertions(+), 1 deletion(-) create mode 100644 data/res/images/DarkNote.svg create mode 100644 data/res/images/LightNote.svg create mode 160000 deps/libremidi create mode 100644 src/macro-external/midi/CMakeLists.txt create mode 100644 src/macro-external/midi/macro-action-midi.cpp create mode 100644 src/macro-external/midi/macro-action-midi.hpp create mode 100644 src/macro-external/midi/macro-condition-midi.cpp create mode 100644 src/macro-external/midi/macro-condition-midi.hpp create mode 100644 src/macro-external/midi/midi-helpers.cpp create mode 100644 src/macro-external/midi/midi-helpers.hpp diff --git a/.gitmodules b/.gitmodules index c7af8e2f..5587bd77 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,3 +16,6 @@ [submodule "deps/tesseract"] path = deps/tesseract url = https://github.com/tesseract-ocr/tesseract.git +[submodule "deps/libremidi"] + path = deps/libremidi + url = https://github.com/jcelerier/libremidi.git diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index 3d99d039..33a8140e 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -431,6 +431,9 @@ AdvSceneSwitcher.condition.variable.entry="{{variables}}{{conditions}}{{strValue AdvSceneSwitcher.condition.run="Run" AdvSceneSwitcher.condition.run.entry="Process exits before timeout of{{timeout}} seconds" AdvSceneSwitcher.condition.run.entry.exit="{{checkExitCode}}Check for exit code{{exitCode}}" +AdvSceneSwitcher.condition.midi="MIDI" +AdvSceneSwitcher.condition.midi.entry="Mesasge was received from {{device}} which matches:" +AdvSceneSwitcher.condition.midi.entry.listen="Set MIDI message selection to messages incoming on selected device: {{listenButton}}" ; Macro Actions AdvSceneSwitcher.action.switchScene="Switch scene" @@ -650,7 +653,9 @@ AdvSceneSwitcher.action.projector.windowed="Windowed" AdvSceneSwitcher.action.projector.fullscreen="Fullscreen" AdvSceneSwitcher.action.projector.entry="Open{{windowTypes}}projector of{{types}}{{scenes}}{{sources}}" AdvSceneSwitcher.action.projector.entry.monitor="on{{monitors}}" - +AdvSceneSwitcher.action.midi="MIDI" +AdvSceneSwitcher.action.midi.entry="Send message to {{device}}:" +AdvSceneSwitcher.action.midi.entry.listen="Set MIDI message selection to messages incoming on {{listenDevices}}: {{listenButton}}" ; Transition Tab AdvSceneSwitcher.transitionTab.title="Transition" @@ -952,6 +957,44 @@ AdvSceneSwitcher.process.entry.workingDirectory="Working directory:{{workingDire AdvSceneSwitcher.math.expressionFail="Failed evaluate expression" +AdvSceneSwitcher.midi.deviceNamePattern="[Port #%1] %2" +AdvSceneSwitcher.midi.message.type="Type:" +AdvSceneSwitcher.midi.message.type.optional="Any" +AdvSceneSwitcher.midi.message.type.invalid="Invalid" +AdvSceneSwitcher.midi.message.type.noteOff="Note Off" +AdvSceneSwitcher.midi.message.type.noteOn="Note On" +AdvSceneSwitcher.midi.message.type.polyphonicPressure="Polyphonic Pressure" +AdvSceneSwitcher.midi.message.type.controlChange="Control Change" +AdvSceneSwitcher.midi.message.type.programChange="Program Change" +AdvSceneSwitcher.midi.message.type.channelAftertouch="Channel Aftertouch" +AdvSceneSwitcher.midi.message.type.pitchBend="Pitch Bend" +AdvSceneSwitcher.midi.message.type.systemExclusive="System Exclusive" +AdvSceneSwitcher.midi.message.type.timeCode="Time Code" +AdvSceneSwitcher.midi.message.type.songPositionPointer="Song Position Pointer" +AdvSceneSwitcher.midi.message.type.songSelect="Song Select" +AdvSceneSwitcher.midi.message.type.reserved1="Reserved (1)" +AdvSceneSwitcher.midi.message.type.reserved2="Reserved (2)" +AdvSceneSwitcher.midi.message.type.tuneRequest="Tune Request" +AdvSceneSwitcher.midi.message.type.endOfSystemExclusive="End of System Exclusive" +AdvSceneSwitcher.midi.message.type.timeClock="Time Clock" +AdvSceneSwitcher.midi.message.type.reserved3="Reserved (3)" +AdvSceneSwitcher.midi.message.type.startFile="Start File" +AdvSceneSwitcher.midi.message.type.continueFile="Continue File" +AdvSceneSwitcher.midi.message.type.stopFile="Stop File" +AdvSceneSwitcher.midi.message.type.reserved4="Reserved (4)" +AdvSceneSwitcher.midi.message.type.activeSensing="Active Sensing" +AdvSceneSwitcher.midi.message.type.systemReset="System Reset" +AdvSceneSwitcher.midi.message.type.unknown="Unknown" +AdvSceneSwitcher.midi.message.channel="Channel:" +AdvSceneSwitcher.midi.message.note="Note / Value(1):" +AdvSceneSwitcher.midi.message.value="Value(2):" +AdvSceneSwitcher.midi.message.placeholder="Any" +AdvSceneSwitcher.midi.resetDevices="Reconnect all MIDI devices" +AdvSceneSwitcher.midi.startListen="Start listening" +AdvSceneSwitcher.midi.stopListen="Stop listening" +AdvSceneSwitcher.midi.startListenFail="Device is busy!\nSomething else is already listening!" +AdvSceneSwitcher.midi.deviceOpenFail="Failed to initialize MIDI device!" + AdvSceneSwitcher.selectScene="--select scene--" AdvSceneSwitcher.selectPreviousScene="Previous Scene" AdvSceneSwitcher.selectCurrentScene="Current Scene" diff --git a/data/res/images/DarkNote.svg b/data/res/images/DarkNote.svg new file mode 100644 index 00000000..55db9289 --- /dev/null +++ b/data/res/images/DarkNote.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + Layer 1 + + + + + + + + + + + \ No newline at end of file diff --git a/data/res/images/LightNote.svg b/data/res/images/LightNote.svg new file mode 100644 index 00000000..aac0447b --- /dev/null +++ b/data/res/images/LightNote.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + Layer 1 + + + + + + + + + + + \ No newline at end of file diff --git a/deps/libremidi b/deps/libremidi new file mode 160000 index 00000000..56147984 --- /dev/null +++ b/deps/libremidi @@ -0,0 +1 @@ +Subproject commit 56147984eea2bd4aeeb5fb51b677f058d29daef8 diff --git a/src/macro-external/CMakeLists.txt b/src/macro-external/CMakeLists.txt index e1a6fa7c..0b04dd5f 100644 --- a/src/macro-external/CMakeLists.txt +++ b/src/macro-external/CMakeLists.txt @@ -17,5 +17,6 @@ install_advss_plugin_dependency(...) ... to install the plugin and its dependencies. #]] +add_subdirectory(midi) add_subdirectory(openvr) add_subdirectory(video) diff --git a/src/macro-external/midi/CMakeLists.txt b/src/macro-external/midi/CMakeLists.txt new file mode 100644 index 00000000..bc18360d --- /dev/null +++ b/src/macro-external/midi/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.14) +project(advanced-scene-switcher-midi) + +# --- Check libremidi requirements --- + +get_target_property(ADVSS_SOURCE_DIR advanced-scene-switcher-lib SOURCE_DIR) +add_subdirectory("${ADVSS_SOURCE_DIR}/deps/libremidi" + "${ADVSS_SOURCE_DIR}/deps/libremidi/build") + +# --- End of section --- + +add_library(${PROJECT_NAME} MODULE) + +target_sources( + ${PROJECT_NAME} + PRIVATE macro-condition-midi.cpp macro-condition-midi.hpp + macro-action-midi.cpp macro-action-midi.hpp midi-helpers.cpp + midi-helpers.hpp) + +setup_advss_plugin(${PROJECT_NAME}) +set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "") +target_include_directories(${PROJECT_NAME} + PRIVATE "${ADVSS_SOURCE_DIR}/deps/libremidi/include") +target_link_libraries(${PROJECT_NAME} PRIVATE libremidi) +install_advss_plugin(${PROJECT_NAME}) diff --git a/src/macro-external/midi/macro-action-midi.cpp b/src/macro-external/midi/macro-action-midi.cpp new file mode 100644 index 00000000..7910daac --- /dev/null +++ b/src/macro-external/midi/macro-action-midi.cpp @@ -0,0 +1,206 @@ +#include "macro-action-midi.hpp" +#include "utility.hpp" + +namespace advss { + +const std::string MacroActionMidi::id = "midi"; + +bool MacroActionMidi::_registered = MacroActionFactory::Register( + MacroActionMidi::id, + {MacroActionMidi::Create, MacroActionMidiEdit::Create, + "AdvSceneSwitcher.action.midi"}); + +bool MacroActionMidi::PerformAction() +{ + if (!_device.SendMessge(_message)) { + blog(LOG_WARNING, + "failed to send midi message \"%s\" to \"%s\"", + _message.ToString().c_str(), _device.Name().c_str()); + } + return true; +} + +void MacroActionMidi::LogAction() const +{ + vblog(LOG_INFO, "send midi message \"%s\" to \"%s\"", + _message.ToString().c_str(), _device.Name().c_str()); +} + +bool MacroActionMidi::Save(obs_data_t *obj) const +{ + MacroAction::Save(obj); + _message.Save(obj); + _device.Save(obj); + return true; +} + +bool MacroActionMidi::Load(obs_data_t *obj) +{ + MacroAction::Load(obj); + _message.Load(obj); + _device.Load(obj); + return true; +} + +std::string MacroActionMidi::GetShortDesc() const +{ + return _device.Name(); +} + +MacroActionMidiEdit::MacroActionMidiEdit( + QWidget *parent, std::shared_ptr entryData) + : QWidget(parent), + _devices(new MidiDeviceSelection(this, MidiDeviceType::OUTPUT)), + _message(new MidiMessageSelection(this)), + _resetMidiDevices(new QPushButton( + obs_module_text("AdvSceneSwitcher.midi.resetDevices"))), + _listenDevices(new MidiDeviceSelection(this, MidiDeviceType::INPUT)), + _listen(new QPushButton( + obs_module_text("AdvSceneSwitcher.midi.startListen"))) +{ + QWidget::connect(_devices, + SIGNAL(DeviceSelectionChanged(const MidiDevice &)), + this, + SLOT(DeviceSelectionChanged(const MidiDevice &))); + QWidget::connect(_message, + SIGNAL(MidiMessageChanged(const MidiMessage &)), this, + SLOT(MidiMessageChanged(const MidiMessage &))); + QWidget::connect(_resetMidiDevices, SIGNAL(clicked()), this, + SLOT(ResetMidiDevices())); + QWidget::connect(_listen, SIGNAL(clicked()), this, + SLOT(ToggleListen())); + QWidget::connect( + _listenDevices, + SIGNAL(DeviceSelectionChanged(const MidiDevice &)), this, + SLOT(ListenDeviceSelectionChanged(const MidiDevice &))); + QWidget::connect(&_listenTimer, SIGNAL(timeout()), this, + SLOT(SetMessageSelectionToLastReceived())); + + auto entryLayout = new QHBoxLayout; + PlaceWidgets(obs_module_text("AdvSceneSwitcher.action.midi.entry"), + entryLayout, {{"{{device}}", _devices}}); + auto listenLayout = new QHBoxLayout; + PlaceWidgets( + obs_module_text("AdvSceneSwitcher.action.midi.entry.listen"), + listenLayout, + {{"{{listenButton}}", _listen}, + {"{{listenDevices}}", _listenDevices}}); + + auto mainLayout = new QVBoxLayout; + mainLayout->addLayout(entryLayout); + mainLayout->addWidget(_message); + mainLayout->addLayout(listenLayout); + mainLayout->addWidget(_resetMidiDevices); + setLayout(mainLayout); + + _listenTimer.setInterval(100); + + _entryData = entryData; + UpdateEntryData(); + _loading = false; +} + +MacroActionMidiEdit::~MacroActionMidiEdit() +{ + EnableListening(false); +} + +void MacroActionMidiEdit::UpdateEntryData() +{ + if (!_entryData) { + return; + } + + _message->SetMessage(_entryData->_message); + _devices->SetDevice(_entryData->_device); + + adjustSize(); + updateGeometry(); +} + +void MacroActionMidiEdit::DeviceSelectionChanged(const MidiDevice &device) +{ + if (_loading || !_entryData) { + return; + } + + { + auto lock = LockContext(); + _entryData->_device = device; + } + emit HeaderInfoChanged( + QString::fromStdString(_entryData->GetShortDesc())); +} + +void MacroActionMidiEdit::ListenDeviceSelectionChanged(const MidiDevice &dev) +{ + if (_currentlyListening) { + ToggleListen(); + } + _listenDevice = dev; +} + +void MacroActionMidiEdit::MidiMessageChanged(const MidiMessage &message) +{ + if (_loading || !_entryData) { + return; + } + + auto lock = LockContext(); + _entryData->_message = message; +} + +void MacroActionMidiEdit::EnableListening(bool enable) +{ + if (_currentlyListening == enable) { + return; + } + _listenDevice.UseForMessageSelection(enable); + if (enable) { + _listenTimer.start(); + } else { + _listenTimer.stop(); + } +} + +void MacroActionMidiEdit::ResetMidiDevices() +{ + auto lock = LockContext(); + MidiDeviceInstance::ResetAllDevices(); +} + +void MacroActionMidiEdit::ToggleListen() +{ + if (!_entryData || !_listenDevice.DeviceSelected()) { + return; + } + + if (!_currentlyListening && _listenDevice.IsUsedForMessageSelection()) { + DisplayMessage(obs_module_text( + "AdvSceneSwitcher.midi.startListenFail")); + return; + } + + _listen->setText( + _currentlyListening + ? obs_module_text("AdvSceneSwitcher.midi.startListen") + : obs_module_text("AdvSceneSwitcher.midi.stopListen")); + EnableListening(!_currentlyListening); + _currentlyListening = !_currentlyListening; + _message->setDisabled(_currentlyListening); +} + +void MacroActionMidiEdit::SetMessageSelectionToLastReceived() +{ + auto lock = LockContext(); + auto messages = _listenDevice.GetMessages(true); + if (!_entryData || !messages || messages->empty()) { + return; + } + + _message->SetMessage(messages->back()); + _entryData->_message = messages->back(); + _listenDevice.ClearMessageBuffer(); +} + +} // namespace advss diff --git a/src/macro-external/midi/macro-action-midi.hpp b/src/macro-external/midi/macro-action-midi.hpp new file mode 100644 index 00000000..65359fce --- /dev/null +++ b/src/macro-external/midi/macro-action-midi.hpp @@ -0,0 +1,75 @@ +#pragma once +#include "macro-action-edit.hpp" +#include "midi-helpers.hpp" + +#include +#include + +namespace advss { + +class MacroActionMidi : public MacroAction { +public: + MacroActionMidi(Macro *m) : MacroAction(m, true) {} + bool PerformAction(); + void LogAction() const; + bool Save(obs_data_t *obj) const; + bool Load(obs_data_t *obj); + std::string GetShortDesc() const; + std::string GetId() const { return id; }; + static std::shared_ptr Create(Macro *m) + { + return std::make_shared(m); + } + + MidiDevice _device; + MidiMessage _message; + +private: + static bool _registered; + static const std::string id; +}; + +class MacroActionMidiEdit : public QWidget { + Q_OBJECT + +public: + MacroActionMidiEdit( + QWidget *parent, + std::shared_ptr entryData = nullptr); + virtual ~MacroActionMidiEdit(); + void UpdateEntryData(); + static QWidget *Create(QWidget *parent, + std::shared_ptr action) + { + return new MacroActionMidiEdit( + parent, + std::dynamic_pointer_cast(action)); + } + +private slots: + void DeviceSelectionChanged(const MidiDevice &); + void ListenDeviceSelectionChanged(const MidiDevice &); + void MidiMessageChanged(const MidiMessage &); + void ResetMidiDevices(); + void ToggleListen(); + void SetMessageSelectionToLastReceived(); +signals: + void HeaderInfoChanged(const QString &); + +private: + void EnableListening(bool); + + std::shared_ptr _entryData; + + MidiDeviceSelection *_devices; + MidiMessageSelection *_message; + MidiDeviceSelection *_listenDevices; + QPushButton *_resetMidiDevices; + QPushButton *_listen; + MidiDevice _listenDevice; + QTimer _listenTimer; + bool _currentlyListening = false; + bool _loading = true; +}; + +} // namespace advss diff --git a/src/macro-external/midi/macro-condition-midi.cpp b/src/macro-external/midi/macro-condition-midi.cpp new file mode 100644 index 00000000..c2e4aed0 --- /dev/null +++ b/src/macro-external/midi/macro-condition-midi.cpp @@ -0,0 +1,196 @@ +#include "macro-condition-midi.hpp" +#include "utility.hpp" + +namespace advss { + +const std::string MacroConditionMidi::id = "midi"; + +bool MacroConditionMidi::_registered = MacroConditionFactory::Register( + MacroConditionMidi::id, + {MacroConditionMidi::Create, MacroConditionMidiEdit::Create, + "AdvSceneSwitcher.condition.midi"}); + +bool MacroConditionMidi::CheckCondition() +{ + auto messages = _device.GetMessages(); + if (!messages) { + return false; + } + + for (const auto &m : *messages) { + if (m.Matches(_message)) { + return true; + } + } + + return false; +} + +bool MacroConditionMidi::Save(obs_data_t *obj) const +{ + MacroCondition::Save(obj); + _message.Save(obj); + _device.Save(obj); + return true; +} + +bool MacroConditionMidi::Load(obs_data_t *obj) +{ + MacroCondition::Load(obj); + _message.Load(obj); + _device.Load(obj); + return true; +} + +std::string MacroConditionMidi::GetShortDesc() const +{ + return _device.Name(); +} + +MacroConditionMidiEdit::MacroConditionMidiEdit( + QWidget *parent, std::shared_ptr entryData) + : QWidget(parent), + _devices(new MidiDeviceSelection(this, MidiDeviceType::INPUT)), + _message(new MidiMessageSelection(this)), + _resetMidiDevices(new QPushButton( + obs_module_text("AdvSceneSwitcher.midi.resetDevices"))), + _listen(new QPushButton( + obs_module_text("AdvSceneSwitcher.midi.startListen"))) +{ + QWidget::connect(_devices, + SIGNAL(DeviceSelectionChanged(const MidiDevice &)), + this, + SLOT(DeviceSelectionChanged(const MidiDevice &))); + QWidget::connect(_message, + SIGNAL(MidiMessageChanged(const MidiMessage &)), this, + SLOT(MidiMessageChanged(const MidiMessage &))); + QWidget::connect(_resetMidiDevices, SIGNAL(clicked()), this, + SLOT(ResetMidiDevices())); + QWidget::connect(_listen, SIGNAL(clicked()), this, + SLOT(ToggleListen())); + QWidget::connect(&_listenTimer, SIGNAL(timeout()), this, + SLOT(SetMessageSelectionToLastReceived())); + + auto entryLayout = new QHBoxLayout; + PlaceWidgets(obs_module_text("AdvSceneSwitcher.condition.midi.entry"), + entryLayout, {{"{{device}}", _devices}}); + auto listenLayout = new QHBoxLayout; + PlaceWidgets( + obs_module_text("AdvSceneSwitcher.condition.midi.entry.listen"), + listenLayout, {{"{{listenButton}}", _listen}}); + + auto mainLayout = new QVBoxLayout; + mainLayout->addLayout(entryLayout); + mainLayout->addWidget(_message); + mainLayout->addLayout(listenLayout); + mainLayout->addWidget(_resetMidiDevices); + setLayout(mainLayout); + + _listenTimer.setInterval(100); + + _entryData = entryData; + UpdateEntryData(); + _loading = false; +} + +MacroConditionMidiEdit::~MacroConditionMidiEdit() +{ + EnableListening(false); +} + +void MacroConditionMidiEdit::UpdateEntryData() +{ + if (!_entryData) { + return; + } + + _message->SetMessage(_entryData->_message); + _devices->SetDevice(_entryData->_device); + + adjustSize(); + updateGeometry(); +} + +void MacroConditionMidiEdit::DeviceSelectionChanged(const MidiDevice &device) +{ + if (_loading || !_entryData) { + return; + } + + if (_currentlyListening) { + ToggleListen(); + } + + { + auto lock = LockContext(); + _entryData->_device = device; + } + emit HeaderInfoChanged( + QString::fromStdString(_entryData->GetShortDesc())); +} + +void MacroConditionMidiEdit::MidiMessageChanged(const MidiMessage &message) +{ + if (_loading || !_entryData) { + return; + } + + auto lock = LockContext(); + _entryData->_message = message; +} + +void MacroConditionMidiEdit::ResetMidiDevices() +{ + auto lock = LockContext(); + MidiDeviceInstance::ResetAllDevices(); +} + +void MacroConditionMidiEdit::EnableListening(bool enable) +{ + if (_currentlyListening == enable) { + return; + } + _entryData->_device.UseForMessageSelection(enable); + if (enable) { + _listenTimer.start(); + } else { + _listenTimer.stop(); + } +} + +void MacroConditionMidiEdit::ToggleListen() +{ + if (!_entryData) { + return; + } + + if (!_currentlyListening && + _entryData->_device.IsUsedForMessageSelection()) { + DisplayMessage(obs_module_text( + "AdvSceneSwitcher.midi.startListenFail")); + return; + } + + _listen->setText( + _currentlyListening + ? obs_module_text("AdvSceneSwitcher.midi.startListen") + : obs_module_text("AdvSceneSwitcher.midi.stopListen")); + EnableListening(!_currentlyListening); + _currentlyListening = !_currentlyListening; + _message->setDisabled(_currentlyListening); +} + +void MacroConditionMidiEdit::SetMessageSelectionToLastReceived() +{ + auto lock = LockContext(); + auto messages = _entryData->_device.GetMessages(true); + if (!_entryData || !messages || messages->empty()) { + return; + } + + _message->SetMessage(messages->back()); + _entryData->_message = messages->back(); + _entryData->_device.ClearMessageBuffer(); +} + +} // namespace advss diff --git a/src/macro-external/midi/macro-condition-midi.hpp b/src/macro-external/midi/macro-condition-midi.hpp new file mode 100644 index 00000000..e916b4e8 --- /dev/null +++ b/src/macro-external/midi/macro-condition-midi.hpp @@ -0,0 +1,71 @@ +#pragma once +#include "macro-condition-edit.hpp" +#include "midi-helpers.hpp" + +#include +#include + +namespace advss { + +class MacroConditionMidi : public MacroCondition { +public: + MacroConditionMidi(Macro *m) : MacroCondition(m, true) {} + bool CheckCondition(); + bool Save(obs_data_t *obj) const; + bool Load(obs_data_t *obj); + std::string GetShortDesc() const; + std::string GetId() const { return id; }; + static std::shared_ptr Create(Macro *m) + { + return std::make_shared(m); + } + + MidiDevice _device; + MidiMessage _message; + +private: + static bool _registered; + static const std::string id; +}; + +class MacroConditionMidiEdit : public QWidget { + Q_OBJECT + +public: + MacroConditionMidiEdit( + QWidget *parent, + std::shared_ptr cond = nullptr); + virtual ~MacroConditionMidiEdit(); + void UpdateEntryData(); + static QWidget *Create(QWidget *parent, + std::shared_ptr cond) + { + return new MacroConditionMidiEdit( + parent, + std::dynamic_pointer_cast(cond)); + } + +private slots: + void DeviceSelectionChanged(const MidiDevice &); + void MidiMessageChanged(const MidiMessage &); + void ResetMidiDevices(); + void ToggleListen(); + void SetMessageSelectionToLastReceived(); +signals: + void HeaderInfoChanged(const QString &); + +private: + void EnableListening(bool); + + std::shared_ptr _entryData; + + MidiDeviceSelection *_devices; + MidiMessageSelection *_message; + QPushButton *_resetMidiDevices; + QPushButton *_listen; + QTimer _listenTimer; + bool _currentlyListening = false; + bool _loading = true; +}; + +} // namespace advss diff --git a/src/macro-external/midi/midi-helpers.cpp b/src/macro-external/midi/midi-helpers.cpp new file mode 100644 index 00000000..919b065d --- /dev/null +++ b/src/macro-external/midi/midi-helpers.cpp @@ -0,0 +1,930 @@ +#include "midi-helpers.hpp" + +#include +#include +#include + +namespace advss { + +static std::map, MidiDeviceInstance *> +SetupMidiMessageVector() +{ + GetSwitcher()->AddResetForNextIntervalFunction( + MidiDeviceInstance::ClearMessageBuffersOfAllDevices); + return {}; +} + +std::map, MidiDeviceInstance *> + MidiDeviceInstance::devices = SetupMidiMessageVector(); + +void MidiDeviceInstance::ClearMessageBuffersOfAllDevices() +{ + for (auto const &[_, device] : MidiDeviceInstance::devices) { + if (device->_skipBufferClear) { + continue; + } + device->ClearMessageBuffer(); + } +} + +void MidiDeviceInstance::ResetAllDevices() +{ + for (auto const &[_, device] : MidiDeviceInstance::devices) { + if (device->_skipBufferClear) { + continue; + } + device->ClosePort(); + device->ClearMessageBuffer(); + device->OpenPort(); + } +} + +void MidiDeviceInstance::ClearMessageBuffer() +{ + _messages.clear(); +} + +MidiMessage::MidiMessage(const libremidi::message &message) +{ + _typeIsOptional = false; + _type = message.get_message_type(); + _channel = message.get_channel(); + _note = GetMidiNote(message); + _value = GetMidiValue(message); +} + +void MidiMessage::Save(obs_data_t *obj) const +{ + auto data = obs_data_create(); + obs_data_set_bool(data, "typeIsOptional", _typeIsOptional); + obs_data_set_int(data, "type", static_cast(_type)); + _channel.Save(data, "channel"); + _note.Save(data, "note"); + _value.Save(data, "value"); + obs_data_set_obj(obj, "midiMessage", data); + obs_data_release(data); +} + +void MidiMessage::Load(obs_data_t *obj) +{ + auto data = obs_data_get_obj(obj, "midiMessage"); + _typeIsOptional = obs_data_get_bool(data, "typeIsOptional"); + _type = static_cast( + obs_data_get_int(data, "type")); + _channel.Load(data, "channel"); + _note.Load(data, "note"); + _value.Load(data, "value"); + obs_data_release(data); +} + +int MidiMessage::GetMidiNote(const libremidi::message &msg) +{ + switch (msg.get_message_type()) { + case libremidi::message_type::NOTE_OFF: + case libremidi::message_type::NOTE_ON: + case libremidi::message_type::CONTROL_CHANGE: + return msg[1]; + default: + return 0; + } +} + +int MidiMessage::GetMidiValue(const libremidi::message &msg) +{ + switch (msg.get_message_type()) { + case libremidi::message_type::NOTE_ON: + case libremidi::message_type::NOTE_OFF: + case libremidi::message_type::CONTROL_CHANGE: + case libremidi::message_type::PITCH_BEND: + return msg[2]; + case libremidi::message_type::PROGRAM_CHANGE: + return msg[1]; + default: + return -1; + } +} + +std::string MidiMessage::ToString() const +{ + return "Type: " + MidiTypeToString(_type) + + " Note: " + std::to_string(_note) + + " Channel: " + std::to_string(_channel) + + " Value: " + std::to_string(_value); +} + +std::string MidiMessage::ToString(const libremidi::message &msg) +{ + return "Type: " + GetMidiType(msg) + + " Note: " + std::to_string(GetMidiNote(msg)) + + " Channel: " + std::to_string(msg.get_channel()) + + " Value: " + std::to_string(GetMidiValue(msg)); +} + +std::string MidiMessage::MidiTypeToString(libremidi::message_type type) +{ + switch (type) { + case libremidi::message_type::INVALID: + return obs_module_text( + "AdvSceneSwitcher.midi.message.type.invalid"); + // Standard Messages + case libremidi::message_type::NOTE_OFF: + return obs_module_text( + "AdvSceneSwitcher.midi.message.type.noteOff"); + case libremidi::message_type::NOTE_ON: + return obs_module_text( + "AdvSceneSwitcher.midi.message.type.noteOn"); + case libremidi::message_type::POLY_PRESSURE: + return obs_module_text( + "AdvSceneSwitcher.midi.message.type.polyphonicPressure"); + case libremidi::message_type::CONTROL_CHANGE: + return obs_module_text( + "AdvSceneSwitcher.midi.message.type.controlChange"); + case libremidi::message_type::PROGRAM_CHANGE: + return obs_module_text( + "AdvSceneSwitcher.midi.message.type.programChange"); + case libremidi::message_type::AFTERTOUCH: + return obs_module_text( + "AdvSceneSwitcher.midi.message.type.channelAftertouch"); + case libremidi::message_type::PITCH_BEND: + return obs_module_text( + "AdvSceneSwitcher.midi.message.type.pitchBend"); + // System Common Messages + case libremidi::message_type::SYSTEM_EXCLUSIVE: + return obs_module_text( + "AdvSceneSwitcher.midi.message.type.systemExclusive"); + case libremidi::message_type::TIME_CODE: + return obs_module_text( + "AdvSceneSwitcher.midi.message.type.timeCode"); + case libremidi::message_type::SONG_POS_POINTER: + return obs_module_text( + "AdvSceneSwitcher.midi.message.type.songPositionPointer"); + case libremidi::message_type::SONG_SELECT: + return obs_module_text( + "AdvSceneSwitcher.midi.message.type.songSelect"); + case libremidi::message_type::RESERVED1: + return obs_module_text( + "AdvSceneSwitcher.midi.message.type.reserved1"); + case libremidi::message_type::RESERVED2: + return obs_module_text( + "AdvSceneSwitcher.midi.message.type.reserved2"); + case libremidi::message_type::TUNE_REQUEST: + return obs_module_text( + "AdvSceneSwitcher.midi.message.type.tuneRequest"); + case libremidi::message_type::EOX: + return obs_module_text( + "AdvSceneSwitcher.midi.message.type.endOfSystemExclusive"); + // System Realtime Messages + case libremidi::message_type::TIME_CLOCK: + return obs_module_text( + "AdvSceneSwitcher.midi.message.type.timeClock"); + case libremidi::message_type::RESERVED3: + return obs_module_text( + "AdvSceneSwitcher.midi.message.type.reserved3"); + case libremidi::message_type::START: + return obs_module_text( + "AdvSceneSwitcher.midi.message.type.startFile"); + case libremidi::message_type::CONTINUE: + return obs_module_text( + "AdvSceneSwitcher.midi.message.type.continueFile"); + case libremidi::message_type::STOP: + return obs_module_text( + "AdvSceneSwitcher.midi.message.type.stopFile"); + case libremidi::message_type::RESERVED4: + return obs_module_text( + "AdvSceneSwitcher.midi.message.type.reserved4"); + case libremidi::message_type::ACTIVE_SENSING: + return obs_module_text( + "AdvSceneSwitcher.midi.message.type.activeSensing"); + case libremidi::message_type::SYSTEM_RESET: + return obs_module_text( + "AdvSceneSwitcher.midi.message.type.systemReset"); + default: + return obs_module_text( + "AdvSceneSwitcher.midi.message.type.unknown"); + } + return ""; +} + +std::string MidiMessage::GetMidiType(const libremidi::message &msg) +{ + return MidiTypeToString(msg.get_message_type()); +} + +bool MidiMessage::Matches(const MidiMessage &m) const +{ + const bool channelMatch = _channel == optionalChannelIndicator || + m._channel == optionalChannelIndicator || + (_channel == m._channel); + const bool noteMatch = _note == optionalNoteIndicator || + m._note == optionalNoteIndicator || + (_note == m._note); + const bool valueMatch = _value == optionalValueIndicator || + m._value == optionalValueIndicator || + (_value == m._value); + const bool typeMatch = _typeIsOptional || m._typeIsOptional || + (_type == m._type); + return channelMatch && noteMatch && valueMatch && typeMatch; +} + +MidiDeviceInstance *MidiDeviceInstance::GetDevice(MidiDeviceType type, int port) +{ + if (port < 0) { + return nullptr; + } + + auto key = std::make_pair(type, port); + auto it = devices.find(key); + if (it != devices.end()) { + it->second->OpenPort(); + return it->second; + } + + try { + auto device = new MidiDeviceInstance(); + device->_type = type; + device->_port = port; + devices[key] = device; + device->OpenPort(); + return device; + } catch (const libremidi::driver_error &error) { + blog(LOG_WARNING, + "Failed to create midi %s device instance for port #%d: %s", + type == MidiDeviceType::INPUT ? "input" : "output", port, + error.what()); + } + return nullptr; +} + +void MidiDevice::Save(obs_data_t *obj) const +{ + auto data = obs_data_create(); + obs_data_set_int(data, "type", static_cast(_type)); + obs_data_set_int(data, "port", _dev ? _dev->_port : -1); + obs_data_set_obj(obj, "midiDevice", data); + obs_data_release(data); +} + +void MidiDevice::Load(obs_data_t *obj) +{ + auto data = obs_data_get_obj(obj, "midiDevice"); + _type = static_cast(obs_data_get_int(data, "type")); + obs_data_set_default_int(data, "port", -1); + _port = obs_data_get_int(data, "port"); + _dev = MidiDeviceInstance::GetDevice(_type, _port); + obs_data_release(data); +} + +bool MidiDevice::SendMessge(const MidiMessage &m) +{ + if (_type == MidiDeviceType::INPUT || _port == -1 || !_dev) { + return false; + } + + return _dev->SendMessge(m); +} + +bool MidiDeviceInstance::OpenPort() +{ + if ((_type == MidiDeviceType::INPUT && in.is_port_open()) || + (_type == MidiDeviceType::OUTPUT && out.is_port_open())) { + return true; + } + + if (_type == MidiDeviceType::OUTPUT) { + try { + out.open_port(_port); + blog(LOG_INFO, "Opened output midi port #%d", _port); + return true; + } catch (const libremidi::driver_error &error) { + blog(LOG_WARNING, + "Failed to open output midi port #%d: %s", _port, + error.what()); + } catch (const libremidi::system_error &error) { + blog(LOG_WARNING, + "Failed to open output midi port #%d: %s", _port, + error.what()); + } catch (const libremidi::midi_exception &error) { + blog(LOG_WARNING, + "Failed to open output midi port #%d: %s", _port, + error.what()); + } + return false; + } + + auto cb = [this](const libremidi::message &m) { + this->ReceiveMidiMessage(m); + }; + + in.set_callback(cb); + try { + in.open_port(_port); + blog(LOG_INFO, "Opened input midi port #%d", _port); + return true; + } catch (const libremidi::driver_error &error) { + blog(LOG_WARNING, "Failed to open input midi port #%d: %s", + _port, error.what()); + } catch (const libremidi::system_error &error) { + blog(LOG_WARNING, "Failed to open input midi port #%d: %s", + _port, error.what()); + } catch (const libremidi::midi_exception &error) { + blog(LOG_WARNING, "Failed to open input midi port #%d: %s", + _port, error.what()); + } + return false; +} + +void MidiDeviceInstance::ClosePort() +{ + if ((_type == MidiDeviceType::INPUT && !in.is_port_open()) || + (_type == MidiDeviceType::OUTPUT && !out.is_port_open())) { + return; + } + + if (_type == MidiDeviceType::OUTPUT) { + try { + out.close_port(); + blog(LOG_INFO, "Closed output midi port #%d", _port); + } catch (const libremidi::driver_error &error) { + blog(LOG_WARNING, + "Failed to close output midi port #%d: %s", _port, + error.what()); + } catch (const libremidi::system_error &error) { + blog(LOG_WARNING, + "Failed to close output midi port #%d: %s", _port, + error.what()); + } catch (const libremidi::midi_exception &error) { + blog(LOG_WARNING, + "Failed to close output midi port #%d: %s", _port, + error.what()); + } + return; + } + + auto cb = [this](const libremidi::message &m) { + this->ReceiveMidiMessage(m); + }; + + in.set_callback(cb); + try { + in.close_port(); + blog(LOG_INFO, "Closed input midi port #%d", _port); + } catch (const libremidi::driver_error &error) { + blog(LOG_WARNING, "Failed to close input midi port #%d: %s", + _port, error.what()); + } catch (const libremidi::system_error &error) { + blog(LOG_WARNING, "Failed to close input midi port #%d: %s", + _port, error.what()); + } catch (const libremidi::midi_exception &error) { + blog(LOG_WARNING, "Failed to close input midi port #%d: %s", + _port, error.what()); + } +} + +bool MidiDeviceInstance::SendMessge(const MidiMessage &m) +{ + libremidi::message message; + int channel = m.Channel(); + int note = m.Note(); + int value = m.Value(); + + switch (m.Type()) { + case libremidi::message_type::NOTE_OFF: + message = libremidi::message::note_off(channel, note, value); + break; + case libremidi::message_type::NOTE_ON: + message = libremidi::message::note_on(channel, note, value); + break; + case libremidi::message_type::CONTROL_CHANGE: + message = libremidi::message::control_change(channel, note, + value); + break; + case libremidi::message_type::PROGRAM_CHANGE: + message = libremidi::message::program_change(channel, value); + break; + case libremidi::message_type::PITCH_BEND: + message = libremidi::message::pitch_bend( + channel, (value <= 64 ? 0 : value - 64) * 2, value); + break; + case libremidi::message_type::POLY_PRESSURE: + message = + libremidi::message::poly_pressure(channel, note, value); + break; + case libremidi::message_type::AFTERTOUCH: + message = libremidi::message::aftertouch(channel, value); + break; + default: + message = {libremidi::message::make_command(m.Type(), channel), + (unsigned char)note, (unsigned char)value}; + blog(LOG_WARNING, + "sending midi message of non-default type \"%s\"", + MidiMessage::MidiTypeToString(m.Type()).c_str()); + break; + } + + try { + out.send_message(message); + return true; + } catch (const libremidi::driver_error &err) { + blog(LOG_WARNING, "%s", err.what()); + } + + return false; +} + +const std::vector &MidiDeviceInstance::GetMessages() +{ + return _messages; +} + +void MidiDeviceInstance::ReceiveMidiMessage(const libremidi::message &msg) +{ + auto lock = LockContext(); + _messages.emplace_back(msg); + vblog(LOG_INFO, "received midi: %s", + MidiMessage::ToString(msg).c_str()); +} + +void MidiDevice::UseForMessageSelection(bool skipBufferClear) +{ + if (!_dev) { + return; + } + + blog(LOG_INFO, "%s \"listen\" mode for midi input device \"%s\"! %s", + skipBufferClear ? "Enable" : "Disable", Name().c_str(), + skipBufferClear + ? "This will block incoming messages from being processed!" + : ""); + ClearMessageBuffer(); + _dev->_skipBufferClear = skipBufferClear; +} + +bool MidiDevice::IsUsedForMessageSelection() +{ + return _dev && _dev->_skipBufferClear; +} + +void MidiDevice::ClearMessageBuffer() +{ + if (_dev) { + _dev->ClearMessageBuffer(); + } +} + +const std::vector *MidiDevice::GetMessages(bool ignoreSkip) +{ + if (_type == MidiDeviceType::OUTPUT || _port == -1 || !_dev || + (_dev->_skipBufferClear && !ignoreSkip)) { + return nullptr; + } + + return &_dev->GetMessages(); +} + +static QString portToName(bool input, int port) +{ + std::string name; + try { + if (input) { + auto midiin = libremidi::midi_in(); + name = midiin.get_port_name(port); + } else { + auto midiout = libremidi::midi_out(); + name = midiout.get_port_name(port); + } + } catch (const libremidi::driver_error &error) { + blog(LOG_WARNING, + "Failed to get midi %s device name of port #%d: %s", + input ? "input" : "output", port, error.what()); + } + + const QString deviceNamePattern = + obs_module_text("AdvSceneSwitcher.midi.deviceNamePattern"); + return deviceNamePattern.arg(QString::number(port), + QString::fromStdString(name)); +} + +std::string MidiDevice::GetInputName() const +{ + return portToName(true, _port).toStdString(); +} + +std::string MidiDevice::GetOutputName() const +{ + return portToName(false, _port).toStdString(); +} + +std::string MidiDevice::Name() const +{ + if (_port < 0) { + return ""; + } + + if (_type == MidiDeviceType::INPUT) { + return GetInputName(); + } + return GetOutputName(); +} + +static inline QStringList getInputDeviceNames() +{ + QStringList devices; + try { + auto midiin = libremidi::midi_in(); + auto nPorts = midiin.get_port_count(); + for (unsigned i = 0; i < nPorts; i++) { + devices << portToName(true, i); + } + } catch (const libremidi::driver_error &error) { + blog(LOG_WARNING, "Failed to get midi input devices: %s", + error.what()); + } + return devices; +} + +static inline QStringList getOutputDeviceNames() +{ + QStringList devices; + try { + auto midiout = libremidi::midi_out(); + auto nPorts = midiout.get_port_count(); + for (unsigned i = 0; i < nPorts; i++) { + devices << portToName(false, i); + } + } catch (const libremidi::driver_error &error) { + blog(LOG_WARNING, "Failed to get midi output devices: %s", + error.what()); + } + return devices; +} + +MidiDeviceSelection::MidiDeviceSelection(QWidget *parent, MidiDeviceType t) + : QComboBox(parent), _type(t) +{ + AddSelectionEntry(this, obs_module_text("AdvSceneSwitcher.selectItem")); + + if (_type == MidiDeviceType::INPUT) { + addItems(getInputDeviceNames()); + } else { + addItems(getOutputDeviceNames()); + } + + QWidget::connect(this, SIGNAL(currentIndexChanged(int)), this, + SLOT(IdxChangedHelper(int))); +} + +void MidiDeviceSelection::SetDevice(const MidiDevice &_dev) +{ + setCurrentText(QString::fromStdString(_dev.Name())); +} + +void MidiDeviceSelection::IdxChangedHelper(int idx) +{ + if (idx == 0) { + emit DeviceSelectionChanged(MidiDevice()); + } + + auto devInstance = MidiDeviceInstance::GetDevice(_type, idx - 1); + if (!devInstance) { + DisplayMessage(obs_module_text( + "AdvSceneSwitcher.midi.deviceOpenFail")); + const QSignalBlocker b(this); + setCurrentIndex(0); + idx = 0; + } + + MidiDevice dev; + dev._type = _type; + dev._port = idx - 1; + dev._dev = devInstance; + emit DeviceSelectionChanged(dev); +} + +static void populateMidiMessageTypeSelection(QComboBox *list) +{ + list->addItem( + obs_module_text("AdvSceneSwitcher.midi.message.type.optional")); + list->addItem(QString::fromStdString(MidiMessage::MidiTypeToString( + libremidi::message_type::NOTE_OFF))); + list->addItem(QString::fromStdString(MidiMessage::MidiTypeToString( + libremidi::message_type::NOTE_ON))); + list->addItem(QString::fromStdString(MidiMessage::MidiTypeToString( + libremidi::message_type::POLY_PRESSURE))); + list->addItem(QString::fromStdString(MidiMessage::MidiTypeToString( + libremidi::message_type::CONTROL_CHANGE))); + list->addItem(QString::fromStdString(MidiMessage::MidiTypeToString( + libremidi::message_type::PROGRAM_CHANGE))); + list->addItem(QString::fromStdString(MidiMessage::MidiTypeToString( + libremidi::message_type::AFTERTOUCH))); + list->addItem(QString::fromStdString(MidiMessage::MidiTypeToString( + libremidi::message_type::PITCH_BEND))); + list->addItem(QString::fromStdString(MidiMessage::MidiTypeToString( + libremidi::message_type::SYSTEM_EXCLUSIVE))); + list->addItem(QString::fromStdString(MidiMessage::MidiTypeToString( + libremidi::message_type::TIME_CODE))); + list->addItem(QString::fromStdString(MidiMessage::MidiTypeToString( + libremidi::message_type::SONG_POS_POINTER))); + list->addItem(QString::fromStdString(MidiMessage::MidiTypeToString( + libremidi::message_type::SONG_SELECT))); + list->addItem(QString::fromStdString(MidiMessage::MidiTypeToString( + libremidi::message_type::RESERVED1))); + list->addItem(QString::fromStdString(MidiMessage::MidiTypeToString( + libremidi::message_type::RESERVED2))); + list->addItem(QString::fromStdString(MidiMessage::MidiTypeToString( + libremidi::message_type::TUNE_REQUEST))); + list->addItem(QString::fromStdString( + MidiMessage::MidiTypeToString(libremidi::message_type::EOX))); + list->addItem(QString::fromStdString(MidiMessage::MidiTypeToString( + libremidi::message_type::TIME_CLOCK))); + list->addItem(QString::fromStdString(MidiMessage::MidiTypeToString( + libremidi::message_type::RESERVED3))); + list->addItem(QString::fromStdString( + MidiMessage::MidiTypeToString(libremidi::message_type::START))); + list->addItem(QString::fromStdString(MidiMessage::MidiTypeToString( + libremidi::message_type::CONTINUE))); + list->addItem(QString::fromStdString( + MidiMessage::MidiTypeToString(libremidi::message_type::STOP))); + list->addItem(QString::fromStdString(MidiMessage::MidiTypeToString( + libremidi::message_type::RESERVED4))); + list->addItem(QString::fromStdString(MidiMessage::MidiTypeToString( + libremidi::message_type::ACTIVE_SENSING))); + list->addItem(QString::fromStdString(MidiMessage::MidiTypeToString( + libremidi::message_type::SYSTEM_RESET))); +} + +MidiMessageSelection::MidiMessageSelection(QWidget *parent) + : QWidget(parent), + _type(new QComboBox()), + _channel(new VariableSpinBox()), + _noteValue(new VariableSpinBox()), + _noteString(new QComboBox()), + _noteValueStringToggle(new QPushButton()), + _value(new VariableSpinBox()) +{ + populateMidiMessageTypeSelection(_type); + + _noteString->addItem( + obs_module_text("AdvSceneSwitcher.midi.message.placeholder")); + _noteString->addItems(GetAllNotes()); + _noteString->setEditable(true); + _noteString->setInsertPolicy(QComboBox::NoInsert); + + _noteValueStringToggle->setMaximumWidth(22); + _noteValueStringToggle->setCheckable(true); + const auto path = GetDataFilePath("res/images/" + GetThemeTypeName() + + "Note.svg"); + SetButtonIcon(_noteValueStringToggle, path.c_str()); + + _channel->specialValueText( + obs_module_text("AdvSceneSwitcher.midi.message.placeholder")); + _noteValue->specialValueText( + obs_module_text("AdvSceneSwitcher.midi.message.placeholder")); + _value->specialValueText( + obs_module_text("AdvSceneSwitcher.midi.message.placeholder")); + + _channel->setMinimum(MidiMessage::optionalChannelIndicator); + _noteValue->setMinimum(MidiMessage::optionalNoteIndicator); + _value->setMinimum(MidiMessage::optionalValueIndicator); + + _channel->setMaximum(16); + _noteValue->setMaximum(127); + _value->setMaximum(127); + + connect(_type, SIGNAL(currentTextChanged(const QString &)), this, + SLOT(TypeChanged(const QString &))); + connect(_channel, + SIGNAL(NumberVariableChanged(const NumberVariable &)), + this, SLOT(ChannelChanged(const NumberVariable &))); + connect(_noteValue, + SIGNAL(NumberVariableChanged(const NumberVariable &)), + this, SLOT(NoteChanged(const NumberVariable &))); + connect(_noteString, SIGNAL(currentIndexChanged(int)), this, + SLOT(NoteStringIdxChanged(int))); + connect(_noteValueStringToggle, SIGNAL(toggled(bool)), this, + SLOT(ShowNote(bool))); + connect(_value, + SIGNAL(NumberVariableChanged(const NumberVariable &)), + this, SLOT(ValueChanged(const NumberVariable &))); + + auto layout = new QGridLayout; + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(new QLabel(obs_module_text( + "AdvSceneSwitcher.midi.message.type")), + 0, 0); + layout->addWidget(_type, 0, 1); + layout->addWidget(new QLabel(obs_module_text( + "AdvSceneSwitcher.midi.message.channel")), + 1, 0); + layout->addWidget(_channel, 1, 1); + layout->addWidget(new QLabel(obs_module_text( + "AdvSceneSwitcher.midi.message.note")), + 2, 0); + auto noteLayout = new QHBoxLayout; + noteLayout->addWidget(_noteValue); + noteLayout->addWidget(_noteString); + noteLayout->addWidget(_noteValueStringToggle); + noteLayout->setContentsMargins(0, 0, 0, 0); + layout->addLayout(noteLayout, 2, 1); + layout->addWidget(new QLabel(obs_module_text( + "AdvSceneSwitcher.midi.message.value")), + 3, 0); + layout->addWidget(_value, 3, 1); + + // Reduce label column to its minimum size + MinimizeSizeOfColumn(layout, 0); + + setLayout(layout); + + ShowNote(false); +} + +void MidiMessageSelection::SetMessage(const MidiMessage &m) +{ + _currentSelection = m; + const QSignalBlocker b(this); + if (m._typeIsOptional) { + _type->setCurrentText(obs_module_text( + "AdvSceneSwitcher.midi.message.type.optional")); + } else { + _type->setCurrentText(QString::fromStdString( + MidiMessage::MidiTypeToString(m._type))); + } + _channel->SetValue(m._channel); + _noteValue->SetValue(m._note); + _noteString->setCurrentIndex(m._note + 1); + _value->SetValue(m._value); +} + +libremidi::message_type +MidiMessageSelection::TextToMidiType(const QString &text) +{ + if (text.toStdString() == + MidiMessage::MidiTypeToString(libremidi::message_type::NOTE_OFF)) { + return libremidi::message_type::NOTE_OFF; + } + if (text.toStdString() == + MidiMessage::MidiTypeToString(libremidi::message_type::NOTE_ON)) { + return libremidi::message_type::NOTE_ON; + } + if (text.toStdString() == + MidiMessage::MidiTypeToString( + libremidi::message_type::POLY_PRESSURE)) { + return libremidi::message_type::POLY_PRESSURE; + } + if (text.toStdString() == + MidiMessage::MidiTypeToString( + libremidi::message_type::CONTROL_CHANGE)) { + return libremidi::message_type::CONTROL_CHANGE; + } + if (text.toStdString() == + MidiMessage::MidiTypeToString( + libremidi::message_type::PROGRAM_CHANGE)) { + return libremidi::message_type::PROGRAM_CHANGE; + } + if (text.toStdString() == + MidiMessage::MidiTypeToString( + libremidi::message_type::AFTERTOUCH)) { + return libremidi::message_type::AFTERTOUCH; + } + if (text.toStdString() == + MidiMessage::MidiTypeToString( + libremidi::message_type::PITCH_BEND)) { + return libremidi::message_type::PITCH_BEND; + } + if (text.toStdString() == + MidiMessage::MidiTypeToString( + libremidi::message_type::SYSTEM_EXCLUSIVE)) { + return libremidi::message_type::SYSTEM_EXCLUSIVE; + } + if (text.toStdString() == + MidiMessage::MidiTypeToString(libremidi::message_type::TIME_CODE)) { + return libremidi::message_type::TIME_CODE; + } + if (text.toStdString() == + MidiMessage::MidiTypeToString( + libremidi::message_type::SONG_POS_POINTER)) { + return libremidi::message_type::SONG_POS_POINTER; + } + if (text.toStdString() == + MidiMessage::MidiTypeToString( + libremidi::message_type::SONG_SELECT)) { + return libremidi::message_type::SONG_SELECT; + } + if (text.toStdString() == + MidiMessage::MidiTypeToString(libremidi::message_type::RESERVED1)) { + return libremidi::message_type::RESERVED1; + } + if (text.toStdString() == + MidiMessage::MidiTypeToString(libremidi::message_type::RESERVED2)) { + return libremidi::message_type::RESERVED2; + } + if (text.toStdString() == + MidiMessage::MidiTypeToString( + libremidi::message_type::TUNE_REQUEST)) { + return libremidi::message_type::TUNE_REQUEST; + } + if (text.toStdString() == + MidiMessage::MidiTypeToString(libremidi::message_type::EOX)) { + return libremidi::message_type::EOX; + } + if (text.toStdString() == + MidiMessage::MidiTypeToString( + libremidi::message_type::TIME_CLOCK)) { + return libremidi::message_type::TIME_CLOCK; + } + if (text.toStdString() == + MidiMessage::MidiTypeToString(libremidi::message_type::RESERVED3)) { + return libremidi::message_type::RESERVED3; + } + if (text.toStdString() == + MidiMessage::MidiTypeToString(libremidi::message_type::START)) { + return libremidi::message_type::START; + } + if (text.toStdString() == + MidiMessage::MidiTypeToString(libremidi::message_type::CONTINUE)) { + return libremidi::message_type::CONTINUE; + } + if (text.toStdString() == + MidiMessage::MidiTypeToString(libremidi::message_type::STOP)) { + return libremidi::message_type::STOP; + } + if (text.toStdString() == + MidiMessage::MidiTypeToString(libremidi::message_type::RESERVED4)) { + return libremidi::message_type::RESERVED4; + } + if (text.toStdString() == + MidiMessage::MidiTypeToString( + libremidi::message_type::ACTIVE_SENSING)) { + return libremidi::message_type::ACTIVE_SENSING; + } + if (text.toStdString() == + MidiMessage::MidiTypeToString( + libremidi::message_type::SYSTEM_RESET)) { + return libremidi::message_type::SYSTEM_RESET; + } + return libremidi::message_type::INVALID; +} + +void MidiMessageSelection::TypeChanged(const QString &text) +{ + _currentSelection._typeIsOptional = + text == + obs_module_text("AdvSceneSwitcher.midi.message.type.optional"); + if (_currentSelection._typeIsOptional) { + emit MidiMessageChanged(_currentSelection); + return; + } + + _currentSelection._type = TextToMidiType(text); + emit MidiMessageChanged(_currentSelection); +} + +void MidiMessageSelection::ChannelChanged(const NumberVariable &c) +{ + _currentSelection._channel = c; + emit MidiMessageChanged(_currentSelection); +} + +void MidiMessageSelection::NoteChanged(const NumberVariable &n) +{ + const QSignalBlocker b(_noteString); + _noteString->setCurrentIndex(n.GetFixedValue() + 1); + _currentSelection._note = n; + emit MidiMessageChanged(_currentSelection); +} + +void MidiMessageSelection::NoteStringIdxChanged(int value) +{ + const QSignalBlocker b(_noteValue); + _currentSelection._note = value; + _noteValue->SetFixedValue(value - 1); + emit MidiMessageChanged(_currentSelection); +} + +void MidiMessageSelection::ShowNote(bool show) +{ + _noteValue->setVisible(!show); + _noteString->setVisible(show); +} + +void MidiMessageSelection::ValueChanged(const NumberVariable &v) +{ + _currentSelection._value = v; + emit MidiMessageChanged((_currentSelection)); +} + +QStringList GetAllNotes() +{ + QStringList result; + QStringList notes = {"C", "C#", "D", "D#", "E", "F", + "F#", "G", "G#", "A", "A#", "B"}; + for (int octave = -1; octave <= 9; octave++) { + for (int noteIndex = 0; noteIndex < 12; noteIndex++) { + int midiNote = octave * 12 + noteIndex; + result << notes[noteIndex] + QString::number(octave); + } + } + return result; +} + +} // namespace advss diff --git a/src/macro-external/midi/midi-helpers.hpp b/src/macro-external/midi/midi-helpers.hpp new file mode 100644 index 00000000..e8521e7a --- /dev/null +++ b/src/macro-external/midi/midi-helpers.hpp @@ -0,0 +1,172 @@ +#pragma once +#include +#include +#include +#include +#include + +#pragma warning(push) +#pragma warning(disable : 4005) +#define LIBREMIDI_HEADER_ONLY 1 +#include +#pragma warning(pop) + +namespace advss { + +// Based on https://github.com/nhielost/obs-midi-mg MMGMessage +class MidiMessage { +public: + MidiMessage() = default; + MidiMessage(const libremidi::message &message); + + void Save(obs_data_t *obj) const; + void Load(obs_data_t *obj); + + bool Matches(const MidiMessage &) const; + + static std::string ToString(const libremidi::message &msg); + static std::string MidiTypeToString(libremidi::message_type type); + static std::string GetMidiType(const libremidi::message &msg); + static int GetMidiNote(const libremidi::message &msg); + static int GetMidiValue(const libremidi::message &msg); + + std::string ToString() const; + libremidi::message_type Type() const { return _type; } + int Channel() const { return _channel; } + int Note() const { return _note; } + int Value() const { return _value; } + +private: + // Values which don't appear for channel, note, and value will be used + // to indicate whether this part of the message is optional and can be + // disregarded (e.g.during comparison using Matches()) + static const int optionalChannelIndicator = 0; + static const int optionalNoteIndicator = -1; + static const int optionalValueIndicator = -1; + + bool _typeIsOptional = true; + libremidi::message_type _type = libremidi::message_type::INVALID; + NumberVariable _channel = optionalChannelIndicator; + NumberVariable _note = optionalNoteIndicator; + NumberVariable _value = optionalValueIndicator; + + friend class MidiMessageSelection; +}; + +enum class MidiDeviceType { + INPUT, + OUTPUT, +}; + +class MidiDeviceInstance { +public: + static MidiDeviceInstance *GetDevice(MidiDeviceType type, int port); + static void ClearMessageBuffersOfAllDevices(); + static void ResetAllDevices(); + +private: + MidiDeviceInstance() = default; + ~MidiDeviceInstance() = default; + bool OpenPort(); + void ClosePort(); + bool SendMessge(const MidiMessage &); + const std::vector &GetMessages(); + void ReceiveMidiMessage(const libremidi::message &); + void ClearMessageBuffer(); + + static std::map, MidiDeviceInstance *> + devices; + + bool _skipBufferClear = false; + + MidiDeviceType _type = MidiDeviceType::INPUT; + int _port = -1; + libremidi::midi_in in; + libremidi::midi_out out; + std::vector _messages; + + friend class MidiDevice; +}; + +class MidiDevice { +public: + MidiDevice() = default; + + void Save(obs_data_t *obj) const; + void Load(obs_data_t *obj); + + bool SendMessge(const MidiMessage &); + + const std::vector * // Might resize! Only call + GetMessages(bool ignoreListenMode = false); // while holding switcher + // lock! + std::string Name() const; + + // Used for "listen" mode of message selection + // Listen mode disables automatic clearing of buffers + void UseForMessageSelection(bool); + bool IsUsedForMessageSelection(); + void ClearMessageBuffer(); + bool DeviceSelected() { return !!_dev; } + +private: + std::string GetInputName() const; + std::string GetOutputName() const; + + MidiDeviceType _type = MidiDeviceType::INPUT; + int _port = -1; + MidiDeviceInstance *_dev = nullptr; + + friend class MidiDeviceSelection; +}; + +class MidiMessageSelection : public QWidget { + Q_OBJECT + +public: + MidiMessageSelection(QWidget *parent); + void SetMessage(const MidiMessage &); + +private slots: + void TypeChanged(const QString &); + void ChannelChanged(const NumberVariable &); + void NoteChanged(const NumberVariable &); + void NoteStringIdxChanged(int); + void ShowNote(bool); + void ValueChanged(const NumberVariable &); + +signals: + void MidiMessageChanged(const MidiMessage &); + +private: + static libremidi::message_type TextToMidiType(const QString &); + + QComboBox *_type; + VariableSpinBox *_channel; + VariableSpinBox *_noteValue; + QComboBox *_noteString; + QPushButton *_noteValueStringToggle; + VariableSpinBox *_value; + + MidiMessage _currentSelection; +}; + +class MidiDeviceSelection : public QComboBox { + Q_OBJECT + +public: + MidiDeviceSelection(QWidget *parent, MidiDeviceType); + void SetDevice(const MidiDevice &); + +private slots: + void IdxChangedHelper(int); +signals: + void DeviceSelectionChanged(const MidiDevice &); + +private: + const MidiDeviceType _type; +}; + +QStringList GetAllNotes(); + +} // namespace advss