Skip to content

Commit ed0847c

Browse files
committed
New feature #468: Add a secondary MIDI output that mirrors all MIDI messages KnobKraft sends
1 parent 64e3043 commit ed0847c

4 files changed

Lines changed: 134 additions & 3 deletions

File tree

The-Orm/KeyboardMacroView.cpp

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
#include <spdlog/spdlog.h>
1717
#include "SpdLogJuce.h"
1818
#include <string>
19+
#include <algorithm>
20+
#include <cctype>
1921

2022
// Standardize text
2123
const char *kMacrosEnabled = "Macros enabled";
@@ -24,6 +26,7 @@ const char *kRouteMasterkeyboard = "Forward MIDI to synth";
2426
const char *kFixedSynthSelected = "Fixed synth played";
2527
const char *kFixedSynthSelectedSettingKey = "Fixed synth played name";
2628
const char *kUseElectraOne = "Forward Electra One";
29+
const char* kSecondaryMIDIOut = "Secondary MIDI OUT";
2730
const char *kInputDevice = "MIDI Input Device";
2831
const char *kInputDeviceSettingKey = "MIDI Input Device Name";
2932
const char *kMidiChannel = "MIDI channel";
@@ -203,6 +206,7 @@ KeyboardMacroView::~KeyboardMacroView()
203206
void KeyboardMacroView::setupPropertyEditor() {
204207
// Midi Device Selector can broadcast a change message when a new device is detected or removed
205208
midiDeviceList_ = std::make_shared<MidiDevicePropertyEditor>(kInputDevice, "Setup Masterkeyboard", true);
209+
secondaryMidiOutList_ = std::make_shared<MidiDevicePropertyEditor>(kSecondaryMIDIOut, "Secondary MIDI OUT", false, true, "Disabled");
206210
midikraft::MidiController::instance()->addChangeListener(this);
207211

208212
customMasterkeyboardSetup_.clear();
@@ -214,6 +218,7 @@ void KeyboardMacroView::setupPropertyEditor() {
214218
synthListEditor_ = std::make_shared<TypedNamedValue>(kFixedSynthSelected, "MIDI Routing", 1, std::map<int, std::string>());
215219
refreshSynthList();
216220
customMasterkeyboardSetup_.push_back(synthListEditor_);
221+
customMasterkeyboardSetup_.push_back(secondaryMidiOutList_);
217222
customMasterkeyboardSetup_.push_back(std::make_shared<TypedNamedValue>(kUseElectraOne, "MIDI Routing", false));
218223
customMasterkeyboardSetup_.push_back(midiDeviceList_);
219224
customMasterkeyboardSetup_.push_back(std::make_shared<MidiChannelPropertyEditor>(kMidiChannel, "Setup Masterkeyboard"));
@@ -299,6 +304,34 @@ void KeyboardMacroView::loadFromSettings() {
299304
if (index != 0) {
300305
midiDeviceProp->value().setValue(index);
301306
}
307+
else if (!storedValue.empty()) {
308+
auto appended = midiDeviceProp->findOrAppendLookup(storedValue);
309+
if (appended != 0) {
310+
midiDeviceProp->value().setValue(appended);
311+
}
312+
}
313+
}
314+
continue;
315+
}
316+
317+
if (propertyName == kSecondaryMIDIOut) {
318+
auto allDigits = std::all_of(storedValue.begin(), storedValue.end(), [](unsigned char ch) { return std::isdigit(ch) != 0; });
319+
if (allDigits) {
320+
prop->value().setValue(0);
321+
continue;
322+
}
323+
auto midiDeviceProp = std::dynamic_pointer_cast<MidiDevicePropertyEditor>(prop);
324+
if (midiDeviceProp) {
325+
int index = midiDeviceProp->indexOfValue(storedValue);
326+
if (index != 0) {
327+
midiDeviceProp->value().setValue(index);
328+
}
329+
else if (!storedValue.empty()) {
330+
auto appended = midiDeviceProp->findOrAppendLookup(storedValue);
331+
if (appended != 0) {
332+
midiDeviceProp->value().setValue(appended);
333+
}
334+
}
302335
}
303336
continue;
304337
}
@@ -319,6 +352,7 @@ void KeyboardMacroView::loadFromSettings() {
319352
spdlog::error("Keyboard macro definition corrupt in settings file, not loading. Error is {}", e.what());
320353
}
321354
}
355+
updateSecondaryMidiOutSelection();
322356
}
323357
void KeyboardMacroView::saveSettings() {
324358
var result;
@@ -347,6 +381,21 @@ void KeyboardMacroView::saveSettings() {
347381
settingKey = kFixedSynthSelectedSettingKey;
348382
Settings::instance().set(settingKey, prop->lookupValue());
349383
}
384+
else if (propertyName == kSecondaryMIDIOut) {
385+
auto secondaryProp = std::dynamic_pointer_cast<MidiDevicePropertyEditor>(prop);
386+
if (secondaryProp) {
387+
int selectedRow = int(secondaryProp->value().getValue());
388+
if (selectedRow <= 0) {
389+
Settings::instance().set(settingKey, "");
390+
}
391+
else {
392+
Settings::instance().set(settingKey, secondaryProp->lookupValue());
393+
}
394+
}
395+
else {
396+
Settings::instance().set(settingKey, prop->value().toString().toStdString());
397+
}
398+
}
350399
else {
351400
Settings::instance().set(settingKey, prop->value().toString().toStdString());
352401
}
@@ -396,6 +445,10 @@ void KeyboardMacroView::valueChanged(Value& value)
396445
else if (value.refersToSameSourceAs(customMasterkeyboardSetup_.valueByName(kUseElectraOne))) {
397446
controllerRouter_.enable(value.getValue());
398447
}
448+
else if (customMasterkeyboardSetup_.hasValue(kSecondaryMIDIOut) &&
449+
value.refersToSameSourceAs(customMasterkeyboardSetup_.valueByName(kSecondaryMIDIOut))) {
450+
updateSecondaryMidiOutSelection();
451+
}
399452
}
400453

