Get initial working version of syphon input

pull/300/head
James H Ball 2025-04-26 20:30:10 +01:00
rodzic fb1db3c886
commit 590eace784
16 zmienionych plików z 393 dodań i 39 usunięć

Wyświetl plik

@ -0,0 +1,28 @@
#pragma once
#include <JuceHeader.h>
// An invisible component that owns a persistent OpenGL context, always attached to the desktop.
class InvisibleOpenGLContextComponent : public juce::Component {
public:
InvisibleOpenGLContextComponent() {
setSize(1, 1); // Minimal size
setBounds(0, 0, 1, 1); // Minimal bounds
setVisible(true);
setOpaque(false);
context.setComponentPaintingEnabled(false);
context.attachTo(*this);
addToDesktop(
juce::ComponentPeer::windowIsTemporary |
juce::ComponentPeer::windowIgnoresKeyPresses |
juce::ComponentPeer::windowIgnoresMouseClicks
);
}
~InvisibleOpenGLContextComponent() override {
context.detach();
}
juce::OpenGLContext& getContext() { return context; }
private:
juce::OpenGLContext context;
};

Wyświetl plik

@ -157,13 +157,18 @@ void MainComponent::updateFileLabel() {
showLeftArrow = audioProcessor.getCurrentFileIndex() > 0;
showRightArrow = audioProcessor.getCurrentFileIndex() < audioProcessor.numFiles() - 1;
if (audioProcessor.objectServerRendering) {
fileLabel.setText("Rendering from Blender", juce::dontSendNotification);
} else if (audioProcessor.getCurrentFileIndex() == -1) {
fileLabel.setText("No file open", juce::dontSendNotification);
} else {
fileLabel.setText(audioProcessor.getCurrentFileName(), juce::dontSendNotification);
}
{
juce::SpinLock::ScopedLockType lock(audioProcessor.syphonLock);
if (audioProcessor.objectServerRendering) {
fileLabel.setText("Rendering from Blender", juce::dontSendNotification);
} else if (audioProcessor.isSyphonInputActive()) {
fileLabel.setText(audioProcessor.getSyphonSourceName(), juce::dontSendNotification);
} else if (audioProcessor.getCurrentFileIndex() == -1) {
fileLabel.setText("No file open", juce::dontSendNotification);
} else {
fileLabel.setText(audioProcessor.getCurrentFileName(), juce::dontSendNotification);
}
}
resized();
}

Wyświetl plik

@ -1,6 +1,9 @@
#include "PluginProcessor.h"
#include "PluginEditor.h"
#include "CustomStandaloneFilterWindow.h"
#include "components/SyphonInputSelectorComponent.h"
#include "../modules/juce_sharedtexture/SharedTexture.h"
#include <memory>
void OscirenderAudioProcessorEditor::registerFileRemovedCallback() {
audioProcessor.setFileRemovedCallback([this](int index) {
@ -522,3 +525,38 @@ void OscirenderAudioProcessorEditor::openVisualiserSettings() {
visualiserSettingsWindow.setVisible(true);
visualiserSettingsWindow.toFront(true);
}
void OscirenderAudioProcessorEditor::openSyphonInputDialog() {
#if JUCE_MAC || JUCE_WINDOWS
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()
);
}
juce::DialogWindow::LaunchOptions options;
options.content.setOwned(selector);
options.content->setSize(350, 120);
options.dialogTitle = "Select Syphon/Spout Input";
options.dialogBackgroundColour = juce::Colours::darkgrey;
options.escapeKeyTriggersCloseButton = true;
options.useNativeTitleBar = true;
options.resizable = false;
options.launchAsync();
#endif
}
void OscirenderAudioProcessorEditor::onSyphonInputSelected(const juce::String& server, const juce::String& app) {
juce::SpinLock::ScopedLockType lock(audioProcessor.syphonLock);
audioProcessor.connectSyphonInput(server, app);
}
void OscirenderAudioProcessorEditor::onSyphonInputDisconnected() {
juce::SpinLock::ScopedLockType lock(audioProcessor.syphonLock);
audioProcessor.disconnectSyphonInput();
}

Wyświetl plik

@ -86,5 +86,10 @@ public:
void mouseDown(const juce::MouseEvent& event) override;
void mouseMove(const juce::MouseEvent& event) override;
// Syphon/Spout input dialog
void openSyphonInputDialog();
void onSyphonInputSelected(const juce::String& server, const juce::String& app);
void onSyphonInputDisconnected();
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OscirenderAudioProcessorEditor)
};

