From 67650b682ecf8794a53d3162acb03f2bd3f4f6d2 Mon Sep 17 00:00:00 2001 From: James H Ball Date: Sat, 3 May 2025 11:57:14 +0100 Subject: [PATCH] Refactor how syphon/spout input works to be cleaner and less buggy --- Source/MainComponent.cpp | 16 +----- Source/PluginEditor.cpp | 59 ++++++++++++---------- Source/PluginEditor.h | 33 +++++++----- Source/PluginProcessor.cpp | 48 +----------------- Source/PluginProcessor.h | 11 +--- Source/SettingsComponent.cpp | 2 +- Source/components/OsciMainMenuBarModel.cpp | 13 ++--- Source/video/SyphonFrameGrabber.h | 6 ++- osci-render.jucer | 2 +- 9 files changed, 71 insertions(+), 119 deletions(-) diff --git a/Source/MainComponent.cpp b/Source/MainComponent.cpp index 5f19b8da..6890e4eb 100644 --- a/Source/MainComponent.cpp +++ b/Source/MainComponent.cpp @@ -22,9 +22,6 @@ MainComponent::MainComponent(OscirenderAudioProcessor& p, OscirenderAudioProcess juce::FileBrowserComponent::canSelectFiles; chooser->launchAsync(flags, [this](const juce::FileChooser& chooser) { -#if (JUCE_MAC || JUCE_WINDOWS) && OSCI_PREMIUM - juce::SpinLock::ScopedLockType syphonLock(audioProcessor.syphonLock); -#endif juce::SpinLock::ScopedLockType parsersLock(audioProcessor.parsersLock); bool fileAdded = false; for (auto& file : chooser.getResults()) { @@ -66,9 +63,6 @@ MainComponent::MainComponent(OscirenderAudioProcessor& p, OscirenderAudioProcess addAndMakeVisible(leftArrow); leftArrow.onClick = [this] { -#if (JUCE_MAC || JUCE_WINDOWS) && OSCI_PREMIUM - juce::SpinLock::ScopedLockType lock(audioProcessor.syphonLock); -#endif juce::SpinLock::ScopedLockType parserLock(audioProcessor.parsersLock); juce::SpinLock::ScopedLockType effectsLock(audioProcessor.effectsLock); @@ -83,9 +77,6 @@ MainComponent::MainComponent(OscirenderAudioProcessor& p, OscirenderAudioProcess addAndMakeVisible(rightArrow); rightArrow.onClick = [this] { -#if (JUCE_MAC || JUCE_WINDOWS) && OSCI_PREMIUM - juce::SpinLock::ScopedLockType lock(audioProcessor.syphonLock); -#endif juce::SpinLock::ScopedLockType parserLock(audioProcessor.parsersLock); juce::SpinLock::ScopedLockType effectsLock(audioProcessor.effectsLock); @@ -108,9 +99,6 @@ MainComponent::MainComponent(OscirenderAudioProcessor& p, OscirenderAudioProcess addAndMakeVisible(createFile); createFile.onClick = [this] { -#if (JUCE_MAC || JUCE_WINDOWS) && OSCI_PREMIUM - juce::SpinLock::ScopedLockType syphonLock(audioProcessor.syphonLock); -#endif juce::SpinLock::ScopedLockType parsersLock(audioProcessor.parsersLock); auto fileNameText = fileName.getText(); auto fileTypeText = fileType.getText(); @@ -173,8 +161,8 @@ void MainComponent::updateFileLabel() { { #if (JUCE_MAC || JUCE_WINDOWS) && OSCI_PREMIUM - if (audioProcessor.isSyphonInputActive()) { - fileLabel.setText(audioProcessor.getSyphonSourceName(), juce::dontSendNotification); + if (audioProcessor.syphonInputActive) { + fileLabel.setText(pluginEditor.getSyphonSourceName(), juce::dontSendNotification); } else #endif if (audioProcessor.objectServerRendering) { diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index f3d370b3..be8d2943 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -54,9 +54,6 @@ OscirenderAudioProcessorEditor::OscirenderAudioProcessorEditor(OscirenderAudioPr colourScheme = lookAndFeel.getDefaultColourScheme(); { -#if (JUCE_MAC || JUCE_WINDOWS) && OSCI_PREMIUM - juce::SpinLock::ScopedLockType syphonLock(audioProcessor.syphonLock); -#endif juce::SpinLock::ScopedLockType lock(audioProcessor.parsersLock); initialiseCodeEditors(); } @@ -143,9 +140,6 @@ void OscirenderAudioProcessorEditor::filesDropped(const juce::StringArray& files if (file.hasFileExtension("osci")) { openProject(file); } else { -#if (JUCE_MAC || JUCE_WINDOWS) && OSCI_PREMIUM - juce::SpinLock::ScopedLockType syphonLock(audioProcessor.syphonLock); -#endif juce::SpinLock::ScopedLockType parsersLock(audioProcessor.parsersLock); juce::SpinLock::ScopedLockType effectsLock(audioProcessor.effectsLock); @@ -379,9 +373,6 @@ void OscirenderAudioProcessorEditor::handleAsyncUpdate() { void OscirenderAudioProcessorEditor::changeListenerCallback(juce::ChangeBroadcaster* source) { if (source == &audioProcessor.broadcaster) { { -#if (JUCE_MAC || JUCE_WINDOWS) && OSCI_PREMIUM - juce::SpinLock::ScopedLockType syphonLock(audioProcessor.syphonLock); -#endif juce::SpinLock::ScopedLockType parsersLock(audioProcessor.parsersLock); initialiseCodeEditors(); settings.update(); @@ -389,9 +380,6 @@ void OscirenderAudioProcessorEditor::changeListenerCallback(juce::ChangeBroadcas resized(); repaint(); } else if (source == &audioProcessor.fileChangeBroadcaster) { -#if (JUCE_MAC || JUCE_WINDOWS) && OSCI_PREMIUM - juce::SpinLock::ScopedLockType syphonLock(audioProcessor.syphonLock); -#endif juce::SpinLock::ScopedLockType parsersLock(audioProcessor.parsersLock); // triggered when the audioProcessor changes the current file (e.g. to Blender) settings.fileUpdated(audioProcessor.getCurrentFileName()); @@ -462,9 +450,6 @@ void OscirenderAudioProcessorEditor::updateCodeDocument() { bool OscirenderAudioProcessorEditor::keyPressed(const juce::KeyPress& key) { bool consumeKey = false; { -#if (JUCE_MAC || JUCE_WINDOWS) && OSCI_PREMIUM - juce::SpinLock::ScopedLockType lock(audioProcessor.syphonLock); -#endif juce::SpinLock::ScopedLockType parserLock(audioProcessor.parsersLock); juce::SpinLock::ScopedLockType effectsLock(audioProcessor.effectsLock); @@ -527,13 +512,12 @@ void OscirenderAudioProcessorEditor::openVisualiserSettings() { void OscirenderAudioProcessorEditor::openSyphonInputDialog() { SyphonInputSelectorComponent* selector = nullptr; { - juce::SpinLock::ScopedLockType lock(audioProcessor.syphonLock); selector = new SyphonInputSelectorComponent( sharedTextureManager, - [this](const juce::String& server, const juce::String& app) { onSyphonInputSelected(server, app); }, - [this]() { onSyphonInputDisconnected(); }, - audioProcessor.isSyphonInputActive(), - audioProcessor.getSyphonSourceName()); + [this](const juce::String& server, const juce::String& app) { connectSyphonInput(server, app); }, + [this]() { disconnectSyphonInput(); }, + syphonFrameGrabber && syphonFrameGrabber->isActive(), + getSyphonSourceName()); } juce::DialogWindow::LaunchOptions options; options.content.setOwned(selector); @@ -546,13 +530,36 @@ void OscirenderAudioProcessorEditor::openSyphonInputDialog() { options.launchAsync(); } -void OscirenderAudioProcessorEditor::onSyphonInputSelected(const juce::String& server, const juce::String& app) { - juce::SpinLock::ScopedLockType lock(audioProcessor.syphonLock); - audioProcessor.connectSyphonInput(server, app); +void OscirenderAudioProcessorEditor::connectSyphonInput(const juce::String& server, const juce::String& app) { + juce::SpinLock::ScopedLockType lock(syphonLock); + if (!syphonFrameGrabber) { + syphonFrameGrabber = std::make_unique(sharedTextureManager, server, app, audioProcessor.syphonImageParser); + audioProcessor.syphonInputActive = true; + { + juce::MessageManagerLock lock; + audioProcessor.fileChangeBroadcaster.sendChangeMessage(); + } + } } -void OscirenderAudioProcessorEditor::onSyphonInputDisconnected() { - juce::SpinLock::ScopedLockType lock(audioProcessor.syphonLock); - audioProcessor.disconnectSyphonInput(); +void OscirenderAudioProcessorEditor::disconnectSyphonInput() { + juce::SpinLock::ScopedLockType lock(syphonLock); + if (!syphonFrameGrabber) { + return; + } + audioProcessor.syphonInputActive = false; + syphonFrameGrabber.reset(); + { + juce::MessageManagerLock lock; + audioProcessor.fileChangeBroadcaster.sendChangeMessage(); + } +} + +juce::String OscirenderAudioProcessorEditor::getSyphonSourceName() const { + juce::SpinLock::ScopedLockType lock(syphonLock); + if (syphonFrameGrabber) { + return syphonFrameGrabber->getSourceName(); + } + return ""; } #endif diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index ce148b8c..5d6379be 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -1,15 +1,16 @@ #pragma once #include + +#include "CommonPluginEditor.h" +#include "LookAndFeel.h" +#include "MidiComponent.h" #include "PluginProcessor.h" #include "SettingsComponent.h" -#include "MidiComponent.h" -#include "components/OsciMainMenuBarModel.h" -#include "LookAndFeel.h" #include "components/ErrorCodeEditorComponent.h" #include "components/LuaConsole.h" +#include "components/OsciMainMenuBarModel.h" #include "visualiser/VisualiserSettings.h" -#include "CommonPluginEditor.h" class OscirenderAudioProcessorEditor : public CommonPluginEditor, private juce::CodeDocument::Listener, public juce::AsyncUpdater, public juce::ChangeListener, public juce::FileDragAndDropTarget { public: @@ -18,7 +19,7 @@ public: void paint(juce::Graphics&) override; void resized() override; - + bool isBinaryFile(juce::String name); void initialiseCodeEditors(); void addCodeEditor(int index); @@ -37,15 +38,15 @@ private: void registerFileRemovedCallback(); OscirenderAudioProcessor& audioProcessor; -public: +public: const double CLOSED_PREF_SIZE = 30.0; const double RESIZER_BAR_SIZE = 7.0; std::atomic editingCustomFunction = false; SettingsComponent settings{audioProcessor, *this}; - + #if !OSCI_PREMIUM juce::TextButton upgradeButton{"Upgrade to premium!"}; #endif @@ -62,7 +63,7 @@ public: juce::CodeEditorComponent::ColourScheme colourScheme; juce::LuaTokeniser luaTokeniser; juce::XmlTokeniser xmlTokeniser; - juce::ShapeButton collapseButton; + juce::ShapeButton collapseButton; std::shared_ptr customFunctionCodeDocument = std::make_shared(); std::shared_ptr customFunctionCodeEditor = std::make_shared(*customFunctionCodeDocument, &luaTokeniser, audioProcessor, CustomEffect::UNIQUE_ID, CustomEffect::FILE_NAME); @@ -76,8 +77,8 @@ public: std::atomic updatingDocumentsWithParserLock = false; - void codeDocumentTextInserted(const juce::String& newText, int insertIndex) override; - void codeDocumentTextDeleted(int startIndex, int endIndex) override; + void codeDocumentTextInserted(const juce::String& newText, int insertIndex) override; + void codeDocumentTextDeleted(int startIndex, int endIndex) override; void updateCodeDocument(); void updateCodeEditor(bool binaryFile, bool shouldOpenEditor = false); void setCodeEditorVisible(std::optional visible); @@ -86,10 +87,16 @@ public: void mouseDown(const juce::MouseEvent& event) override; void mouseMove(const juce::MouseEvent& event) override; +#if (JUCE_MAC || JUCE_WINDOWS) && OSCI_PREMIUM // Syphon/Spout input dialog void openSyphonInputDialog(); - void onSyphonInputSelected(const juce::String& server, const juce::String& app); - void onSyphonInputDisconnected(); + void connectSyphonInput(const juce::String& server, const juce::String& app); + void disconnectSyphonInput(); + juce::String getSyphonSourceName() const; - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OscirenderAudioProcessorEditor) + juce::SpinLock syphonLock; + std::unique_ptr syphonFrameGrabber; +#endif + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(OscirenderAudioProcessorEditor) }; diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 1a17e447..c1160fc6 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -505,8 +505,7 @@ void OscirenderAudioProcessor::processBlock(juce::AudioBuffer& buffer, ju { #if (JUCE_MAC || JUCE_WINDOWS) && OSCI_PREMIUM - juce::SpinLock::ScopedLockType sLock(syphonLock); - if (isSyphonInputActive()) { + if (syphonInputActive) { for (int sample = 0; sample < outputBuffer3d.getNumSamples(); sample++) { osci::Point point = syphonImageParser.getSample(); outputBuffer3d.setSample(0, sample, point.x); @@ -514,7 +513,7 @@ void OscirenderAudioProcessor::processBlock(juce::AudioBuffer& buffer, ju } } else #endif - if (usingInput && totalNumInputChannels >= 1) { + if (usingInput && totalNumInputChannels >= 1) { if (totalNumInputChannels >= 2) { for (auto channel = 0; channel < juce::jmin(2, totalNumInputChannels); channel++) { outputBuffer3d.copyFrom(channel, 0, inputBuffer, channel, 0, buffer.getNumSamples()); @@ -898,49 +897,6 @@ void OscirenderAudioProcessor::envelopeChanged(EnvelopeComponent* changedEnvelop } } -#if (JUCE_MAC || JUCE_WINDOWS) && OSCI_PREMIUM -// Syphon/Spout input management - -// syphonLock must be held when calling this function -bool OscirenderAudioProcessor::isSyphonInputActive() const { - return syphonFrameGrabber != nullptr && syphonFrameGrabber->isActive(); -} - -// syphonLock must be held when calling this function -bool OscirenderAudioProcessor::isSyphonInputStarted() const { - return syphonFrameGrabber != nullptr; -} - -// syphonLock must be held when calling this function -void OscirenderAudioProcessor::connectSyphonInput(const juce::String& server, const juce::String& app) { - auto editor = dynamic_cast(getActiveEditor()); - if (!syphonFrameGrabber && editor) { - syphonFrameGrabber = std::make_unique(editor->sharedTextureManager, server, app, syphonImageParser); - { - juce::MessageManagerLock lock; - fileChangeBroadcaster.sendChangeMessage(); - } - } -} - -// syphonLock must be held when calling this function -void OscirenderAudioProcessor::disconnectSyphonInput() { - syphonFrameGrabber.reset(); - { - juce::MessageManagerLock lock; - fileChangeBroadcaster.sendChangeMessage(); - } -} - -// syphonLock must be held when calling this function -juce::String OscirenderAudioProcessor::getSyphonSourceName() const { - if (syphonFrameGrabber) { - return syphonFrameGrabber->getSourceName(); - } - return ""; -} -#endif - juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() { return new OscirenderAudioProcessor(); } diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h index 1ea8fb67..cc314bde 100644 --- a/Source/PluginProcessor.h +++ b/Source/PluginProcessor.h @@ -282,19 +282,12 @@ private: #if (JUCE_MAC || JUCE_WINDOWS) && OSCI_PREMIUM public: - bool isSyphonInputActive() const; - bool isSyphonInputStarted() const; - void connectSyphonInput(const juce::String& server, const juce::String& app); - void disconnectSyphonInput(); - juce::String getSyphonSourceName() const; + std::atomic syphonInputActive = false; - juce::SpinLock syphonLock; - -private: ImageParser syphonImageParser = ImageParser(*this); - std::unique_ptr syphonFrameGrabber; #endif + //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(OscirenderAudioProcessor) }; diff --git a/Source/SettingsComponent.cpp b/Source/SettingsComponent.cpp index 42217e61..a5c3bcfa 100644 --- a/Source/SettingsComponent.cpp +++ b/Source/SettingsComponent.cpp @@ -84,7 +84,7 @@ void SettingsComponent::fileUpdated(juce::String fileName) { // Check if the file is an image based on extension or Syphon/Spout input bool isSyphonActive = false; #if (JUCE_MAC || JUCE_WINDOWS) && OSCI_PREMIUM - isSyphonActive = audioProcessor.isSyphonInputStarted(); + isSyphonActive = audioProcessor.syphonInputActive; #endif bool isImage = isSyphonActive || diff --git a/Source/components/OsciMainMenuBarModel.cpp b/Source/components/OsciMainMenuBarModel.cpp index 65adbc91..167d53d8 100644 --- a/Source/components/OsciMainMenuBarModel.cpp +++ b/Source/components/OsciMainMenuBarModel.cpp @@ -64,11 +64,12 @@ OsciMainMenuBarModel::OsciMainMenuBarModel(OscirenderAudioProcessor& p, Oscirend #if (JUCE_MAC || JUCE_WINDOWS) && OSCI_PREMIUM // Add Syphon/Spout input menu item under Recording - addMenuItem(2, audioProcessor.isSyphonInputActive() ? "Disconnect Syphon/Spout Input" : "Select Syphon/Spout Input...", [this] { - if (audioProcessor.isSyphonInputActive()) - disconnectSyphonInput(); - else + addMenuItem(2, audioProcessor.syphonInputActive ? "Disconnect Syphon/Spout Input" : "Select Syphon/Spout Input...", [this] { + if (audioProcessor.syphonInputActive) { + editor.disconnectSyphonInput(); + } else { openSyphonInputDialog(); + } }); #endif @@ -83,8 +84,4 @@ OsciMainMenuBarModel::OsciMainMenuBarModel(OscirenderAudioProcessor& p, Oscirend void OsciMainMenuBarModel::openSyphonInputDialog() { editor.openSyphonInputDialog(); } - -void OsciMainMenuBarModel::disconnectSyphonInput() { - audioProcessor.disconnectSyphonInput(); -} #endif diff --git a/Source/video/SyphonFrameGrabber.h b/Source/video/SyphonFrameGrabber.h index 3cde3373..abe599dd 100644 --- a/Source/video/SyphonFrameGrabber.h +++ b/Source/video/SyphonFrameGrabber.h @@ -5,7 +5,7 @@ class SyphonFrameGrabber : private juce::Thread, public juce::Component { public: - SyphonFrameGrabber(SharedTextureManager& manager, juce::String server, juce::String app, ImageParser& parser, int pollMs = 16) + SyphonFrameGrabber(SharedTextureManager& manager, juce::String server, juce::String app, ImageParser& parser, int pollMs = 8) : juce::Thread("SyphonFrameGrabber"), pollIntervalMs(pollMs), manager(manager), parser(parser) { // Create the invisible OpenGL context component glContextComponent = std::make_unique(); @@ -30,6 +30,8 @@ public: } ~SyphonFrameGrabber() override { + juce::CriticalSection::ScopedLockType lock(openGLLock); + stopThread(500); if (receiver) { manager.removeReceiver(receiver); @@ -41,6 +43,8 @@ public: void run() override { while (!threadShouldExit()) { { + juce::CriticalSection::ScopedLockType lock(openGLLock); + bool activated = false; if (glContextComponent) { activated = glContextComponent->getContext().makeActive(); diff --git a/osci-render.jucer b/osci-render.jucer index 3089e7f9..08585079 100644 --- a/osci-render.jucer +++ b/osci-render.jucer @@ -4,7 +4,7 @@ addUsingNamespaceToJuceHeader="0" jucerFormatVersion="1" pluginCharacteristicsValue="pluginWantsMidiIn" pluginManufacturer="jameshball" aaxIdentifier="sh.ball.oscirender" cppLanguageStandard="20" projectLineFeed=" " headerPath="./include" - version="2.5.0.0" companyName="James H Ball" companyWebsite="https://osci-render.com" + version="2.5.0.1" companyName="James H Ball" companyWebsite="https://osci-render.com" companyEmail="james@ball.sh" defines="NOMINMAX=1 INTERNET_FLAG_NO_AUTO_REDIRECT=0 OSCI_PREMIUM=1 JUCE_USE_CUSTOM_PLUGIN_STANDALONE_APP=1 JUCE_MODAL_LOOPS_PERMITTED=1" pluginAUMainType="'aumf'">