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