1616#include < spdlog/spdlog.h>
1717#include " SpdLogJuce.h"
1818#include < string>
19+ #include < algorithm>
20+ #include < cctype>
1921
2022// Standardize text
2123const char *kMacrosEnabled = " Macros enabled" ;
@@ -24,6 +26,7 @@ const char *kRouteMasterkeyboard = "Forward MIDI to synth";
2426const char *kFixedSynthSelected = " Fixed synth played" ;
2527const char *kFixedSynthSelectedSettingKey = " Fixed synth played name" ;
2628const char *kUseElectraOne = " Forward Electra One" ;
29+ const char * kSecondaryMIDIOut = " Secondary MIDI OUT" ;
2730const char *kInputDevice = " MIDI Input Device" ;
2831const char *kInputDeviceSettingKey = " MIDI Input Device Name" ;
2932const char *kMidiChannel = " MIDI channel" ;
@@ -203,6 +206,7 @@ KeyboardMacroView::~KeyboardMacroView()
203206void 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}
323357void 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
401454void 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 ¯o)
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+ }
0 commit comments