401454
void KeyboardMacroView::turnOnMasterkeyboardInput() {
@@ -412,7 +465,9 @@ void KeyboardMacroView::changeListenerCallback(ChangeBroadcaster* source)
412465
if (source == midikraft::MidiController::instance()) {
413466
// The list of MIDI devices changed, need to refresh the property editor
414467
midiDeviceList_->refreshDeviceList();
468+
refreshSecondaryMidiOutList();
415469
customSetup_.setProperties(customMasterkeyboardSetup_);
470+
updateSecondaryMidiOutSelection();
416471
}
417472
else if (source == &UIModel::instance()->synthList_) {
418473
refreshSynthList();
@@ -479,3 +534,69 @@ bool KeyboardMacroView::isMacroState(KeyboardMacro const &macro)
479534
}
480535
return allDetected && !extraKeyDetected;
481536
}
537+
538+
void KeyboardMacroView::handleMidiMessage(const MidiMessage& message, const String& source, bool isOut)
539+
{
540+
if (!isOut) {
541+
return;
542+
}
543+
544+
juce::String secondaryName;
545+
{
546+
std::scoped_lock lock(secondaryMidiOutMutex_);
547+
secondaryName = secondaryMidiOutName_;
548+
}
549+
550+
if (secondaryName.isEmpty() || source == secondaryName) {
551+
return;
552+
}
553+
554+
auto controller = midikraft::MidiController::instance();
555+
if (!controller) {
556+
return;
557+
}
558+
559+
auto secondaryInfo = controller->getMidiOutputByName(secondaryName);
560+
if (secondaryInfo.identifier.isEmpty()) {
561+
return;
562+
}
563+
564+
auto secondaryOutput = controller->getMidiOutput(secondaryInfo);
565+
if (!secondaryOutput || !secondaryOutput->isValid()) {
566+
return;
567+
}
568+
569+
// Forward a copy to the secondary output
570+
secondaryOutput->sendMessageNow(message);
571+
}
572+
573+
void KeyboardMacroView::refreshSecondaryMidiOutList()
574+
{
575+
if (secondaryMidiOutList_) {
576+
secondaryMidiOutList_->refreshDeviceList();
577+
}
578+
}
579+
580+
void KeyboardMacroView::updateSecondaryMidiOutSelection()
581+
{
582+
if (!secondaryMidiOutList_) {
583+
return;
584+
}
585+
586+
const int selectedRow = int(secondaryMidiOutList_->value().getValue());
587+
juce::String selectedName;
588+
if (selectedRow > 0) {
589+
auto lookup = secondaryMidiOutList_->lookup();
590+
auto entry = lookup.find(selectedRow);
591+
if (entry == lookup.end()) {
592+
secondaryMidiOutList_->value().setValue(0);
593+
return;
594+
}
595+
selectedName = juce::String(entry->second);
596+
}
597+
598+
{
599+
std::scoped_lock lock(secondaryMidiOutMutex_);
600+
secondaryMidiOutName_ = juce::String(selectedName);
601+
}
602+
}

