Add MIDI condition and action

This new action will allow you to send MIDI messages to the selected
device.
The condition type will allow you to perform actions based on the MIDI
messages received from the selected device.
This commit is contained in:
WarmUpTill 2023-04-12 16:18:00 +02:00 committed by WarmUpTill
parent a992f7a0e8
commit 530bbc07c5
13 changed files with 1772 additions and 1 deletions

3
.gitmodules vendored
View File

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

View File

@ -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"

View File

@ -0,0 +1,24 @@
<?xml version="1.0"?>
<svg width="640" height="640" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet" version="1.1">
<defs>
<path d="m599.62,325.49c0,151.77 -123.21,274.98 -274.98,274.98c-151.76,0 -274.97,-123.21 -274.97,-274.98c0,-151.76 123.21,-274.97 274.97,-274.97c151.77,0 274.98,123.21 274.98,274.97z" id="c3Wyq0kfYS"/>
<mask height="689.95" id="maskanTyyngos" maskUnits="userSpaceOnUse" width="689.95" x="-20.33" y="-19.48">
<rect fill="white" height="689.95" id="svg_1" width="689.95" x="-20.33" y="-19.48"/>
<use fill="black" id="svg_2" xlink:href="#c3Wyq0kfYS"/>
</mask>
<path d="m334.97,96.07c4.54,0 8.23,3.68 8.23,8.23c0,46.94 0,179.54 0,226.48c0,4.54 -3.69,8.22 -8.23,8.22c-5.77,0 -14.88,0 -20.66,0c-4.54,0 -8.22,-3.68 -8.22,-8.22c0,-46.94 0,-179.54 0,-226.48c0,-4.55 3.68,-8.23 8.22,-8.23c5.78,0 14.89,0 20.66,0z" id="fxMeu932c"/>
<path d="m471.96,338.21c0.01,3.1 -2.5,5.62 -5.59,5.63c-32.02,0.09 -122.45,0.37 -154.46,0.47c-3.1,0.01 -5.61,-2.49 -5.62,-5.59c-0.02,-6.3 -0.06,-19.59 -0.08,-25.89c-0.01,-3.1 2.49,-5.62 5.59,-5.63c32.01,-0.09 122.44,-0.37 154.45,-0.47c3.1,-0.01 5.62,2.5 5.63,5.59c0.02,6.3 0.06,19.59 0.08,25.89z" id="h65nGmP2BL"/>
</defs>
<g class="layer">
<title>Layer 1</title>
<path fill="none" fill-opacity="null" id="svg_11" stroke="#000000" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" stroke-opacity="0.02"/>
<ellipse cx="103.84" cy="562.04" fill="#fefefe" id="svg_3" rx="80" ry="45" stroke="#000000" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" stroke-opacity="0.02" transform="rotate(-18.699, 103.843, 562.038)"/>
<ellipse cx="506" cy="554" fill="#fefefe" id="svg_4" rx="77" ry="45" stroke="#000000" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" stroke-opacity="0.02" transform="rotate(-18.699, 506, 554)"/>
<rect fill="#fefefe" height="359" id="svg_7" stroke="#000000" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" stroke-opacity="0.02" stroke-width="0" width="58" x="122" y="185"/>
<rect fill="#fefefe" height="453" id="svg_8" stroke="#000000" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" stroke-opacity="0.02" width="58" x="522" y="84.5"/>
<rect fill="#fefefe" height="37" id="svg_9" stroke="#000000" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" stroke-opacity="0.02" transform="rotate(-14.1985, 351.556, 146.245)" width="463.07" x="120.02" y="127.74"/>
<rect fill="none" fill-opacity="null" height="0" id="svg_10" stroke="#000000" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" stroke-opacity="0.02" width="0" x="538.25" y="107.12"/>
<path d="m537.42,112.06l0,-67.88l17.58,67.88z" fill="#fefefe" id="svg_16" stroke="#000000" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" stroke-opacity="0.02" transform="rotate(-90, 546.203, 78.1248)"/>
<rect fill="#fefefe" height="0" id="svg_18" stroke="#000000" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" stroke-opacity="0.02" width="0" x="82.12" y="287.56"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -0,0 +1,24 @@
<?xml version="1.0"?>
<svg width="640" height="640" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet" version="1.1">
<defs>
<path d="m599.62,325.49c0,151.77 -123.21,274.98 -274.98,274.98c-151.76,0 -274.97,-123.21 -274.97,-274.98c0,-151.76 123.21,-274.97 274.97,-274.97c151.77,0 274.98,123.21 274.98,274.97z" id="c3Wyq0kfYS"/>
<mask height="689.95" id="maskanTyyngos" maskUnits="userSpaceOnUse" width="689.95" x="-20.33" y="-19.48">
<rect fill="white" height="689.95" id="svg_1" width="689.95" x="-20.33" y="-19.48"/>
<use fill="black" id="svg_2" xlink:href="#c3Wyq0kfYS"/>
</mask>
<path d="m334.97,96.07c4.54,0 8.23,3.68 8.23,8.23c0,46.94 0,179.54 0,226.48c0,4.54 -3.69,8.22 -8.23,8.22c-5.77,0 -14.88,0 -20.66,0c-4.54,0 -8.22,-3.68 -8.22,-8.22c0,-46.94 0,-179.54 0,-226.48c0,-4.55 3.68,-8.23 8.22,-8.23c5.78,0 14.89,0 20.66,0z" id="fxMeu932c"/>
<path d="m471.96,338.21c0.01,3.1 -2.5,5.62 -5.59,5.63c-32.02,0.09 -122.45,0.37 -154.46,0.47c-3.1,0.01 -5.61,-2.49 -5.62,-5.59c-0.02,-6.3 -0.06,-19.59 -0.08,-25.89c-0.01,-3.1 2.49,-5.62 5.59,-5.63c32.01,-0.09 122.44,-0.37 154.45,-0.47c3.1,-0.01 5.62,2.5 5.63,5.59c0.02,6.3 0.06,19.59 0.08,25.89z" id="h65nGmP2BL"/>
</defs>
<g class="layer">
<title>Layer 1</title>
<path fill="none" fill-opacity="null" id="svg_11" stroke="#000000" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" stroke-opacity="0.02"/>
<ellipse cx="103.84" cy="562.04" fill="#202020" id="svg_3" rx="80" ry="45" stroke="#000000" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" stroke-opacity="0.02" transform="rotate(-18.699, 103.843, 562.038)"/>
<ellipse cx="506" cy="554" fill="#202020" id="svg_4" rx="77" ry="45" stroke="#000000" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" stroke-opacity="0.02" transform="rotate(-18.699, 506, 554)"/>
<rect fill="#202020" height="359" id="svg_7" stroke="#000000" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" stroke-opacity="0.02" stroke-width="0" width="58" x="122" y="185"/>
<rect fill="#202020" height="453" id="svg_8" stroke="#000000" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" stroke-opacity="0.02" width="58" x="522" y="84.5"/>
<rect fill="#202020" height="37" id="svg_9" stroke="#000000" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" stroke-opacity="0.02" transform="rotate(-14.1985, 351.556, 146.245)" width="463.07" x="120.02" y="127.74"/>
<rect fill="none" fill-opacity="null" height="0" id="svg_10" stroke="#000000" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" stroke-opacity="0.02" width="0" x="538.25" y="107.12"/>
<path d="m537.42,112.06l0,-67.88l17.58,67.88z" fill="#202020" id="svg_16" stroke="#000000" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" stroke-opacity="0.02" transform="rotate(-90, 546.203, 78.1248)"/>
<rect fill="#202020" height="0" id="svg_18" stroke="#000000" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" stroke-opacity="0.02" width="0" x="82.12" y="287.56"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