Wyświetl plik

@ -16,6 +16,12 @@
#include "audio/BitCrushEffect.h"
#include "audio/BulgeEffect.h"
#if JUCE_MAC || JUCE_WINDOWS
#include "SyphonFrameGrabber.h"
#include "img/ImageParser.h"
#include "../modules/juce_sharedtexture/SharedTexture.h"
#endif
//==============================================================================
OscirenderAudioProcessor::OscirenderAudioProcessor() : CommonAudioProcessor(BusesProperties().withInput("Input", juce::AudioChannelSet::namedChannelSet(2), true).withOutput("Output", juce::AudioChannelSet::stereo(), true)) {
// locking isn't necessary here because we are in the constructor
@ -507,32 +513,41 @@ void OscirenderAudioProcessor::processBlock(juce::AudioBuffer<float>& buffer, ju
juce::AudioBuffer<float> outputBuffer3d = juce::AudioBuffer<float>(3, buffer.getNumSamples());
outputBuffer3d.clear();
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());
{
juce::SpinLock::ScopedLockType sLock(syphonLock);
if (isSyphonInputActive()) {
for (int sample = 0; sample < outputBuffer3d.getNumSamples(); sample++) {
osci::Point point = syphonImageParser.getSample();
outputBuffer3d.setSample(0, sample, point.x);
outputBuffer3d.setSample(1, sample, point.y);
}
} else 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());
}
} else {
// For mono input, copy the single channel to both left and right
outputBuffer3d.copyFrom(0, 0, inputBuffer, 0, 0, buffer.getNumSamples());
outputBuffer3d.copyFrom(1, 0, inputBuffer, 0, 0, buffer.getNumSamples());
}
} else {
// For mono input, copy the single channel to both left and right
outputBuffer3d.copyFrom(0, 0, inputBuffer, 0, 0, buffer.getNumSamples());
outputBuffer3d.copyFrom(1, 0, inputBuffer, 0, 0, buffer.getNumSamples());
}
// handle all midi messages
auto midiIterator = midiMessages.cbegin();
std::for_each(midiIterator,
midiMessages.cend(),
[&] (const juce::MidiMessageMetadata& meta) { synth.publicHandleMidiEvent(meta.getMessage()); }
);
} else {
juce::SpinLock::ScopedLockType lock1(parsersLock);
juce::SpinLock::ScopedLockType lock2(effectsLock);
synth.renderNextBlock(outputBuffer3d, midiMessages, 0, buffer.getNumSamples());
for (int i = 0; i < synth.getNumVoices(); i++) {
auto voice = dynamic_cast<ShapeVoice*>(synth.getVoice(i));
if (voice->isVoiceActive()) {
customEffect->frequency = voice->getFrequency();
break;
// handle all midi messages
auto midiIterator = midiMessages.cbegin();
std::for_each(midiIterator,
midiMessages.cend(),
[&] (const juce::MidiMessageMetadata& meta) { synth.publicHandleMidiEvent(meta.getMessage()); }
);
} else {
juce::SpinLock::ScopedLockType lock1(parsersLock);
juce::SpinLock::ScopedLockType lock2(effectsLock);
synth.renderNextBlock(outputBuffer3d, midiMessages, 0, buffer.getNumSamples());
for (int i = 0; i < synth.getNumVoices(); i++) {
auto voice = dynamic_cast<ShapeVoice*>(synth.getVoice(i));
if (voice->isVoiceActive()) {
customEffect->frequency = voice->getFrequency();
break;
}
}
}
}
@ -542,7 +557,6 @@ void OscirenderAudioProcessor::processBlock(juce::AudioBuffer<float>& buffer, ju
auto* channelData = buffer.getArrayOfWritePointers();
for (int sample = 0; sample < buffer.getNumSamples(); ++sample) {
// Update frame animation
if (animateFrames->getBoolValue()) {
if (juce::JUCEApplicationBase::isStandaloneApp()) {
animationFrame = animationFrame + sTimeSec * animationRate->getValueUnnormalised();
@ -890,6 +904,49 @@ void OscirenderAudioProcessor::envelopeChanged(EnvelopeComponent* changedEnvelop
}
}
#if JUCE_MAC || JUCE_WINDOWS
// 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<OscirenderAudioProcessorEditor*>(getActiveEditor());
if (!syphonFrameGrabber && editor) {
syphonFrameGrabber = std::make_unique<SyphonFrameGrabber>(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();
}

Wyświetl plik

@ -25,6 +25,11 @@
#include "audio/CustomEffect.h"
#include "audio/DashedLineEffect.h"
#include "CommonPluginProcessor.h"
#include "SyphonFrameGrabber.h"
#if JUCE_MAC || JUCE_WINDOWS
#include "../modules/juce_sharedtexture/SharedTexture.h"
#endif
//==============================================================================
/**
@ -282,6 +287,21 @@ private:
juce::AudioPlayHead* playHead;
#if JUCE_MAC || JUCE_WINDOWS
public:
bool isSyphonInputActive() const;
bool isSyphonInputStarted() const;
void connectSyphonInput(const juce::String& server, const juce::String& app);
void disconnectSyphonInput();
juce::String getSyphonSourceName() const;
juce::SpinLock syphonLock;
private:
ImageParser syphonImageParser = ImageParser(*this);
std::unique_ptr<SyphonFrameGrabber> syphonFrameGrabber;
#endif
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OscirenderAudioProcessor)
};

Wyświetl plik

@ -79,8 +79,8 @@ void SettingsComponent::fileUpdated(juce::String fileName) {
juce::String extension = fileName.fromLastOccurrenceOf(".", true, false).toLowerCase();
txt.setVisible(false);
frame.setVisible(false);
bool isImage = extension == ".gif" || extension == ".png" || extension == ".jpg" || extension == ".jpeg" || extension == ".mov" || extension == ".mp4";
if (fileName.isEmpty() || audioProcessor.objectServerRendering) {
bool isImage = extension == ".gif" || extension == ".png" || extension == ".jpg" || extension == ".jpeg" || extension == ".mov" || extension == ".mp4" || audioProcessor.isSyphonInputStarted();
if ((fileName.isEmpty() && !audioProcessor.isSyphonInputStarted()) || audioProcessor.objectServerRendering) {
// do nothing
} else if (extension == ".txt") {
txt.setVisible(true);

Wyświetl plik

@ -0,0 +1,69 @@
#pragma once
#include <JuceHeader.h>
#include "InvisibleOpenGLContextComponent.h"
class SyphonFrameGrabber : private juce::Thread, public juce::Component
{
public:
SyphonFrameGrabber(SharedTextureManager& manager, juce::String server, juce::String app, ImageParser& parser, int pollMs = 16)
: juce::Thread("SyphonFrameGrabber"), pollIntervalMs(pollMs), manager(manager), parser(parser)
{
// Create the invisible OpenGL context component
glContextComponent = std::make_unique<InvisibleOpenGLContextComponent>();
receiver = manager.addReceiver(server, app);
if (receiver) {
receiver->setUseCPUImage(true); // for pixel access
}
startThread();
}
~SyphonFrameGrabber() override {
stopThread(500);
if (receiver) {
manager.removeReceiver(receiver);
receiver = nullptr;
}
glContextComponent.reset();
}
void run() override {
while (!threadShouldExit()) {
{
if (glContextComponent) {
glContextComponent->getContext().makeActive();
}
receiver->renderGL();
if (glContextComponent) {
glContextComponent->getContext().deactivateCurrentContext();
}
if (isActive() && receiver->isConnected) {
juce::Image image = receiver->getImage();
parser.updateLiveFrame(image);
}
}
wait(pollIntervalMs);
}
}
bool isActive() const
{
return receiver != nullptr && receiver->isInit && receiver->enabled;
}
juce::String getSourceName() const
{
if (receiver) {
return receiver->sharingName + " (" + receiver->sharingAppName + ")";
}
return "";
}
private:
int pollIntervalMs;
SharedTextureManager& manager;
SharedTextureReceiver* receiver = nullptr;
ImageParser& parser;
std::unique_ptr<InvisibleOpenGLContextComponent> glContextComponent;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SyphonFrameGrabber)
};

Wyświetl plik

@ -62,9 +62,25 @@ OsciMainMenuBarModel::OsciMainMenuBarModel(OscirenderAudioProcessor& p, Oscirend
editor.openRecordingSettings();
});
// 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
openSyphonInputDialog();
});
if (editor.processor.wrapperType == juce::AudioProcessor::WrapperType::wrapperType_Standalone) {
addMenuItem(3, "Settings...", [this] {
editor.openAudioSettings();
});
}
}
void OsciMainMenuBarModel::openSyphonInputDialog() {
editor.openSyphonInputDialog();
}
void OsciMainMenuBarModel::disconnectSyphonInput() {
audioProcessor.disconnectSyphonInput();
}

Wyświetl plik

@ -9,6 +9,8 @@ class OscirenderAudioProcessor;
class OsciMainMenuBarModel : public MainMenuBarModel {
public:
OsciMainMenuBarModel(OscirenderAudioProcessor& p, OscirenderAudioProcessorEditor& editor);
void openSyphonInputDialog();
void disconnectSyphonInput();
private:
OscirenderAudioProcessor& audioProcessor;

Wyświetl plik

@ -0,0 +1,68 @@
#pragma once
#include <JuceHeader.h>
#include "../modules/juce_sharedtexture/SharedTexture.h"
class SyphonInputSelectorComponent : public juce::Component, private juce::Button::Listener {
public:
SyphonInputSelectorComponent(SharedTextureManager& manager, std::function<void(const juce::String&, const juce::String&)> onConnect, std::function<void()> onDisconnect, bool isConnected, juce::String currentSource)
: sharedTextureManager(manager), onConnectCallback(onConnect), onDisconnectCallback(onDisconnect), connected(isConnected), currentSourceName(currentSource) {
addAndMakeVisible(sourceLabel);
sourceLabel.setText("Syphon/Spout Source:", juce::dontSendNotification);
addAndMakeVisible(sourceDropdown);
sourceDropdown.onChange = [this] {
selectedSource = sourceDropdown.getText();
};
addAndMakeVisible(connectButton);
connectButton.setButtonText(connected ? "Disconnect" : "Connect");
connectButton.addListener(this);
refreshSources();
if (!currentSourceName.isEmpty()) {
sourceDropdown.setText(currentSourceName, juce::dontSendNotification);
}
}
void refreshSources() {
sourceDropdown.clear();
auto sources = sharedTextureManager.getAvailableSenders();
for (const auto& s : sources)
sourceDropdown.addItem(s, sourceDropdown.getNumItems() + 1);
}
void resized() override {
auto area = getLocalBounds().reduced(10);
sourceLabel.setBounds(area.removeFromTop(24));
sourceDropdown.setBounds(area.removeFromTop(28));
connectButton.setBounds(area.removeFromTop(28).reduced(0, 8));
}
void buttonClicked(juce::Button* b) override {
if (connected) {
onDisconnectCallback();
} else {
auto selected = sourceDropdown.getText();
if (selected.isNotEmpty()) {
// Syphon: "ServerName - AppName"
auto parts = juce::StringArray::fromTokens(selected, "-", "");
juce::String server = parts[0].trim();
juce::String app = parts.size() > 1 ? parts[1].trim() : juce::String();
onConnectCallback(server, app);
}
}
}
private:
SharedTextureManager& sharedTextureManager;
std::function<void(const juce::String&, const juce::String&)> onConnectCallback;
std::function<void()> onDisconnectCallback;
bool connected;
juce::String currentSourceName;
juce::String selectedSource;
juce::Label sourceLabel;
juce::ComboBox sourceDropdown;
juce::TextButton connectButton;
};

Wyświetl plik

@ -48,6 +48,25 @@ ImageParser::ImageParser(OscirenderAudioProcessor& p, juce::String extension, ju
setFrame(0);
}
// Constructor for live Syphon/Spout input
ImageParser::ImageParser(OscirenderAudioProcessor& p) : audioProcessor(p), usingLiveImage(true) {
width = 1;
height = 1;
}
void ImageParser::updateLiveFrame(const juce::Image& newImage)
{
if (newImage.isValid()) {
juce::SpinLock::ScopedLockType lock(liveImageLock);
liveImage = newImage;
liveImage.duplicateIfShared();
width = liveImage.getWidth();
height = liveImage.getHeight();
visited.resize(width * height);
visited.assign(width * height, false);
}
}
void ImageParser::processGifFile(juce::File& file) {
juce::String fileName = file.getFullPathName();
gd_GIF *gif = gd_open_gif(fileName.toRawUTF8());
@ -301,12 +320,22 @@ void ImageParser::resetPosition() {
}
float ImageParser::getPixelValue(int x, int y, bool invert) {
if (usingLiveImage) {
if (liveImage.isValid()) {
if (x < 0 || x >= width || y < 0 || y >= height) return 0;
juce::Colour pixel = liveImage.getPixelAt(x, height - y - 1);
float value = pixel.getBrightness();
if (invert && value > 0) value = 1.0f - value;
return value;
}
return 0;
}
int index = (height - y - 1) * width + x;
if (index < 0 || frames.size() <= 0 || index >= frames[frameIndex].size()) {
return 0;
}
float pixel = frames[frameIndex][index] / (float) std::numeric_limits<uint8_t>::max();
// never traverse transparent pixels
if (invert && pixel > 0) {
pixel = 1 - pixel;
}
@ -360,6 +389,8 @@ void ImageParser::findNearestNeighbour(int searchRadius, float thresholdPow, int
}
osci::Point ImageParser::getSample() {
juce::SpinLock::ScopedLockType lock(liveImageLock);
if (ALGORITHM == "HILLIGOSS") {
if (count % jumpFrequency() == 0) {
resetPosition();

Wyświetl plik

@ -7,8 +7,13 @@ class CommonPluginEditor;
class ImageParser {
public:
ImageParser(OscirenderAudioProcessor& p, juce::String fileName, juce::MemoryBlock image);
~ImageParser();
ImageParser(OscirenderAudioProcessor& p, juce::String fileName, juce::MemoryBlock image);
// Constructor for live Syphon/Spout input
ImageParser(OscirenderAudioProcessor& p);
~ImageParser();
// Update the live frame (for Syphon/Spout)
void updateLiveFrame(const juce::Image& newImage);
void setFrame(int index);
osci::Point getSample();
@ -58,4 +63,9 @@ private:
double scanX = -1;
double scanY = 1;
int scanCount = 0;
// Live image support
juce::SpinLock liveImageLock;
bool usingLiveImage = false;
juce::Image liveImage;
};

Wyświetl plik

@ -120,7 +120,6 @@ void ObjectServer::run() {
}
}
}
}
}
}

@ -1 +1 @@
Subproject commit f2faeeb981b5b1676c3aa349e60335922c966ae4
Subproject commit 8c77ff43e2d22ae927bfb3a268d0724e4174b53f

Wyświetl plik

@ -179,6 +179,8 @@
<FILE id="QQzSwh" name="SliderTextBox.h" compile="0" resource="0" file="Source/components/SliderTextBox.h"/>
<FILE id="QrDKRZ" name="SvgButton.h" compile="0" resource="0" file="Source/components/SvgButton.h"/>
<FILE id="qzfstC" name="SwitchButton.h" compile="0" resource="0" file="Source/components/SwitchButton.h"/>
<FILE id="rA7fII" name="SyphonInputSelectorComponent.h" compile="0"
resource="0" file="Source/components/SyphonInputSelectorComponent.h"/>
<FILE id="WfUlLH" name="TimelineComponent.cpp" compile="1" resource="0"
file="Source/components/TimelineComponent.cpp"/>
<FILE id="fsckjw" name="TimelineComponent.h" compile="0" resource="0"
@ -642,6 +644,8 @@
file="Source/FrameSettingsComponent.cpp"/>
<FILE id="lzBNS1" name="FrameSettingsComponent.h" compile="0" resource="0"
file="Source/FrameSettingsComponent.h"/>
<FILE id="nfoWJk" name="InvisibleOpenGLContextComponent.h" compile="0"
resource="0" file="Source/InvisibleOpenGLContextComponent.h"/>
<FILE id="d2zFqF" name="LookAndFeel.cpp" compile="1" resource="0" file="Source/LookAndFeel.cpp"/>
<FILE id="TJDqWs" name="LookAndFeel.h" compile="0" resource="0" file="Source/LookAndFeel.h"/>
<FILE id="X26RjJ" name="LuaComponent.cpp" compile="1" resource="0"
@ -669,6 +673,8 @@
file="Source/SettingsComponent.cpp"/>
<FILE id="Vlmozi" name="SettingsComponent.h" compile="0" resource="0"
file="Source/SettingsComponent.h"/>
<FILE id="jyHVpz" name="SyphonFrameGrabber.h" compile="0" resource="0"
file="Source/SyphonFrameGrabber.h"/>
<FILE id="UxZu4n" name="TxtComponent.cpp" compile="1" resource="0"
file="Source/TxtComponent.cpp"/>
<FILE id="kxPbsL" name="TxtComponent.h" compile="0" resource="0" file="Source/TxtComponent.h"/>