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 @@
+
+
\ 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 @@
+
+
\ 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