From 236e3fa711bc08a10367cbdb19a15e5e541e3360 Mon Sep 17 00:00:00 2001 From: Steven Wilson <2165238+mendeni@users.noreply.github.com> Date: Fri, 16 Jan 2026 08:30:04 -0500 Subject: [PATCH 01/20] Add OSCManager component --- CMakeLists.txt | 3 ++ Source/OSCManager.cpp | 77 +++++++++++++++++++++++++++++++ Source/OSCManager.h | 21 +++++++++ Source/SonobusPluginEditor.cpp | 2 +- Source/SonobusPluginProcessor.cpp | 18 ++++++++ Source/SonobusPluginProcessor.h | 4 ++ 6 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 Source/OSCManager.cpp create mode 100644 Source/OSCManager.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 9a7567a5a..a647d02d4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -280,6 +280,8 @@ function(sono_add_custom_plugin_target target_name product_name formats is_instr Source/MonitorDelayView.h Source/OptionsView.cpp Source/OptionsView.h + Source/OSCManager.cpp + Source/OSCManager.h Source/ParametricEqView.h Source/PeersContainerView.cpp Source/PeersContainerView.h @@ -614,6 +616,7 @@ function(sono_add_custom_plugin_target target_name product_name formats is_instr juce::juce_dsp juce::juce_cryptography juce::juce_audio_plugin_client + juce::juce_osc ff_meters diff --git a/Source/OSCManager.cpp b/Source/OSCManager.cpp new file mode 100644 index 000000000..6b50aef5e --- /dev/null +++ b/Source/OSCManager.cpp @@ -0,0 +1,77 @@ +#include "OSCManager.h" + +OSCManager::OSCManager() +{ + juce::Logger::writeToLog("OSCManager initialized."); +} + +OSCManager::~OSCManager() +{ + sender.disconnect(); + disconnect(); +} + +// Initialize Receiver +bool OSCManager::initializeReceiver(int port) +{ + if (!connect(port)) + { + juce::Logger::writeToLog("Receiver failed to connect on port " + juce::String(port)); + return false; + } + + addListener(this, "/volumeControl"); + juce::Logger::writeToLog("Receiver connected on port " + juce::String(port)); + return true; +} + +// Initialize Sender +bool OSCManager::initializeSender(const juce::String& targetIP, int port) +{ + senderConnected = sender.connect(targetIP, port); + if (!senderConnected) + { + juce::Logger::writeToLog("Sender failed to connect to " + targetIP + ":" + juce::String(port)); + } + return senderConnected; +} + +// Send Message +void OSCManager::sendMessage(const juce::String& address, const juce::var& value) +{ + if (senderConnected) + { + if (value.isDouble()) + { + sender.send(address, static_cast(value)); // Convert double to float + } + else if (value.isInt()) + { + sender.send(address, static_cast(value)); + } + else if (value.isString()) + { + sender.send(address, value.toString()); + } + else + { + juce::Logger::writeToLog("Unsupported var type in sendMessage for: " + address); + } + } + else + { + juce::Logger::writeToLog("OSC Sender is not connected! Cannot send message to: " + address); + } +} + +// Handle Received Messages +void OSCManager::oscMessageReceived(const juce::OSCMessage& message) +{ + juce::Logger::writeToLog("Incoming OSC message: " + message.getAddressPattern().toString()); + + if (message.getAddressPattern().toString() == "/OutGainSlider") + { + float volume = message[0].getFloat32(); + mOutGainSlider.setValue(volume); + } +} diff --git a/Source/OSCManager.h b/Source/OSCManager.h new file mode 100644 index 000000000..db6d4dcc4 --- /dev/null +++ b/Source/OSCManager.h @@ -0,0 +1,21 @@ +#pragma once + +#include + +class OSCManager : private juce::OSCReceiver, + private juce::OSCReceiver::ListenerWithOSCAddress +{ +public: + OSCManager(); + ~OSCManager(); + + bool initializeReceiver(int port); + bool initializeSender(const juce::String& targetIP, int port); + void sendMessage(const juce::String& address, const juce::var& value); + +private: + void oscMessageReceived(const juce::OSCMessage& message) override; + + juce::OSCSender sender; + bool senderConnected = false; // Track sender connection state +}; diff --git a/Source/SonobusPluginEditor.cpp b/Source/SonobusPluginEditor.cpp index 97f98182e..cbac2574f 100644 --- a/Source/SonobusPluginEditor.cpp +++ b/Source/SonobusPluginEditor.cpp @@ -5,6 +5,7 @@ #include "SonobusPluginProcessor.h" #include "SonobusPluginEditor.h" +#include "OSCManager.h" #include "BeatToggleGrid.h" @@ -6143,4 +6144,3 @@ void SonobusAudioProcessorEditor::SonobusMenuBarModel::menuItemSelected (int men } #endif } - diff --git a/Source/SonobusPluginProcessor.cpp b/Source/SonobusPluginProcessor.cpp index a12a044f5..0a36ea3a8 100644 --- a/Source/SonobusPluginProcessor.cpp +++ b/Source/SonobusPluginProcessor.cpp @@ -837,6 +837,24 @@ mState (*this, &mUndoManager, "SonoBusAoO", initializeAoo(); + // Initialize the OSC receiver + const int receivePort = 9000; + if (!oscManager.initializeReceiver(receivePort)) + { + juce::Logger::writeToLog("Failed to initialize OSC Receiver."); + } + + // Initialize the OSC sender + const juce::String targetIPAddress = "10.10.10.10"; // Replace with actual destination + const int targetPort = 8000; + if (!oscManager.initializeSender(targetIPAddress, targetPort)) + { + juce::Logger::writeToLog("Failed to initialize OSC Sender."); + } + + // Example: Send a test message + // oscManager.sendMessage("/testMessage", 123); + mFreshInit = false; // need to ensure this before loaddefaultpluginstate if (isplugin) { diff --git a/Source/SonobusPluginProcessor.h b/Source/SonobusPluginProcessor.h index 52065ef31..0c1aae103 100644 --- a/Source/SonobusPluginProcessor.h +++ b/Source/SonobusPluginProcessor.h @@ -22,6 +22,8 @@ #include "SoundboardChannelProcessor.h" +#include "OSCManager.h" + typedef MVerb MVerbFloat; namespace SonoAudio { @@ -834,6 +836,8 @@ class SonobusAudioProcessor : public AudioProcessor, public AudioProcessorValue private: //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SonobusAudioProcessor) + + OSCManager oscManager; struct PeerStateCache { From 1a0ac4302c3f0bc6df73e554bf37f3fcdb3b9483 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 13:12:23 -0500 Subject: [PATCH 02/20] Add generic control registration system for OSC receive on selected controls OSC receive for these controls: ``` /OutGainSlider /MainMuteButton /RecvSyncButton /OptionsMaxRecvPaddingSlider /OptionsRecStealth ``` --- Source/OSCManager.cpp | 40 ++++++++++++-- Source/OSCManager.h | 10 ++++ Source/OptionsView.h | 2 + Source/SonobusPluginEditor.cpp | 92 +++++++++++++++++++++++++++++++ Source/SonobusPluginProcessor.cpp | 13 +++-- Source/SonobusPluginProcessor.h | 2 + 6 files changed, 150 insertions(+), 9 deletions(-) diff --git a/Source/OSCManager.cpp b/Source/OSCManager.cpp index 6b50aef5e..8e01eccb6 100644 --- a/Source/OSCManager.cpp +++ b/Source/OSCManager.cpp @@ -20,6 +20,7 @@ bool OSCManager::initializeReceiver(int port) return false; } + // Legacy listener - keeping for backward compatibility addListener(this, "/volumeControl"); juce::Logger::writeToLog("Receiver connected on port " + juce::String(port)); return true; @@ -64,14 +65,43 @@ void OSCManager::sendMessage(const juce::String& address, const juce::var& value } } +// Register Control +void OSCManager::registerControl(const juce::String& address, ControlCallback callback) +{ + controlRegistry[address] = callback; + // Also register as a listener for this OSC address pattern + addListener(this, address); + juce::Logger::writeToLog("Registered control for OSC address: " + address); +} + +// Unregister Control +void OSCManager::unregisterControl(const juce::String& address) +{ + auto it = controlRegistry.find(address); + if (it != controlRegistry.end()) + { + controlRegistry.erase(it); + // Note: JUCE doesn't provide a way to remove a specific listener, + // but the callback will no longer be called since it's removed from the registry + juce::Logger::writeToLog("Unregistered control for OSC address: " + address); + } +} + // Handle Received Messages void OSCManager::oscMessageReceived(const juce::OSCMessage& message) { - juce::Logger::writeToLog("Incoming OSC message: " + message.getAddressPattern().toString()); - - if (message.getAddressPattern().toString() == "/OutGainSlider") + juce::String address = message.getAddressPattern().toString(); + juce::Logger::writeToLog("Incoming OSC message: " + address); + + // Check if a callback is registered for this address + auto it = controlRegistry.find(address); + if (it != controlRegistry.end()) + { + // Call the registered callback + it->second(message); + } + else { - float volume = message[0].getFloat32(); - mOutGainSlider.setValue(volume); + juce::Logger::writeToLog("No handler registered for OSC address: " + address); } } diff --git a/Source/OSCManager.h b/Source/OSCManager.h index db6d4dcc4..e91137fa1 100644 --- a/Source/OSCManager.h +++ b/Source/OSCManager.h @@ -1,6 +1,8 @@ #pragma once #include +#include +#include class OSCManager : private juce::OSCReceiver, private juce::OSCReceiver::ListenerWithOSCAddress @@ -12,10 +14,18 @@ class OSCManager : private juce::OSCReceiver, bool initializeReceiver(int port); bool initializeSender(const juce::String& targetIP, int port); void sendMessage(const juce::String& address, const juce::var& value); + + // Generic control registration + using ControlCallback = std::function; + void registerControl(const juce::String& address, ControlCallback callback); + void unregisterControl(const juce::String& address); private: void oscMessageReceived(const juce::OSCMessage& message) override; juce::OSCSender sender; bool senderConnected = false; // Track sender connection state + + // Control registry for dynamic OSC message handling + std::map controlRegistry; }; diff --git a/Source/OptionsView.h b/Source/OptionsView.h index b1fbdee24..64101c20d 100644 --- a/Source/OptionsView.h +++ b/Source/OptionsView.h @@ -68,6 +68,8 @@ public MultiTimer void showWarnings(); + Slider* getOptionsMaxRecvPaddingSlider() { return mOptionsMaxRecvPaddingSlider.get(); } + ToggleButton* getOptionsRecStealth() { return mOptionsRecStealth.get(); } std::function getAudioDeviceManager; // = []() { return 0; }; std::function getShouldOverrideSampleRateValue; // = []() { return 0; }; diff --git a/Source/SonobusPluginEditor.cpp b/Source/SonobusPluginEditor.cpp index b0868df86..aa2a21a4d 100644 --- a/Source/SonobusPluginEditor.cpp +++ b/Source/SonobusPluginEditor.cpp @@ -1366,6 +1366,90 @@ SonobusAudioProcessorEditor::SonobusAudioProcessorEditor (SonobusAudioProcessor& processor.addClientListener(this); processor.getTransportSource().addChangeListener (this); + + // Register OSC controls with the OSCManager + OSCManager& oscManager = processor.getOSCManager(); + + // Register OutGainSlider - updates slider value with a float + oscManager.registerControl("/OutGainSlider", [this](const juce::OSCMessage& message) { + if (message.size() > 0 && message[0].isFloat32()) { + float value = message[0].getFloat32(); + juce::MessageManager::callAsync([this, value]() { + if (mOutGainSlider) { + mOutGainSlider->setValue(value, juce::NotificationType::sendNotificationAsync); + } + }); + } + }); + + // Register MainMuteButton - toggles mute button state with a boolean + oscManager.registerControl("/MainMuteButton", [this](const juce::OSCMessage& message) { + if (message.size() > 0) { + bool muteState = false; + if (message[0].isInt32()) { + muteState = (message[0].getInt32() != 0); + } else if (message[0].isFloat32()) { + muteState = (message[0].getFloat32() != 0.0f); + } + juce::MessageManager::callAsync([this, muteState]() { + if (mMainMuteButton) { + mMainMuteButton->setToggleState(muteState, juce::NotificationType::sendNotificationAsync); + } + }); + } + }); + + // Register RecvSyncButton - triggers button click event (accepts only integer 1) + oscManager.registerControl("/RecvSyncButton", [this](const juce::OSCMessage& message) { + if (message.size() > 0 && message[0].isInt32()) { + int value = message[0].getInt32(); + if (value == 1) { + juce::MessageManager::callAsync([this]() { + if (mRecvSyncButton) { + buttonClicked(mRecvSyncButton.get()); + } + }); + } + } + }); + + // Register OptionsMaxRecvPaddingSlider - updates sync receive padding parameter directly + oscManager.registerControl("/OptionsMaxRecvPaddingSlider", [this](const juce::OSCMessage& message) { + if (message.size() > 0 && message[0].isFloat32()) { + float value = message[0].getFloat32(); + juce::MessageManager::callAsync([this, value]() { + // Set the parameter directly, which updates the processor's internal value + // and any UI controls (like the slider in Options view if open) + if (auto* param = processor.getValueTreeState().getParameter(SonobusAudioProcessor::paramMaxRecvPaddingMs)) { + float normalizedValue = param->convertTo0to1(value); + param->setValueNotifyingHost(normalizedValue); + } + }); + } + }); + + // Register OptionsRecStealth - toggles stealth recording state with a boolean + oscManager.registerControl("/OptionsRecStealth", [this](const juce::OSCMessage& message) { + if (message.size() > 0) { + bool stealthState = false; + if (message[0].isInt32()) { + stealthState = (message[0].getInt32() != 0); + } else if (message[0].isFloat32()) { + stealthState = (message[0].getFloat32() != 0.0f); + } + juce::MessageManager::callAsync([this, stealthState]() { + // Set the processor value directly, which updates internal state + // and any UI controls (like the checkbox in Options view if open) + processor.setRecordStealth(stealthState); + // Also update the UI checkbox if Options view is open + if (mOptionsView) { + if (auto* checkbox = mOptionsView->getOptionsRecStealth()) { + checkbox->setToggleState(stealthState, juce::NotificationType::dontSendNotification); + } + } + }); + } + }); // handles registering commands updateUseKeybindings(); @@ -1399,6 +1483,14 @@ SonobusAudioProcessorEditor::SonobusAudioProcessorEditor (SonobusAudioProcessor& SonobusAudioProcessorEditor::~SonobusAudioProcessorEditor() { + // Unregister OSC controls to prevent use-after-free + OSCManager& oscManager = processor.getOSCManager(); + oscManager.unregisterControl("/OutGainSlider"); + oscManager.unregisterControl("/MainMuteButton"); + oscManager.unregisterControl("/RecvSyncButton"); + oscManager.unregisterControl("/OptionsMaxRecvPaddingSlider"); + oscManager.unregisterControl("/OptionsRecStealth"); + if (menuBarModel) { menuBarModel->setApplicationCommandManagerToWatch(nullptr); #if JUCE_MAC diff --git a/Source/SonobusPluginProcessor.cpp b/Source/SonobusPluginProcessor.cpp index ed756b14a..7f31a889a 100644 --- a/Source/SonobusPluginProcessor.cpp +++ b/Source/SonobusPluginProcessor.cpp @@ -843,22 +843,22 @@ mState (*this, &mUndoManager, "SonoBusAoO", initializeAoo(); // Initialize the OSC receiver - const int receivePort = 9000; + const int receivePort = 6000; if (!oscManager.initializeReceiver(receivePort)) { juce::Logger::writeToLog("Failed to initialize OSC Receiver."); } // Initialize the OSC sender - const juce::String targetIPAddress = "10.10.10.10"; // Replace with actual destination - const int targetPort = 8000; + const juce::String targetIPAddress = "127.0.0.1"; // Replace with actual destination + const int targetPort = 6001; if (!oscManager.initializeSender(targetIPAddress, targetPort)) { juce::Logger::writeToLog("Failed to initialize OSC Sender."); } // Example: Send a test message - // oscManager.sendMessage("/testMessage", 123); + oscManager.sendMessage("/testMessage", 123); mFreshInit = false; // need to ensure this before loaddefaultpluginstate @@ -8457,6 +8457,11 @@ AudioProcessorValueTreeState& SonobusAudioProcessor::getValueTreeState() return mState; } +OSCManager& SonobusAudioProcessor::getOSCManager() +{ + return oscManager; +} + ValueTree AooServerConnectionInfo::getValueTree() const { ValueTree item(recentsItemKey); diff --git a/Source/SonobusPluginProcessor.h b/Source/SonobusPluginProcessor.h index 833e93b51..4ce8eee1f 100644 --- a/Source/SonobusPluginProcessor.h +++ b/Source/SonobusPluginProcessor.h @@ -236,6 +236,8 @@ class SonobusAudioProcessor : public AudioProcessor, public AudioProcessorValue bool loadDefaultPluginSettings(); AudioProcessorValueTreeState& getValueTreeState(); + + OSCManager& getOSCManager(); static String paramInGain; static String paramInMonitorMonoPan; From c1bba80a8a838dc0225a6e8b9658b117d4779a42 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 22:10:29 -0500 Subject: [PATCH 03/20] Extend OSC functionality with configurable targets, outbound messages, and global enable/disable (#15) * Add OSC configuration storage and UI elements * Added outbound OSC messages for UI value changes mMainMuteButton mOutGainSlider mRecvSyncButton mOptionsRecStealth --- Source/OSCManager.cpp | 15 +++ Source/OSCManager.h | 2 + Source/OptionsView.cpp | 155 ++++++++++++++++++++++++++++++ Source/OptionsView.h | 13 +++ Source/SonobusPluginEditor.cpp | 30 ++++++ Source/SonobusPluginProcessor.cpp | 88 ++++++++++++++--- Source/SonobusPluginProcessor.h | 19 ++++ 7 files changed, 307 insertions(+), 15 deletions(-) diff --git a/Source/OSCManager.cpp b/Source/OSCManager.cpp index 8e01eccb6..5e4640e5a 100644 --- a/Source/OSCManager.cpp +++ b/Source/OSCManager.cpp @@ -37,6 +37,21 @@ bool OSCManager::initializeSender(const juce::String& targetIP, int port) return senderConnected; } +// Disconnect Receiver +void OSCManager::disconnectReceiver() +{ + disconnect(); + juce::Logger::writeToLog("OSC Receiver disconnected."); +} + +// Disconnect Sender +void OSCManager::disconnectSender() +{ + sender.disconnect(); + senderConnected = false; + juce::Logger::writeToLog("OSC Sender disconnected."); +} + // Send Message void OSCManager::sendMessage(const juce::String& address, const juce::var& value) { diff --git a/Source/OSCManager.h b/Source/OSCManager.h index e91137fa1..19cf21e88 100644 --- a/Source/OSCManager.h +++ b/Source/OSCManager.h @@ -13,6 +13,8 @@ class OSCManager : private juce::OSCReceiver, bool initializeReceiver(int port); bool initializeSender(const juce::String& targetIP, int port); + void disconnectReceiver(); + void disconnectSender(); void sendMessage(const juce::String& address, const juce::var& value); // Generic control registration diff --git a/Source/OptionsView.cpp b/Source/OptionsView.cpp index 75e8062bb..e31d53b1d 100644 --- a/Source/OptionsView.cpp +++ b/Source/OptionsView.cpp @@ -367,6 +367,46 @@ OptionsView::OptionsView(SonobusAudioProcessor& proc, std::functionsetJustificationType(Justification::centredLeft); + // OSC Enable toggle + mOSCEnabledButton = std::make_unique(TRANS("Enable OSC")); + mOSCEnabledButton->addListener(this); + mOSCEnabledButton->setTooltip(TRANS("Enable or disable OSC (Open Sound Control) functionality")); + + // OSC Configuration UI elements + mOSCTargetIPAddressLabel = std::make_unique