1
deps/libremidi vendored Submodule

@ -0,0 +1 @@
Subproject commit 56147984eea2bd4aeeb5fb51b677f058d29daef8

View File

@ -17,5 +17,6 @@ install_advss_plugin_dependency(...)
... to install the plugin and its dependencies.
#]]
add_subdirectory(midi)
add_subdirectory(openvr)
add_subdirectory(video)

View File

@ -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})

View File

@ -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<MacroActionMidi> 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

View File

@ -0,0 +1,75 @@
#pragma once
#include "macro-action-edit.hpp"
#include "midi-helpers.hpp"
#include <QPushButton>
#include <QTimer>
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<MacroAction> Create(Macro *m)
{
return std::make_shared<MacroActionMidi>(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<MacroActionMidi> entryData = nullptr);
virtual ~MacroActionMidiEdit();
void UpdateEntryData();
static QWidget *Create(QWidget *parent,
std::shared_ptr<MacroAction> action)
{
return new MacroActionMidiEdit(
parent,
std::dynamic_pointer_cast<MacroActionMidi>(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<MacroActionMidi> _entryData;
MidiDeviceSelection *_devices;
MidiMessageSelection *_message;
MidiDeviceSelection *_listenDevices;
QPushButton *_resetMidiDevices;
QPushButton *_listen;
MidiDevice _listenDevice;
QTimer _listenTimer;
bool _currentlyListening = false;
bool _loading = true;
};
} // namespace advss

View File

@ -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<MacroConditionMidi> 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

View File

@ -0,0 +1,71 @@
#pragma once
#include "macro-condition-edit.hpp"
#include "midi-helpers.hpp"
#include <QPushButton>
#include <QTimer>
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<MacroCondition> Create(Macro *m)
{
return std::make_shared<MacroConditionMidi>(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<MacroConditionMidi> cond = nullptr);
virtual ~MacroConditionMidiEdit();
void UpdateEntryData();
static QWidget *Create(QWidget *parent,
std::shared_ptr<MacroCondition> cond)
{
return new MacroConditionMidiEdit(
parent,
std::dynamic_pointer_cast<MacroConditionMidi>(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<MacroConditionMidi> _entryData;
MidiDeviceSelection *_devices;
MidiMessageSelection *_message;
QPushButton *_resetMidiDevices;
QPushButton *_listen;
QTimer _listenTimer;
bool _currentlyListening = false;
bool _loading = true;
};
} // namespace advss

View File

@ -0,0 +1,930 @@
#include "midi-helpers.hpp"
#include <utility.hpp>
#include <obs-module-helper.hpp>
#include <switcher-data.hpp>
namespace advss {
static std::map<std::pair<MidiDeviceType, int>, MidiDeviceInstance *>
SetupMidiMessageVector()
{
GetSwitcher()->AddResetForNextIntervalFunction(
MidiDeviceInstance::ClearMessageBuffersOfAllDevices);
return {};
}
std::map<std::pair<MidiDeviceType, int>, 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<int>(_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<libremidi::message_type>(
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<int>(_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<MidiDeviceType>(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<MidiMessage> &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<MidiMessage> *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<int> &)),
this, SLOT(ChannelChanged(const NumberVariable<int> &)));
connect(_noteValue,
SIGNAL(NumberVariableChanged(const NumberVariable<int> &)),
this, SLOT(NoteChanged(const NumberVariable<int> &)));
connect(_noteString, SIGNAL(currentIndexChanged(int)), this,
SLOT(NoteStringIdxChanged(int)));
connect(_noteValueStringToggle, SIGNAL(toggled(bool)), this,
SLOT(ShowNote(bool)));
connect(_value,
SIGNAL(NumberVariableChanged(const NumberVariable<int> &)),
this, SLOT(ValueChanged(const NumberVariable<int> &)));
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<int> &c)
{
_currentSelection._channel = c;
emit MidiMessageChanged(_currentSelection);
}
void MidiMessageSelection::NoteChanged(const NumberVariable<int> &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<int> &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

View File

@ -0,0 +1,172 @@
#pragma once
#include <variable-spinbox.hpp>
#include <variable-number.hpp>
#include <variable-string.hpp>
#include <QComboBox>
#include <obs-data.h>
#pragma warning(push)
#pragma warning(disable : 4005)
#define LIBREMIDI_HEADER_ONLY 1
#include <libremidi/libremidi.hpp>
#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<int> _channel = optionalChannelIndicator;
NumberVariable<int> _note = optionalNoteIndicator;
NumberVariable<int> _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<MidiMessage> &GetMessages();
void ReceiveMidiMessage(const libremidi::message &);
void ClearMessageBuffer();
static std::map<std::pair<MidiDeviceType, int>, MidiDeviceInstance *>
devices;
bool _skipBufferClear = false;
MidiDeviceType _type = MidiDeviceType::INPUT;
int _port = -1;
libremidi::midi_in in;
libremidi::midi_out out;
std::vector<MidiMessage> _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<MidiMessage> * // 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<int> &);
void NoteChanged(const NumberVariable<int> &);
void NoteStringIdxChanged(int);
void ShowNote(bool);
void ValueChanged(const NumberVariable<int> &);
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