The-Orm/KeyboardMacroView.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@
1515
#include "MidiChannelPropertyEditor.h"
1616
#include "ElectraOneRouter.h"
1717

18+
#include <mutex>
1819

1920
class KeyboardMacroView : public Component, private ChangeListener, private Value::Listener {
2021
public:
2122
KeyboardMacroView(std::function<void(KeyboardMacroEvent)> callback);
2223
virtual ~KeyboardMacroView() override;
2324

2425
virtual void resized() override;
26+
void handleMidiMessage(const MidiMessage& message, const String& source, bool isOut);
2527

2628
private:
2729
class RecordProgress;
@@ -30,6 +32,8 @@ class KeyboardMacroView : public Component, private ChangeListener, private Valu
3032
void setupKeyboardControl();
3133
void loadFromSettings();
3234
void saveSettings();
35+
void refreshSecondaryMidiOutList();
36+
void updateSecondaryMidiOutSelection();
3337
bool isMacroState(KeyboardMacro const &macro);
3438
void refreshSynthList();
3539
void refreshUI();
@@ -43,6 +47,7 @@ class KeyboardMacroView : public Component, private ChangeListener, private Valu
4347
MidiKeyboardState state_;
4448
MidiKeyboardComponent keyboard_;
4549
std::shared_ptr<MidiDevicePropertyEditor> midiDeviceList_; // Listen to this to get notified of newly available devices!
50+
std::shared_ptr<MidiDevicePropertyEditor> secondaryMidiOutList_;
4651
ElectraOneRouter controllerRouter_;
4752
std::shared_ptr<TypedNamedValue> synthListEditor_;
4853

@@ -55,5 +60,7 @@ class KeyboardMacroView : public Component, private ChangeListener, private Valu
5560

5661
TypedNamedValueSet customMasterkeyboardSetup_;
5762
std::shared_ptr<RecordProgress> activeRecorder_; // Should have maximum one active macro recorders open
58-
};
5963

64+
std::mutex secondaryMidiOutMutex_;
65+
juce::String secondaryMidiOutName_;
66+
};

The-Orm/MainComponent.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,9 @@ MainComponent::MainComponent(bool makeYourOwnSize) :
510510

511511
// Install our MidiLogger
512512
midikraft::MidiController::instance()->setMidiLogFunction([this](const MidiMessage& message, const String& source, bool isOut) {
513+
if (keyboardView_) {
514+
keyboardView_->handleMidiMessage(message, source, isOut);
515+
}
513516
midiLogView_.log().addMessageToList(message, source, isOut);
514517
});
515518

@@ -1096,4 +1099,4 @@ void MainComponent::openSecondMainWindow(bool fromSettings)
10961099
}
10971100
}
10981101

1099-
std::unique_ptr<SecondaryMainWindow> MainComponent::sSecondMainWindow;
1102+
std::unique_ptr<SecondaryMainWindow> MainComponent::sSecondMainWindow;

0 commit comments

Comments
 (0)