diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4d16c389c..8d1d26d26 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,26 +27,23 @@ jobs: if: runner.os == 'Windows' run: cmake -S . -B build -DJUCE_ENABLE_STANDALONE_APPLICATION:BOOL=ON -DJUCE_ASIO:BOOL=ON - - name: Configure CMake / Setup (Linux & macOS) + - name: Configure CMake (Linux & macOS) if: runner.os != 'Windows' - run: | - ./setupcmake.sh + run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release - - name: Build (Windows) - if: runner.os == 'Windows' + - name: Build run: | cmake --build build --config Release --target SonoBusMendeni_Standalone cmake --build build --config Release --target SonoBusMendeni_VST3 - - name: Build (Linux & macOS) - if: runner.os != 'Windows' + - name: Stage TouchOSC run: | - ./buildcmake.sh + cp util/SonoBusMendeni.tosc build/SonoBusMendeni_artefacts/Release - name: Stage VST3 Install Script (macOS) if: runner.os == 'macOS' run: | - cp -a util/install-macos-vst3.sh build/SonoBusMendeni_artefacts/Release + cp util/install-macos-vst3.sh build/SonoBusMendeni_artefacts/Release - name: Upload Sonobus artifacts (Windows & Linux) if: runner.os != 'macOS' @@ -56,6 +53,7 @@ jobs: path: | build/SonoBusMendeni_artefacts/Release/Standalone build/SonoBusMendeni_artefacts/Release/VST3 + build/SonoBusMendeni_artefacts/Release/SonoBusMendeni.tosc - name: Upload Sonobus artifacts if: runner.os == 'macOS' @@ -66,3 +64,4 @@ jobs: build/SonoBusMendeni_artefacts/Release/Standalone build/SonoBusMendeni_artefacts/Release/VST3 build/SonoBusMendeni_artefacts/Release/install-macos-vst3.sh + build/SonoBusMendeni_artefacts/Release/SonoBusMendeni.tosc diff --git a/CMakeLists.txt b/CMakeLists.txt index 4fa4de43a..f03867e45 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/ChannelGroupsView.cpp b/Source/ChannelGroupsView.cpp index 5a7bd6172..fd341e4e8 100644 --- a/Source/ChannelGroupsView.cpp +++ b/Source/ChannelGroupsView.cpp @@ -254,6 +254,17 @@ void ChannelGroupEffectsView::compressorParamsChanged(CompressorView *comp, Sono if (wason != ison) { listeners.call (&ChannelGroupEffectsView::Listener::effectsEnableChanged, this); } + + // Send OSC feedback for peer compressor parameters (only for first 16 peers, channel group 0) + if (processor.getOSCEnabled() && peerIndex < 16 && groupIndex == 0) { + processor.getOSCManager().sendMessage("/Peer" + String(peerIndex + 1) + "CompressorEnable", params.enabled ? 1 : 0); + processor.getOSCManager().sendMessage("/Peer" + String(peerIndex + 1) + "CompressorThreshold", params.thresholdDb); + processor.getOSCManager().sendMessage("/Peer" + String(peerIndex + 1) + "CompressorRatio", params.ratio); + processor.getOSCManager().sendMessage("/Peer" + String(peerIndex + 1) + "CompressorAttack", params.attackMs); + processor.getOSCManager().sendMessage("/Peer" + String(peerIndex + 1) + "CompressorRelease", params.releaseMs); + processor.getOSCManager().sendMessage("/Peer" + String(peerIndex + 1) + "CompressorMakeupGain", params.makeupGainDb); + processor.getOSCManager().sendMessage("/Peer" + String(peerIndex + 1) + "CompressorAuto", params.automakeupGain ? 1 : 0); + } } else { bool wason = processor.getInputEffectsActive(groupIndex); @@ -264,6 +275,15 @@ void ChannelGroupEffectsView::compressorParamsChanged(CompressorView *comp, Sono if (wason != ison) { listeners.call (&ChannelGroupEffectsView::Listener::effectsEnableChanged, this); } + + // Send OSC updates for compressor parameters + processor.getOSCManager().sendMessage("/InputGroup" + String(groupIndex + 1) + "CompressorEnable", params.enabled ? 1.0f : 0.0f); + processor.getOSCManager().sendMessage("/InputGroup" + String(groupIndex + 1) + "CompressorThreshold", params.thresholdDb); + processor.getOSCManager().sendMessage("/InputGroup" + String(groupIndex + 1) + "CompressorRatio", params.ratio); + processor.getOSCManager().sendMessage("/InputGroup" + String(groupIndex + 1) + "CompressorAttack", params.attackMs); + processor.getOSCManager().sendMessage("/InputGroup" + String(groupIndex + 1) + "CompressorRelease", params.releaseMs); + processor.getOSCManager().sendMessage("/InputGroup" + String(groupIndex + 1) + "CompressorMakeupGain", params.makeupGainDb); + processor.getOSCManager().sendMessage("/InputGroup" + String(groupIndex + 1) + "CompressorAuto", params.automakeupGain ? 1.0f : 0.0f); } } @@ -279,6 +299,15 @@ void ChannelGroupEffectsView::expanderParamsChanged(ExpanderView *comp, SonoAudi if (wason != ison) { listeners.call (&ChannelGroupEffectsView::Listener::effectsEnableChanged, this); } + + // Send OSC feedback for peer expander parameters (only for first 16 peers, channel group 0) + if (processor.getOSCEnabled() && peerIndex < 16 && groupIndex == 0) { + processor.getOSCManager().sendMessage("/Peer" + String(peerIndex + 1) + "ExpanderEnable", params.enabled ? 1 : 0); + processor.getOSCManager().sendMessage("/Peer" + String(peerIndex + 1) + "ExpanderNoiseFloor", params.thresholdDb); + processor.getOSCManager().sendMessage("/Peer" + String(peerIndex + 1) + "ExpanderRatio", params.ratio); + processor.getOSCManager().sendMessage("/Peer" + String(peerIndex + 1) + "ExpanderAttack", params.attackMs); + processor.getOSCManager().sendMessage("/Peer" + String(peerIndex + 1) + "ExpanderRelease", params.releaseMs); + } } else { bool wason = processor.getInputEffectsActive(groupIndex); @@ -289,6 +318,13 @@ void ChannelGroupEffectsView::expanderParamsChanged(ExpanderView *comp, SonoAudi if (wason != ison) { listeners.call (&ChannelGroupEffectsView::Listener::effectsEnableChanged, this); } + + // Send OSC updates for expander (noise gate) parameters + processor.getOSCManager().sendMessage("/InputGroup" + String(groupIndex + 1) + "ExpanderEnable", params.enabled ? 1.0f : 0.0f); + processor.getOSCManager().sendMessage("/InputGroup" + String(groupIndex + 1) + "ExpanderNoiseFloor", params.thresholdDb); + processor.getOSCManager().sendMessage("/InputGroup" + String(groupIndex + 1) + "ExpanderRatio", params.ratio); + processor.getOSCManager().sendMessage("/InputGroup" + String(groupIndex + 1) + "ExpanderAttack", params.attackMs); + processor.getOSCManager().sendMessage("/InputGroup" + String(groupIndex + 1) + "ExpanderRelease", params.releaseMs); } } @@ -303,6 +339,21 @@ void ChannelGroupEffectsView::parametricEqParamsChanged(ParametricEqView *comp, if (wason != ison) { listeners.call (&ChannelGroupEffectsView::Listener::effectsEnableChanged, this); } + + // Send OSC feedback for peer EQ parameters (only for first 16 peers, channel group 0) + if (processor.getOSCEnabled() && peerIndex < 16 && groupIndex == 0) { + processor.getOSCManager().sendMessage("/Peer" + String(peerIndex + 1) + "EqEnable", params.enabled ? 1 : 0); + processor.getOSCManager().sendMessage("/Peer" + String(peerIndex + 1) + "EqLowShelfFreq", params.lowShelfFreq); + processor.getOSCManager().sendMessage("/Peer" + String(peerIndex + 1) + "EqLowShelfGain", params.lowShelfGain); + processor.getOSCManager().sendMessage("/Peer" + String(peerIndex + 1) + "EqPara1Freq", params.para1Freq); + processor.getOSCManager().sendMessage("/Peer" + String(peerIndex + 1) + "EqPara1Gain", params.para1Gain); + processor.getOSCManager().sendMessage("/Peer" + String(peerIndex + 1) + "EqPara1Q", params.para1Q); + processor.getOSCManager().sendMessage("/Peer" + String(peerIndex + 1) + "EqHighShelfFreq", params.highShelfFreq); + processor.getOSCManager().sendMessage("/Peer" + String(peerIndex + 1) + "EqHighShelfGain", params.highShelfGain); + processor.getOSCManager().sendMessage("/Peer" + String(peerIndex + 1) + "EqPara2Freq", params.para2Freq); + processor.getOSCManager().sendMessage("/Peer" + String(peerIndex + 1) + "EqPara2Gain", params.para2Gain); + processor.getOSCManager().sendMessage("/Peer" + String(peerIndex + 1) + "EqPara2Q", params.para2Q); + } } else { bool wason = processor.getInputEffectsActive(groupIndex); @@ -312,6 +363,19 @@ void ChannelGroupEffectsView::parametricEqParamsChanged(ParametricEqView *comp, if (wason != ison) { listeners.call (&ChannelGroupEffectsView::Listener::effectsEnableChanged, this); } + + // Send OSC updates for EQ parameters + processor.getOSCManager().sendMessage("/InputGroup" + String(groupIndex + 1) + "EqEnable", params.enabled ? 1.0f : 0.0f); + processor.getOSCManager().sendMessage("/InputGroup" + String(groupIndex + 1) + "EqLowShelfFreq", params.lowShelfFreq); + processor.getOSCManager().sendMessage("/InputGroup" + String(groupIndex + 1) + "EqLowShelfGain", params.lowShelfGain); + processor.getOSCManager().sendMessage("/InputGroup" + String(groupIndex + 1) + "EqPara1Freq", params.para1Freq); + processor.getOSCManager().sendMessage("/InputGroup" + String(groupIndex + 1) + "EqPara1Gain", params.para1Gain); + processor.getOSCManager().sendMessage("/InputGroup" + String(groupIndex + 1) + "EqPara1Q", params.para1Q); + processor.getOSCManager().sendMessage("/InputGroup" + String(groupIndex + 1) + "EqHighShelfFreq", params.highShelfFreq); + processor.getOSCManager().sendMessage("/InputGroup" + String(groupIndex + 1) + "EqHighShelfGain", params.highShelfGain); + processor.getOSCManager().sendMessage("/InputGroup" + String(groupIndex + 1) + "EqPara2Freq", params.para2Freq); + processor.getOSCManager().sendMessage("/InputGroup" + String(groupIndex + 1) + "EqPara2Gain", params.para2Gain); + processor.getOSCManager().sendMessage("/InputGroup" + String(groupIndex + 1) + "EqPara2Q", params.para2Q); } } @@ -326,6 +390,11 @@ void ChannelGroupEffectsView::reverbSendLevelChanged(ReverbSendView *comp, float if (wason != ison) { listeners.call (&ChannelGroupEffectsView::Listener::effectsEnableChanged, this); } + + // Send OSC feedback for peer input reverb send (only for first 16 peers, channel group 0) + if (processor.getOSCEnabled() && peerIndex < 16 && groupIndex == 0) { + processor.getOSCManager().sendMessage("/Peer" + String(peerIndex + 1) + "InputReverbSend", revlevel); + } } else { bool wason = processor.getInputEffectsActive(groupIndex); @@ -337,6 +406,9 @@ void ChannelGroupEffectsView::reverbSendLevelChanged(ReverbSendView *comp, float if (wason != ison) { listeners.call (&ChannelGroupEffectsView::Listener::effectsEnableChanged, this); } + + // Send OSC update for input reverb send + processor.getOSCManager().sendMessage("/InputGroup" + String(groupIndex + 1) + "InputReverbSend", revlevel); } } @@ -344,10 +416,18 @@ void ChannelGroupEffectsView::polarityInvertChanged(PolarityInvertView *comp, bo { if (peerMode) { processor.setRemotePeerPolarityInvert(peerIndex, groupIndex, polinv); + + // Send OSC feedback for peer polarity invert (only for first 16 peers, channel group 0) + if (processor.getOSCEnabled() && peerIndex < 16 && groupIndex == 0) { + processor.getOSCManager().sendMessage("/Peer" + String(peerIndex + 1) + "PolarityInvert", polinv ? 1 : 0); + } } else { // input mode processor.setInputPolarityInvert(groupIndex, polinv); + + // Send OSC update for polarity invert + processor.getOSCManager().sendMessage("/InputGroup" + String(groupIndex + 1) + "PolarityInvert", polinv ? 1.0f : 0.0f); } listeners.call (&ChannelGroupEffectsView::Listener::effectsEnableChanged, this); } @@ -503,6 +583,9 @@ void ChannelGroupMonitorEffectsView::updateStateForInput() { DelayParams monDelayParams; + // Set the group index for OSC addressing + delayView->setGroupIndex(groupIndex); + if (groupIndex == -1) { // met if (processor.getMetronomeMonitorDelayParams(monDelayParams)) { @@ -612,6 +695,11 @@ void ChannelGroupMonitorEffectsView::reverbSendLevelChanged(ReverbSendView *comp if (wason != ison) { listeners.call (&ChannelGroupMonitorEffectsView::Listener::monitorEffectsEnableChanged, this); } + + // Send OSC message for Input Group Mon Reverb Send change + if (groupIndex >= 0 && processor.getOSCEnabled()) { + processor.getOSCManager().sendMessage("/InputGroup" + String(groupIndex + 1) + "MonReverbSend", revlevel); + } } } @@ -652,6 +740,12 @@ void ChannelGroupMonitorEffectsView::monitorDelayParamsChanged(MonitorDelayView } else { wason = processor.getInputMonitorEffectsActive(groupIndex); processor.setInputMonitorDelayParams(groupIndex, params); + + // Send OSC messages for Input Group Mon Delay parameters + if (groupIndex >= 0 && processor.getOSCEnabled()) { + processor.getOSCManager().sendMessage("/InputGroup" + String(groupIndex + 1) + "MonDelayEnable", params.enabled ? 1.0f : 0.0f); + processor.getOSCManager().sendMessage("/InputGroup" + String(groupIndex + 1) + "MonDelayTime", params.delayTimeMs); + } } if (processor.getLinkMonitoringDelayTimes()) { @@ -682,6 +776,16 @@ void ChannelGroupMonitorEffectsView::monitorDelayParamsChanged(MonitorDelayView eparam.delayTimeMs = deltimems; processor.getSoundboardProcessor()->setMonitorDelayParams(eparam); } + + // Send OSC message for Link Delay Time (global setting) + if (processor.getOSCEnabled()) { + processor.getOSCManager().sendMessage("/InputGroup" + String(groupIndex + 1) + "MonDelayLink", 1.0f); + } + } else { + // Send OSC message for Link Delay Time being disabled + if (groupIndex >= 0 && processor.getOSCEnabled()) { + processor.getOSCManager().sendMessage("/InputGroup" + String(groupIndex + 1) + "MonDelayLink", 0.0f); + } } if (wason != ison) { @@ -961,6 +1065,10 @@ ChannelGroupsView::ChannelGroupsView(SonobusAudioProcessor& proc, bool peerMode, } else { showInputReverbView(false); } + // Send OSC message for InReverbButton click + if (processor.getOSCEnabled()) { + processor.getOSCManager().sendMessage("/InReverbButton", 1); + } }; mMonDelayButton = std::make_unique(TRANS("Monitor Delay")); @@ -969,6 +1077,10 @@ ChannelGroupsView::ChannelGroupsView(SonobusAudioProcessor& proc, bool peerMode, addChildComponent(mMonDelayButton.get()); mMonDelayButton->onClick = [this]() { toggleAllMonitorDelay(); + // Send OSC message for MonDelayButton state change + if (processor.getOSCEnabled()) { + processor.getOSCManager().sendMessage("/MonDelayButton", mMonDelayButton->getToggleState() ? 1 : 0); + } }; @@ -1688,10 +1800,18 @@ void ChannelGroupsView::rebuildChannelViews(bool notify) mMetChannelView->panSlider->onValueChange = [this]() { processor.setMetronomePan(mMetChannelView->panSlider->getValue()); + // Send OSC message for MetPanSlider value change + if (processor.getOSCEnabled()) { + processor.getOSCManager().sendMessage("/MetPanSlider", static_cast(mMetChannelView->panSlider->getValue())); + } }; mMetChannelView->monitorSlider->onValueChange = [this]() { processor.setMetronomeMonitor(mMetChannelView->monitorSlider->getValue()); + // Send OSC message for MetMonitorSlider value change + if (processor.getOSCEnabled()) { + processor.getOSCManager().sendMessage("/MetMonitorSlider", static_cast(mMetChannelView->monitorSlider->getValue())); + } }; setupChildren(mMetChannelView.get()); @@ -1761,6 +1881,10 @@ void ChannelGroupsView::rebuildChannelViews(bool notify) mFileChannelView->levelSlider->onValueChange = [this]() { processor.setFilePlaybackGain(mFileChannelView->levelSlider->getValue()); + // Send OSC message for File Playback Pre Level change + if (processor.getOSCEnabled()) { + processor.getOSCManager().sendMessage("/FilePlaybackPreLevel", static_cast(mFileChannelView->levelSlider->getValue())); + } }; //mFileChannelView->panSlider->onValueChange = [this]() { @@ -1769,6 +1893,10 @@ void ChannelGroupsView::rebuildChannelViews(bool notify) mFileChannelView->monitorSlider->onValueChange = [this]() { processor.setFilePlaybackMonitor(mFileChannelView->monitorSlider->getValue()); + // Send OSC message for FileMonitorSlider value change + if (processor.getOSCEnabled()) { + processor.getOSCManager().sendMessage("/FileMonitorSlider", static_cast(mFileChannelView->monitorSlider->getValue())); + } }; setupChildren(mFileChannelView.get()); @@ -1823,6 +1951,10 @@ void ChannelGroupsView::rebuildChannelViews(bool notify) mSoundboardChannelView->levelSlider->onValueChange = [this]() { processor.getSoundboardProcessor()->setGain(mSoundboardChannelView->levelSlider->getValue()); + // Send OSC message for SoundboardLevelSlider value change + if (processor.getOSCEnabled()) { + processor.getOSCManager().sendMessage("/SoundboardLevelSlider", static_cast(mSoundboardChannelView->levelSlider->getValue())); + } }; //mSoundboardChannelView->panSlider->onValueChange = [this]() { @@ -1831,6 +1963,10 @@ void ChannelGroupsView::rebuildChannelViews(bool notify) mSoundboardChannelView->monitorSlider->onValueChange = [this]() { processor.getSoundboardProcessor()->setMonitorGain(mSoundboardChannelView->monitorSlider->getValue()); + // Send OSC message for SoundboardMonitorSlider value change + if (processor.getOSCEnabled()) { + processor.getOSCManager().sendMessage("/SoundboardMonitorSlider", static_cast(mSoundboardChannelView->monitorSlider->getValue())); + } }; setupChildren(mSoundboardChannelView.get()); @@ -3521,6 +3657,7 @@ void ChannelGroupsView::buttonClicked (Button* buttonThatWasClicked) if (pvf->muteButton.get() == buttonThatWasClicked) { processor.setInputGroupMuted(changroup, buttonThatWasClicked->getToggleState()); + processor.getOSCManager().sendMessage("/InputGroup" + String(changroup + 1) + "Mute", buttonThatWasClicked->getToggleState() ? 1.0f : 0.0f); updateChannelViews(); break; } @@ -3554,6 +3691,8 @@ void ChannelGroupsView::buttonClicked (Button* buttonThatWasClicked) else { processor.setInputGroupSoloed(j, false); } + // Send OSC for each group + processor.getOSCManager().sendMessage("/InputGroup" + String(j + 1) + "Solo", (newsolo && changroup == j) ? 1.0f : 0.0f); } // change solo for main monitor too @@ -3563,6 +3702,7 @@ void ChannelGroupsView::buttonClicked (Button* buttonThatWasClicked) } else { bool newsolo = buttonThatWasClicked->getToggleState(); processor.setInputGroupSoloed (changroup, newsolo); + processor.getOSCManager().sendMessage("/InputGroup" + String(changroup + 1) + "Solo", newsolo ? 1.0f : 0.0f); //if (newsolo) { // only enable main in solo @@ -4758,6 +4898,11 @@ void ChannelGroupsView::sliderValueChanged (Slider* slider) if (slider == mMainChannelView->levelSlider.get()) { processor.setRemotePeerLevelGain(mPeerIndex, mMainChannelView->levelSlider->getValue()); + + // Send OSC feedback for peer level slider (only for first 16 peers) + if (processor.getOSCEnabled() && mPeerIndex < 16) { + processor.getOSCManager().sendMessage("/Peer" + String(mPeerIndex + 1) + "Level", static_cast(mMainChannelView->levelSlider->getValue())); + } return; } else if (slider == mMainChannelView->panSlider.get()) { @@ -4766,9 +4911,19 @@ void ChannelGroupsView::sliderValueChanged (Slider* slider) float pan2 = mMainChannelView->panSlider->getMaxValue(); processor.setRemotePeerChannelPan(mPeerIndex, changroup, 0, pan1); processor.setRemotePeerChannelPan(mPeerIndex, changroup, 1, pan2); + + // Send OSC feedback for pan (only for first 16 peers, using first channel value) + if (processor.getOSCEnabled() && mPeerIndex < 16) { + processor.getOSCManager().sendMessage("/Peer" + String(mPeerIndex + 1) + "Pan", static_cast(pan1)); + } } else { processor.setRemotePeerChannelPan(mPeerIndex, changroup, chi, mMainChannelView->panSlider->getValue()); + + // Send OSC feedback for pan (only for first 16 peers) + if (processor.getOSCEnabled() && mPeerIndex < 16) { + processor.getOSCManager().sendMessage("/Peer" + String(mPeerIndex + 1) + "Pan", static_cast(mMainChannelView->panSlider->getValue())); + } } return; } @@ -4815,10 +4970,18 @@ void ChannelGroupsView::sliderValueChanged (Slider* slider) if (pvf->levelSlider.get() == slider) { processor.setInputGroupGain(changroup, pvf->levelSlider->getValue()); + // Send OSC message for Input Group Pre Level change + if (processor.getOSCEnabled()) { + processor.getOSCManager().sendMessage("/InputGroup" + String(changroup + 1) + "PreLevel", static_cast(pvf->levelSlider->getValue())); + } break; } else if (pvf->monitorSlider.get() == slider) { processor.setInputMonitor(changroup, pvf->monitorSlider->getValue()); + // Send OSC message for Input Group Monitor change + if (processor.getOSCEnabled()) { + processor.getOSCManager().sendMessage("/InputGroup" + String(changroup + 1) + "Monitor", static_cast(pvf->monitorSlider->getValue())); + } break; } else if (pvf->panSlider.get() == slider) { @@ -4827,9 +4990,18 @@ void ChannelGroupsView::sliderValueChanged (Slider* slider) float pan2 = pvf->panSlider->getMaxValue(); processor.setInputChannelPan(changroup, 0, pan1); processor.setInputChannelPan(changroup, 1, pan2); + // Send OSC message for Input Group Pan (dual channel) + if (processor.getOSCEnabled()) { + processor.getOSCManager().sendMessage("/InputGroup" + String(changroup + 1) + "PanLeft", pan1); + processor.getOSCManager().sendMessage("/InputGroup" + String(changroup + 1) + "PanRight", pan2); + } } else { processor.setInputChannelPan(changroup, chi, pvf->panSlider->getValue()); + // Send OSC message for Input Group Pan (single channel) + if (processor.getOSCEnabled()) { + processor.getOSCManager().sendMessage("/InputGroup" + String(changroup + 1) + "Pan", static_cast(pvf->panSlider->getValue())); + } } break; } diff --git a/Source/ChannelGroupsView.h b/Source/ChannelGroupsView.h index f3fe3bb3a..bed6d2d50 100644 --- a/Source/ChannelGroupsView.h +++ b/Source/ChannelGroupsView.h @@ -143,6 +143,10 @@ public EffectsBaseView::HeaderListener void effectsHeaderClicked(EffectsBaseView *comp) override; + // Public accessors for OSC control + MonitorDelayView* getDelayView() { return delayView.get(); } + ReverbSendView* getReverbSendView() { return reverbSendView.get(); } + int groupIndex = 0; bool peerMode = false; @@ -373,6 +377,13 @@ public MultiTimer void applyToAllSliders(std::function & routine); void updateLayout(bool notify=true); + + // Getters for OSC access to nested controls + ChannelGroupView* getFileChannelView() { return mFileChannelView.get(); } + ChannelGroupView* getSoundboardChannelView() { return mSoundboardChannelView.get(); } + ChannelGroupView* getMetChannelView() { return mMetChannelView.get(); } + TextButton* getInReverbButton() { return mInReverbButton.get(); } + TextButton* getMonDelayButton() { return mMonDelayButton.get(); } std::function getAudioDeviceManager; // = []() { return 0; }; diff --git a/Source/MonitorDelayView.h b/Source/MonitorDelayView.h index cb284594c..ab91cf3f5 100644 --- a/Source/MonitorDelayView.h +++ b/Source/MonitorDelayView.h @@ -55,6 +55,11 @@ class MonitorDelayView : public EffectsBaseView, linkButton.setButtonText(TRANS("Link Delay Time with other inputs")); linkButton.onClick = [this]() { processor.setLinkMonitoringDelayTimes(linkButton.getToggleState()); + + // Send OSC message for link button state change (only for input groups >= 0) + if (processor.getOSCEnabled() && mGroupIndex >= 0) { + processor.getOSCManager().sendMessage("/InputGroup" + String(mGroupIndex + 1) + "MonDelayLink", linkButton.getToggleState() ? 1.0f : 0.0f); + } }; // these are in the header component @@ -189,6 +194,11 @@ class MonitorDelayView : public EffectsBaseView, if (buttonThatWasClicked == &enableButton) { mParams.enabled = enableButton.getToggleState(); headerComponent.repaint(); + + // Send OSC message for enable button state change (only for input groups >= 0) + if (processor.getOSCEnabled() && mGroupIndex >= 0) { + processor.getOSCManager().sendMessage("/InputGroup" + String(mGroupIndex + 1) + "MonDelayEnable", mParams.enabled ? 1.0f : 0.0f); + } } else if (buttonThatWasClicked == &autoButton) { auto autoScalar = autoModeChoice.getSelectedItemIndex() == 1 ? 2.0f : 1.0f; @@ -206,6 +216,11 @@ class MonitorDelayView : public EffectsBaseView, mParams.enabled = !enableButton.getToggleState(); //processor.setMonitoringDelayActive(!enableButton.getToggleState()); updateParams(mParams); + + // Send OSC message for enable button state change (only for input groups >= 0) + if (processor.getOSCEnabled() && mGroupIndex >= 0) { + processor.getOSCManager().sendMessage("/InputGroup" + String(mGroupIndex + 1) + "MonDelayEnable", mParams.enabled ? 1.0f : 0.0f); + } listeners.call (&MonitorDelayView::Listener::monitorDelayParamsChanged, this, mParams); } @@ -249,6 +264,10 @@ class MonitorDelayView : public EffectsBaseView, headerComponent.repaint(); } + // Setter for group index (needed for OSC addressing) + void setGroupIndex(int index) { mGroupIndex = index; } + int getGroupIndex() const { return mGroupIndex; } + private: @@ -274,6 +293,7 @@ class MonitorDelayView : public EffectsBaseView, FlexBox infoBox; SonoAudio::DelayParams mParams; + int mGroupIndex = 0; // Group index for OSC addressing JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MonitorDelayView) diff --git a/Source/OSCManager.cpp b/Source/OSCManager.cpp new file mode 100644 index 000000000..4702c42c1 --- /dev/null +++ b/Source/OSCManager.cpp @@ -0,0 +1,123 @@ +#include "OSCManager.h" + +OSCManager::OSCManager() +{ + // OSCManager object created, but not initialized until OSC is enabled +} + +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; + } + + // Legacy listener - keeping for backward compatibility + 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; +} + +// 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) +{ + if (senderConnected) + { + if (value.isDouble()) + { + // Apply OSC_SCALE_FACTOR to float/double values + sender.send(address, static_cast(value) * OSC_SCALE_FACTOR); + } + 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); + } +} + +// 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::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 + { + juce::Logger::writeToLog("No handler registered for OSC address: " + address); + } +} diff --git a/Source/OSCManager.h b/Source/OSCManager.h new file mode 100644 index 000000000..07fe06d77 --- /dev/null +++ b/Source/OSCManager.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include + +// Global constant for OSC message scaling factor +// Used to scale outbound float values for TouchOSC compatibility +constexpr float OSC_SCALE_FACTOR = 0.5f; + +// Inverse scale factor for incoming OSC messages +// Used to compensate for the outbound scaling on specific controls +constexpr float OSC_INVERSE_SCALE_FACTOR = 2.0f; + +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 disconnectReceiver(); + void disconnectSender(); + 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.cpp b/Source/OptionsView.cpp index 75e8062bb..bf1c7485c 100644 --- a/Source/OptionsView.cpp +++ b/Source/OptionsView.cpp @@ -2,6 +2,7 @@ // Copyright (C) 2021 Jesse Chappell #include "OptionsView.h" +#include "SonobusPluginEditor.h" #if JUCE_ANDROID #include "juce_core/native/juce_BasicNativeHeaders.h" @@ -334,6 +335,11 @@ OptionsView::OptionsView(SonobusAudioProcessor& proc, std::functiononValueChange = [this]() { auto thresh = 1.0 / jmax(1.0, mOptionsAutoDropThreshSlider->getValue()); processor.setAutoresizeBufferDropRateThreshold(thresh); + + // Send OSC message for OptionsAutoDropThreshSlider value change + if (processor.getOSCEnabled()) { + processor.getOSCManager().sendMessage("/OptionsAutoDropThreshSlider", static_cast(mOptionsAutoDropThreshSlider->getValue())); + } }; mOptionsAutoDropThreshSlider->setTooltip(TRANS("This controls how sensitive the auto-jitter buffer adjustment is when there are audio dropouts. The jitter buffer size will be increased if there are any dropouts within the number of seconds specified here. When this value is smaller it will be less likely to increase the jitter buffer size automatically.")); @@ -367,6 +373,50 @@ 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")); + + mOSCSendStateOnStartButton = std::make_unique(TRANS("Send state to target on start")); + mOSCSendStateOnStartButton->addListener(this); + mOSCSendStateOnStartButton->setTooltip(TRANS("When enabled, sends current values of all OSC-enabled controls to the target address when OSC is enabled")); + + // OSC Configuration UI elements + mOSCTargetIPAddressLabel = std::make_unique