├── data └── locale │ └── en-US.ini ├── lib └── atkaudio │ ├── src │ ├── atkaudio │ │ ├── ModuleInfrastructure │ │ │ ├── Bridge │ │ │ │ ├── ModuleDeviceSettingsComponent.h │ │ │ │ ├── ModuleBridge.h │ │ │ │ ├── ModuleAudioDevice.cpp │ │ │ │ ├── ModuleAudioIODeviceType.h │ │ │ │ └── ModuleDeviceManager.h │ │ │ ├── MidiServer │ │ │ │ ├── MidiServerMessageCollector.h │ │ │ │ ├── MidiServerSettingsComponent.h │ │ │ │ └── MidiServer.h │ │ │ └── AudioServer │ │ │ │ └── ChannelRoutingMatrix.h │ │ ├── SharedPluginList.cpp │ │ ├── PluginHost2 │ │ │ ├── Core │ │ │ │ ├── ARAPlugin.cpp │ │ │ │ ├── DelayLinePlugin.h │ │ │ │ ├── InternalPlugins.h │ │ │ │ ├── PluginGraph.h │ │ │ │ ├── DeviceIo2Plugin.h │ │ │ │ └── DelayLinePlugin.cpp │ │ │ ├── API │ │ │ │ └── PluginHost2.h │ │ │ └── UI │ │ │ │ └── IOConfigurationWindow.h │ │ ├── Delay.h │ │ ├── JuceApp.h │ │ ├── MessagePump.h │ │ ├── PluginHost │ │ │ ├── API │ │ │ │ └── PluginHost.h │ │ │ ├── UI │ │ │ │ ├── UICommon.h │ │ │ │ ├── HostEditorWindow.h │ │ │ │ └── PluginHostFooter.h │ │ │ ├── PluginHost.h │ │ │ └── Core │ │ │ │ ├── PluginHolder.h │ │ │ │ └── HostAudioProcessor.h │ │ ├── DeviceIo │ │ │ └── DeviceIo.h │ │ ├── atkaudio.h │ │ ├── DeviceIo2 │ │ │ ├── DeviceIo2.h │ │ │ └── DeviceIo2App.h │ │ ├── About.h │ │ ├── MessagePump.cpp │ │ ├── AudioProcessorGraphMT │ │ │ └── SpinWait.h │ │ ├── SandboxedPluginScanner.h │ │ ├── AtomicSharedPtr.h │ │ ├── SharedPluginList.h │ │ ├── atkAudioModule.h │ │ ├── Delay │ │ │ └── Delay.cpp │ │ ├── RealtimeThread.h │ │ ├── FifoBuffer.h │ │ ├── CpuInfo.h │ │ ├── UpdateCheck.h │ │ └── atkaudio.cpp │ └── scanner │ │ ├── PluginScanner.cpp │ │ └── CMakeLists.txt │ ├── assets │ ├── icon.ico │ └── icon100.png │ ├── cmake │ ├── patches │ │ ├── juce-effect-min-scale.patch │ │ └── apply-juce-patches.cmake │ ├── preconfig.cmake │ └── linux │ │ └── install.sh │ └── CMakeLists.txt ├── src ├── config.h.in ├── CompareVersionStrings.h ├── plugin_host2_helper.cpp ├── Delay.cpp ├── plugin-main.cpp └── plugin_host2.cpp ├── .gitignore ├── cmake ├── windows │ ├── defaults.cmake │ ├── resources │ │ ├── resource.rc.in │ │ └── installer-Windows.iss.in │ ├── compilerconfig.cmake │ ├── buildspec.cmake │ └── helpers.cmake ├── common │ ├── osconfig.cmake │ ├── helpers_common.cmake │ ├── compiler_common.cmake │ └── bootstrap.cmake ├── macos │ ├── buildspec.cmake │ ├── defaults.cmake │ ├── compilerconfig.cmake │ └── helpers.cmake └── linux │ ├── FindLibObs.cmake │ ├── compilerconfig.cmake │ ├── defaults.cmake │ ├── helpers.cmake.backup │ └── helpers.cmake ├── linux-dependencies.sh ├── README.md └── linux-cross-compile.sh /data/locale/en-US.ini: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/ModuleInfrastructure/Bridge/ModuleDeviceSettingsComponent.h: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/ModuleInfrastructure/MidiServer/MidiServerMessageCollector.h: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/atkaudio/assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atkAudio/PluginForObsRelease/HEAD/lib/atkaudio/assets/icon.ico -------------------------------------------------------------------------------- /lib/atkaudio/assets/icon100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atkAudio/PluginForObsRelease/HEAD/lib/atkaudio/assets/icon100.png -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/SharedPluginList.cpp: -------------------------------------------------------------------------------- 1 | #include "SharedPluginList.h" 2 | 3 | namespace atk 4 | { 5 | 6 | JUCE_IMPLEMENT_SINGLETON(SharedPluginList) 7 | 8 | } // namespace atk 9 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/ModuleInfrastructure/Bridge/ModuleBridge.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ModuleAudioDevice.h" 4 | #include "ModuleAudioServerDevice.h" 5 | #include "ModuleAudioIODeviceType.h" 6 | #include "ModuleDeviceManager.h" 7 | -------------------------------------------------------------------------------- /src/config.h.in: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define PLUGIN_NAME "@CMAKE_PROJECT_NAME@" 4 | #define PLUGIN_VERSION "@CMAKE_PROJECT_VERSION@" 5 | #define PLUGIN_DISPLAY_NAME "@PLUGIN_DISPLAY_NAME@" 6 | #define PLUGIN_YEAR "@PLUGIN_YEAR@" 7 | #define PLUGIN_AUTHOR "@PLUGIN_AUTHOR@" 8 | #define PLUGIN_OBS_VERSION_REQUIRED "@PLUGIN_OBS_VERSION_REQUIRED@" 9 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/PluginHost2/Core/ARAPlugin.cpp: -------------------------------------------------------------------------------- 1 | #include "ARAPlugin.h" 2 | 3 | #if JUCE_PLUGINHOST_ARA && (JUCE_MAC || JUCE_WINDOWS || JUCE_LINUX) 4 | 5 | const Identifier ARAPluginInstanceWrapper::ARATestHost::Context::xmlRootTag{"ARATestHostContext"}; 6 | const Identifier ARAPluginInstanceWrapper::ARATestHost::Context::xmlAudioFileAttrib{"AudioFile"}; 7 | 8 | #endif 9 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/Delay.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "atkaudio.h" 4 | 5 | namespace atk 6 | { 7 | class Delay 8 | { 9 | public: 10 | Delay(); 11 | ~Delay(); 12 | 13 | void process(float** buffer, int numChannels, int numSamples, double sampleRate); 14 | 15 | void setDelay(float delay); 16 | 17 | private: 18 | struct Impl; 19 | Impl* pImpl; 20 | }; 21 | } // namespace atk 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore everything 2 | /* 3 | /.* 4 | 5 | # Except for default project files 6 | !/.github 7 | !/build-aux 8 | !/cmake 9 | !/data 10 | !/src 11 | !.clang-format 12 | !.cmake-format.json 13 | !.gersemirc 14 | !.gitattributes 15 | !.gitignore 16 | !buildspec.json 17 | !CMakeLists.txt 18 | !CMakePresets.json 19 | !README.md 20 | !LICENSE* 21 | !*.cmake 22 | !linux-*.sh 23 | !tests/ 24 | !tests/** 25 | !docs/ 26 | !docs/** 27 | 28 | !/lib 29 | !.vscode 30 | -------------------------------------------------------------------------------- /cmake/windows/defaults.cmake: -------------------------------------------------------------------------------- 1 | # CMake Windows defaults module 2 | 3 | include_guard(GLOBAL) 4 | 5 | # Enable find_package targets to become globally available targets 6 | set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL TRUE) 7 | 8 | include(buildspec) 9 | 10 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 11 | set(CMAKE_INSTALL_PREFIX 12 | "$ENV{ALLUSERSPROFILE}/obs-studio/plugins" 13 | CACHE STRING 14 | "Default plugin installation directory" 15 | FORCE 16 | ) 17 | endif() 18 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/JuceApp.h: -------------------------------------------------------------------------------- 1 | #include "atkaudio.h" 2 | #include 3 | 4 | class Application : public juce::JUCEApplication 5 | { 6 | public: 7 | Application() = default; 8 | 9 | const juce::String getApplicationName() override 10 | { 11 | return PLUGIN_DISPLAY_NAME; 12 | } 13 | 14 | const juce::String getApplicationVersion() override 15 | { 16 | return PLUGIN_VERSION; 17 | } 18 | 19 | void initialise(const juce::String&) override 20 | { 21 | } 22 | 23 | void shutdown() override 24 | { 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/MessagePump.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace atk 8 | { 9 | 10 | // MessagePump bridges Qt event loop with JUCE MessageManager 11 | // Parent (QObject*) handles lifetime - typically Qt main window 12 | class MessagePump : public QObject 13 | { 14 | Q_OBJECT 15 | 16 | public: 17 | explicit MessagePump(QObject* parent = nullptr); 18 | ~MessagePump(); 19 | 20 | void stopPump(); 21 | 22 | private slots: 23 | void onTimeout(); 24 | 25 | private: 26 | std::atomic_bool needsToStop{false}; 27 | QTimer* timer; 28 | }; 29 | 30 | } // namespace atk 31 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/PluginHost/API/PluginHost.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../AudioModule.h" 4 | 5 | #include 6 | 7 | namespace atk 8 | { 9 | class PluginHost : public atkAudioModule 10 | { 11 | public: 12 | PluginHost(); 13 | ~PluginHost(); 14 | 15 | void process(float** buffer, int numChannels, int numSamples, double sampleRate); 16 | 17 | // WindowModule interface 18 | void getState(std::string& s) override; 19 | void setState(std::string& s) override; 20 | 21 | int getInnerPluginChannelCount() const; 22 | 23 | protected: 24 | juce::Component* getWindowComponent() override; 25 | 26 | private: 27 | struct Impl; 28 | Impl* pImpl; 29 | }; 30 | } // namespace atk -------------------------------------------------------------------------------- /lib/atkaudio/cmake/patches/juce-effect-min-scale.patch: -------------------------------------------------------------------------------- 1 | diff --git a/modules/juce_gui_basics/components/juce_Component.cpp b/modules/juce_gui_basics/components/juce_Component.cpp 2 | index abcdef123..456789abc 100644 3 | --- a/modules/juce_gui_basics/components/juce_Component.cpp 4 | +++ b/modules/juce_gui_basics/components/juce_Component.cpp 5 | @@ -211,6 +211,7 @@ public: 6 | void paint (Graphics& g, Component& c, bool ignoreAlphaLevel) 7 | { 8 | auto scale = g.getInternalContext().getPhysicalPixelScaleFactor(); 9 | + scale = jmax (1.0f, scale); 10 | auto scaledBounds = c.getLocalBounds() * scale; 11 | 12 | const auto preferredType = g.getInternalContext().getPreferredImageTypeForTemporaryImages(); 13 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/PluginHost/UI/UICommon.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | constexpr auto margin = 10; 6 | 7 | static void doLayout(juce::Component* main, juce::Component& bottom, int bottomHeight, juce::Rectangle bounds) 8 | { 9 | juce::Grid grid; 10 | grid.setGap(juce::Grid::Px{margin}); 11 | grid.templateColumns = {juce::Grid::TrackInfo{juce::Grid::Fr{1}}}; 12 | grid.templateRows = {juce::Grid::TrackInfo{juce::Grid::Fr{1}}, juce::Grid::TrackInfo{juce::Grid::Px{bottomHeight}}}; 13 | 14 | grid.items = { 15 | juce::GridItem{main}, 16 | juce::GridItem{bottom}.withMargin({0, margin, margin, margin}), 17 | }; 18 | grid.performLayout(bounds); 19 | } 20 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/DeviceIo/DeviceIo.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../atkAudioModule.h" 4 | 5 | #include 6 | 7 | namespace atk 8 | { 9 | class DeviceIo : public atkAudioModule 10 | { 11 | public: 12 | DeviceIo(); 13 | ~DeviceIo(); 14 | 15 | void process(float** buffer, int numChannels, int numSamples, double sampleRate) override; 16 | 17 | void setMixInput(bool mixInput); 18 | void setOutputDelay(float delayMs); 19 | float getOutputDelay() const; 20 | 21 | void getState(std::string& s) override; 22 | void setState(std::string& s) override; 23 | 24 | protected: 25 | // AudioModule interface - only need to provide the window component 26 | juce::Component* getWindowComponent() override; 27 | 28 | private: 29 | struct Impl; 30 | Impl* pImpl; 31 | }; 32 | } // namespace atk -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/PluginHost2/API/PluginHost2.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../../atkAudioModule.h" 4 | 5 | #include 6 | 7 | namespace atk 8 | { 9 | class PluginHost2 : public atkAudioModule 10 | { 11 | public: 12 | PluginHost2(); 13 | ~PluginHost2(); 14 | 15 | void process(float** buffer, int numChannels, int numSamples, double sampleRate) override; 16 | 17 | // WindowModule interface 18 | void getState(std::string& s) override; 19 | void setState(std::string& s) override; 20 | 21 | // Set the parent OBS source (extracts UUID for filtering) 22 | void setParentSource(void* parentSource); 23 | 24 | protected: 25 | // AudioModule interface - only need to provide the window component 26 | juce::Component* getWindowComponent() override; 27 | 28 | private: 29 | struct Impl; 30 | Impl* pImpl; 31 | }; 32 | } // namespace atk -------------------------------------------------------------------------------- /cmake/common/osconfig.cmake: -------------------------------------------------------------------------------- 1 | # CMake operating system bootstrap module 2 | 3 | include_guard(GLOBAL) 4 | 5 | if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows") 6 | set(CMAKE_C_EXTENSIONS FALSE) 7 | set(CMAKE_CXX_EXTENSIONS FALSE) 8 | list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/windows") 9 | set(OS_WINDOWS TRUE) 10 | elseif(CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin") 11 | set(CMAKE_C_EXTENSIONS FALSE) 12 | set(CMAKE_CXX_EXTENSIONS FALSE) 13 | list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/macos") 14 | set(OS_MACOS TRUE) 15 | elseif(CMAKE_HOST_SYSTEM_NAME MATCHES "Linux|FreeBSD|OpenBSD") 16 | set(CMAKE_CXX_EXTENSIONS FALSE) 17 | list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/linux") 18 | string(TOUPPER "${CMAKE_HOST_SYSTEM_NAME}" _SYSTEM_NAME_U) 19 | set(OS_${_SYSTEM_NAME_U} TRUE) 20 | endif() 21 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/ModuleInfrastructure/Bridge/ModuleAudioDevice.cpp: -------------------------------------------------------------------------------- 1 | #include "ModuleAudioDevice.h" 2 | 3 | #include 4 | #include 5 | 6 | namespace atk 7 | { 8 | 9 | int getOBSAudioFrameSize() 10 | { 11 | return AUDIO_OUTPUT_FRAMES; // Actual OBS constant 12 | } 13 | 14 | ModuleOBSAudioDevice::ModuleOBSAudioDevice( 15 | const juce::String& deviceName, 16 | std::shared_ptr deviceCoordinator, 17 | const juce::String& typeName 18 | ) 19 | : juce::AudioIODevice(deviceName, typeName) 20 | , coordinator(deviceCoordinator) 21 | { 22 | // Get current OBS audio configuration 23 | auto* obsAudio = obs_get_audio(); 24 | if (obsAudio) 25 | { 26 | obsChannelCount = audio_output_get_channels(obsAudio); 27 | obsSampleRate = audio_output_get_sample_rate(obsAudio); 28 | } 29 | } 30 | 31 | ModuleOBSAudioDevice::~ModuleOBSAudioDevice() 32 | { 33 | close(); 34 | } 35 | 36 | } // namespace atk 37 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/PluginHost/PluginHost.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../atkAudioModule.h" 4 | 5 | #include 6 | 7 | namespace atk 8 | { 9 | class PluginHost : public atkAudioModule 10 | { 11 | public: 12 | PluginHost(); 13 | ~PluginHost(); 14 | 15 | void process(float** buffer, int numChannels, int numSamples, double sampleRate) override; 16 | 17 | // WindowModule interface 18 | void getState(std::string& s) override; 19 | void setState(std::string& s) override; 20 | void setVisible(bool visible) override; 21 | 22 | void setDockId(const std::string& id); 23 | bool isDockVisible() const; 24 | 25 | int getInnerPluginChannelCount() const; 26 | 27 | bool isMultiCoreEnabled() const; 28 | 29 | void setMultiCoreEnabled(bool enabled); 30 | 31 | float getCpuLoad() const; 32 | 33 | int getLatencyMs() const; 34 | 35 | protected: 36 | juce::Component* getWindowComponent() override; 37 | 38 | private: 39 | struct Impl; 40 | Impl* pImpl; 41 | }; 42 | } // namespace atk -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/atkaudio.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class QObject; 6 | 7 | #define MAX_OBS_AUDIO_BUFFER_SIZE 1024 8 | 9 | namespace atk 10 | { 11 | extern "C" 12 | { 13 | void create(); 14 | void destroy(); 15 | void pump(); 16 | void update(); 17 | 18 | // Start message pump with Qt parent (typically Qt main window) 19 | // Must be called after create() and before any JUCE GUI operations 20 | void startMessagePump(QObject* qtParent); 21 | 22 | // Get Qt main window native handle for per-instance parent creation (fully lazy-initialized) 23 | void* getQtMainWindowHandle(); 24 | 25 | // Helper to set window ownership for JUCE components after addToDesktop() 26 | void setWindowOwnership(juce::Component* component); 27 | 28 | // Apply colors to LookAndFeel from RGB values 29 | void applyColors(uint8_t bgR, uint8_t bgG, uint8_t bgB, uint8_t fgR, uint8_t fgG, uint8_t fgB); 30 | 31 | // Logging helper 32 | void logMessage(const juce::String& message); 33 | } 34 | } // namespace atk 35 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/DeviceIo2/DeviceIo2.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../atkAudioModule.h" 4 | 5 | #include 6 | #include 7 | 8 | namespace atk 9 | { 10 | class DeviceIo2 : public atkAudioModule 11 | { 12 | public: 13 | DeviceIo2(); 14 | ~DeviceIo2(); 15 | 16 | void process(float** buffer, int numChannels, int numSamples, double sampleRate) override; 17 | 18 | void setOutputDelay(float delayMs); 19 | float getOutputDelay() const; 20 | 21 | void setInputChannelMapping(const std::vector>& mapping); 22 | std::vector> getInputChannelMapping() const; 23 | 24 | void setOutputChannelMapping(const std::vector>& mapping); 25 | std::vector> getOutputChannelMapping() const; 26 | 27 | void getState(std::string& s) override; 28 | void setState(std::string& s) override; 29 | 30 | juce::Component* getWindowComponent() override; 31 | juce::Component* createEmbeddableSettingsComponent(); 32 | 33 | private: 34 | struct Impl; 35 | Impl* pImpl; 36 | }; 37 | } // namespace atk 38 | -------------------------------------------------------------------------------- /src/CompareVersionStrings.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | inline std::vector TokenizeVersionString(const std::string& str) 7 | { 8 | std::vector tokens; 9 | std::stringstream ss(str); 10 | std::string item; 11 | while (std::getline(ss, item, '.')) 12 | { 13 | try 14 | { 15 | tokens.push_back(std::stoi(item)); 16 | } 17 | catch (const std::exception& e) 18 | { 19 | (void)e; 20 | tokens.push_back(-1); 21 | } 22 | } 23 | return tokens; 24 | } 25 | 26 | inline int CompareVersionStrings(const std::string& v1, const std::string& v2) 27 | { 28 | auto p1 = TokenizeVersionString(v1); 29 | auto p2 = TokenizeVersionString(v2); 30 | size_t maxLen = std::max(p1.size(), p2.size()); 31 | p1.resize(maxLen, 0); 32 | p2.resize(maxLen, 0); 33 | for (size_t i = 0; i < maxLen; ++i) 34 | { 35 | if (p1[i] < p2[i]) 36 | return -1; 37 | if (p1[i] > p2[i]) 38 | return 1; 39 | } 40 | return 0; 41 | } -------------------------------------------------------------------------------- /cmake/windows/resources/resource.rc.in: -------------------------------------------------------------------------------- 1 | 1 VERSIONINFO 2 | FILEVERSION ${PROJECT_VERSION_MAJOR},${PROJECT_VERSION_MINOR},${PROJECT_VERSION_PATCH},0 3 | PRODUCTVERSION ${PROJECT_VERSION_MAJOR},${PROJECT_VERSION_MINOR},${PROJECT_VERSION_PATCH},0 4 | FILEFLAGSMASK 0x0L 5 | #ifdef _DEBUG 6 | FILEFLAGS 0x1L 7 | #else 8 | FILEFLAGS 0x0L 9 | #endif 10 | FILEOS 0x0L 11 | FILETYPE 0x2L 12 | FILESUBTYPE 0x0L 13 | BEGIN 14 | BLOCK "StringFileInfo" 15 | BEGIN 16 | BLOCK "040904b0" 17 | BEGIN 18 | VALUE "CompanyName", "${PLUGIN_AUTHOR}" 19 | VALUE "FileDescription", "${PROJECT_NAME}" 20 | VALUE "FileVersion", "${PROJECT_VERSION}" 21 | VALUE "InternalName", "${PROJECT_NAME}" 22 | VALUE "LegalCopyright", "(C) ${CURRENT_YEAR} ${PLUGIN_AUTHOR}" 23 | VALUE "OriginalFilename", "${PROJECT_NAME}" 24 | VALUE "ProductName", "${PROJECT_NAME}" 25 | VALUE "ProductVersion", "${PROJECT_VERSION}" 26 | END 27 | END 28 | BLOCK "VarFileInfo" 29 | BEGIN 30 | VALUE "Translation", 0x409, 1200 31 | END 32 | END 33 | -------------------------------------------------------------------------------- /linux-dependencies.sh: -------------------------------------------------------------------------------- 1 | # Linux build dependencies for OBS Plugin 2 | # This file is used by both container builds and native builds 3 | 4 | # Build tools (architecture-independent) 5 | BUILD_TOOLS=( 6 | build-essential 7 | jq 8 | cmake 9 | pkg-config 10 | ninja-build 11 | file 12 | ) 13 | 14 | # JUCE dependencies (from official JUCE Linux Dependencies.md) 15 | # These need architecture suffix for cross-compilation 16 | JUCE_DEPS=( 17 | libasound2-dev 18 | libjack-jackd2-dev 19 | ladspa-sdk 20 | libcurl4-openssl-dev 21 | libfreetype6-dev 22 | libfontconfig1-dev 23 | libx11-dev 24 | libxcomposite-dev 25 | libxcursor-dev 26 | libxext-dev 27 | libxinerama-dev 28 | libxrandr-dev 29 | libxrender-dev 30 | libwebkit2gtk-4.1-dev 31 | libglu1-mesa-dev 32 | mesa-common-dev 33 | libgtk-3-dev 34 | ) 35 | 36 | # OBS dependencies 37 | OBS_DEPS=( 38 | libobs-dev 39 | libsimde-dev 40 | ) 41 | 42 | # Qt6 dependencies 43 | QT6_DEPS=( 44 | qt6-base-dev 45 | libqt6svg6-dev 46 | qt6-base-private-dev 47 | ) 48 | 49 | # Qt6 tools (x86_64 only, for cross-compilation) 50 | QT6_TOOLS=( 51 | qt6-base-dev-tools 52 | ) 53 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/PluginHost2/UI/IOConfigurationWindow.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../../AudioProcessorGraphMT/AudioProcessorGraphMT.h" 3 | 4 | #include 5 | using namespace juce; 6 | using atk::AudioProcessorGraphMT; 7 | 8 | class MainHostWindow; 9 | class GraphDocumentComponent; 10 | 11 | class IOConfigurationWindow final : public AudioProcessorEditor 12 | { 13 | public: 14 | IOConfigurationWindow(AudioProcessor&); 15 | ~IOConfigurationWindow() override; 16 | 17 | void paint(Graphics& g) override; 18 | void resized() override; 19 | 20 | private: 21 | class InputOutputConfig; 22 | 23 | AudioProcessor::BusesLayout currentLayout; 24 | Label title; 25 | std::unique_ptr inConfig, outConfig; 26 | 27 | InputOutputConfig* getConfig(bool isInput) noexcept 28 | { 29 | return isInput ? inConfig.get() : outConfig.get(); 30 | } 31 | 32 | void update(); 33 | 34 | MainHostWindow* getMainWindow() const; 35 | GraphDocumentComponent* getGraphEditor() const; 36 | AudioProcessorGraphMT* getGraph() const; 37 | AudioProcessorGraphMT::NodeID getNodeID() const; 38 | 39 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(IOConfigurationWindow) 40 | }; 41 | -------------------------------------------------------------------------------- /cmake/macos/buildspec.cmake: -------------------------------------------------------------------------------- 1 | # CMake macOS build dependencies module 2 | 3 | include_guard(GLOBAL) 4 | 5 | include(buildspec_common) 6 | 7 | # _check_dependencies_macos: Set up macOS slice for _check_dependencies 8 | function(_check_dependencies_macos) 9 | set(arch universal) 10 | set(platform macos) 11 | 12 | file(READ "${CMAKE_CURRENT_SOURCE_DIR}/buildspec.json" buildspec) 13 | 14 | # Use source-directory-based dependency directory to persist across cache clears 15 | set(dependencies_dir "${CMAKE_SOURCE_DIR}/_deps") 16 | set(prebuilt_filename "macos-deps-VERSION-ARCH_REVISION.tar.xz") 17 | set(prebuilt_destination "obs-deps-VERSION-ARCH") 18 | set(qt6_filename "macos-deps-qt6-VERSION-ARCH-REVISION.tar.xz") 19 | set(qt6_destination "obs-deps-qt6-VERSION-ARCH") 20 | set(obs-studio_filename "VERSION.tar.gz") 21 | set(obs-studio_destination "obs-studio-VERSION") 22 | set(dependencies_list prebuilt qt6 obs-studio) 23 | 24 | _check_dependencies() 25 | 26 | execute_process( 27 | COMMAND "xattr" -r -d com.apple.quarantine "${dependencies_dir}" 28 | ERROR_QUIET 29 | ) 30 | 31 | list(APPEND CMAKE_FRAMEWORK_PATH "${dependencies_dir}/Frameworks") 32 | set(CMAKE_FRAMEWORK_PATH ${CMAKE_FRAMEWORK_PATH} PARENT_SCOPE) 33 | endfunction() 34 | 35 | _check_dependencies_macos() 36 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/About.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | static inline void showAboutDialog() 7 | { 8 | DialogWindow::LaunchOptions options; 9 | auto& lookAndFeel = juce::LookAndFeel::getDefaultLookAndFeel(); 10 | options.dialogTitle = "About"; 11 | String aboutText; 12 | aboutText 13 | << PLUGIN_DISPLAY_NAME 14 | << "\n" 15 | << PLUGIN_VERSION 16 | << "\n\n" 17 | << "Copyright (c) " 18 | << PLUGIN_YEAR 19 | << " " 20 | << PLUGIN_AUTHOR 21 | << "\n" 22 | << "Licensed under AGPL3"; 23 | auto* label = new Label({}, aboutText); 24 | label->setColour(Label::backgroundColourId, lookAndFeel.findColour(ResizableWindow::backgroundColourId)); 25 | label->setColour(Label::textColourId, lookAndFeel.findColour(Label::textColourId)); 26 | label->setJustificationType(Justification::centred); 27 | label->setSize(250, 160); 28 | options.content.setOwned(label); 29 | options.useNativeTitleBar = false; 30 | options.resizable = false; 31 | options.escapeKeyTriggersCloseButton = true; 32 | options.dialogBackgroundColour = lookAndFeel.findColour(ResizableWindow::backgroundColourId); 33 | options.launchAsync(); 34 | } -------------------------------------------------------------------------------- /src/plugin_host2_helper.cpp: -------------------------------------------------------------------------------- 1 | #include "config.h" 2 | 3 | #include 4 | #include 5 | 6 | #define SOURCE_NAME "atkAudio Ph2 OBS Output Node" 7 | #define SOURCE_ID "atkaudio_ph2helper" 8 | 9 | struct ph2helper_data 10 | { 11 | obs_source_t* source; 12 | }; 13 | 14 | static void destroy(void* data) 15 | { 16 | auto* ph2h = static_cast(data); 17 | if (!ph2h) 18 | return; 19 | if (ph2h->source) 20 | { 21 | obs_source_release(ph2h->source); 22 | ph2h->source = nullptr; 23 | } 24 | delete ph2h; 25 | } 26 | 27 | static void* create(obs_data_t* settings, obs_source_t* source) 28 | { 29 | UNUSED_PARAMETER(settings); 30 | UNUSED_PARAMETER(source); 31 | return new ph2helper_data(); 32 | } 33 | 34 | static const char* getname(void* unused) 35 | { 36 | UNUSED_PARAMETER(unused); 37 | return SOURCE_NAME; 38 | } 39 | 40 | static obs_properties_t* properties(void* data) 41 | { 42 | obs_properties_t* props = obs_properties_create(); 43 | 44 | return props; 45 | } 46 | 47 | struct obs_source_info ph2helper_source_info = { 48 | .id = SOURCE_ID, 49 | .type = OBS_SOURCE_TYPE_INPUT, 50 | .output_flags = OBS_SOURCE_AUDIO, 51 | .get_name = getname, 52 | .create = create, 53 | .destroy = destroy, 54 | }; -------------------------------------------------------------------------------- /cmake/macos/defaults.cmake: -------------------------------------------------------------------------------- 1 | # CMake macOS defaults module 2 | 3 | include_guard(GLOBAL) 4 | 5 | # Set empty codesigning team if not specified as cache variable 6 | if(NOT CODESIGN_TEAM) 7 | set(CODESIGN_TEAM "" CACHE STRING "OBS code signing team for macOS" FORCE) 8 | 9 | # Set ad-hoc codesigning identity if not specified as cache variable 10 | if(NOT CODESIGN_IDENTITY) 11 | set(CODESIGN_IDENTITY "-" CACHE STRING "OBS code signing identity for macOS" FORCE) 12 | endif() 13 | endif() 14 | 15 | include(xcode) 16 | 17 | include(buildspec) 18 | 19 | # Use Applications directory as default install destination 20 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 21 | set(CMAKE_INSTALL_PREFIX 22 | "$ENV{HOME}/Library/Application Support/obs-studio/plugins" 23 | CACHE STRING 24 | "Default plugin installation directory" 25 | FORCE 26 | ) 27 | endif() 28 | 29 | # Enable find_package targets to become globally available targets 30 | set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL TRUE) 31 | # Enable RPATH support for generated binaries 32 | set(CMAKE_MACOSX_RPATH TRUE) 33 | # Use RPATHs from build tree _in_ the build tree 34 | set(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE) 35 | # Do not add default linker search paths to RPATH 36 | set(CMAKE_INSTALL_RPATH_USE_LINK_PATH FALSE) 37 | # Use common bundle-relative RPATH for installed targets 38 | set(CMAKE_INSTALL_RPATH "@executable_path/../Frameworks") 39 | -------------------------------------------------------------------------------- /cmake/common/helpers_common.cmake: -------------------------------------------------------------------------------- 1 | # CMake common helper functions module 2 | 3 | include_guard(GLOBAL) 4 | 5 | # check_uuid: Helper function to check for valid UUID 6 | function(check_uuid uuid_string return_value) 7 | set(valid_uuid TRUE) 8 | # gersemi: off 9 | set(uuid_token_lengths 8 4 4 4 12) 10 | # gersemi: on 11 | set(token_num 0) 12 | 13 | string(REPLACE "-" ";" uuid_tokens ${uuid_string}) 14 | list(LENGTH uuid_tokens uuid_num_tokens) 15 | 16 | if(uuid_num_tokens EQUAL 5) 17 | message(DEBUG "UUID ${uuid_string} is valid with 5 tokens.") 18 | foreach(uuid_token IN LISTS uuid_tokens) 19 | list(GET uuid_token_lengths ${token_num} uuid_target_length) 20 | string(LENGTH "${uuid_token}" uuid_actual_length) 21 | if(uuid_actual_length EQUAL uuid_target_length) 22 | string(REGEX MATCH "[0-9a-fA-F]+" uuid_hex_match ${uuid_token}) 23 | if(NOT uuid_hex_match STREQUAL uuid_token) 24 | set(valid_uuid FALSE) 25 | break() 26 | endif() 27 | else() 28 | set(valid_uuid FALSE) 29 | break() 30 | endif() 31 | math(EXPR token_num "${token_num}+1") 32 | endforeach() 33 | else() 34 | set(valid_uuid FALSE) 35 | endif() 36 | message(DEBUG "UUID ${uuid_string} valid: ${valid_uuid}") 37 | set(${return_value} ${valid_uuid} PARENT_SCOPE) 38 | endfunction() 39 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/MessagePump.cpp: -------------------------------------------------------------------------------- 1 | #include "MessagePump.h" 2 | #include "atkaudio.h" 3 | 4 | #include 5 | 6 | #include 7 | 8 | namespace atk 9 | { 10 | 11 | MessagePump::MessagePump(QObject* parent) 12 | : QObject(parent) 13 | { 14 | // Verify JUCE MessageManager is attached to the current (Qt main) thread 15 | if (!juce::MessageManager::getInstance()->isThisTheMessageThread()) 16 | { 17 | // Log error - but don't use blog() here since we're in atkaudio lib 18 | juce::Logger::writeToLog("MessagePump: ERROR - JUCE MessageManager is NOT attached to Qt main thread!"); 19 | } 20 | 21 | // Create timer without parent so we control its lifetime 22 | // This prevents crash if Qt parent is destroyed before we are 23 | timer = new QTimer(nullptr); 24 | connect(timer, &QTimer::timeout, this, &MessagePump::onTimeout); 25 | timer->start(10); 26 | } 27 | 28 | MessagePump::~MessagePump() 29 | { 30 | // Don't touch timer in destructor - Qt may already be shutting down 31 | // The needsToStop flag will prevent further callbacks 32 | timer = nullptr; 33 | } 34 | 35 | void MessagePump::stopPump() 36 | { 37 | // Just set the flag - don't touch Qt objects during shutdown 38 | // The timer callback will see this and stop pumping 39 | needsToStop.store(true, std::memory_order_release); 40 | } 41 | 42 | void MessagePump::onTimeout() 43 | { 44 | if (needsToStop.load(std::memory_order_acquire)) 45 | return; 46 | 47 | atk::pump(); 48 | } 49 | 50 | } // namespace atk 51 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/AudioProcessorGraphMT/SpinWait.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 atkAudio 2 | // Spin wait with exponential backoff (8→8192) then atomic::wait fallback 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | #if defined(_MSC_VER) && (defined(_M_X64) || defined(_M_IX86)) 9 | #include 10 | #elif defined(_MSC_VER) && defined(_M_ARM64) 11 | #include 12 | #endif 13 | 14 | namespace atk 15 | { 16 | 17 | inline void cpuPause() 18 | { 19 | #if defined(_MSC_VER) && (defined(_M_X64) || defined(_M_IX86)) 20 | _mm_pause(); 21 | #elif defined(_MSC_VER) && defined(_M_ARM64) 22 | __yield(); 23 | #elif defined(__x86_64__) || defined(__i386__) 24 | __asm__ __volatile__("pause"); 25 | #elif defined(__aarch64__) || defined(__arm__) 26 | __asm__ __volatile__("yield"); 27 | #else 28 | std::atomic_signal_fence(std::memory_order_seq_cst); 29 | #endif 30 | } 31 | 32 | template 33 | void spinAtomicWait(std::atomic& atomic, T oldValue, std::memory_order order = std::memory_order_acquire) 34 | { 35 | for (int i = 0; i < 10; ++i) 36 | { 37 | if (atomic.load(order) != oldValue) 38 | return; 39 | 40 | for (int p = 0; p < (8 << i); ++p) 41 | cpuPause(); 42 | } 43 | 44 | while (atomic.load(order) == oldValue) 45 | atomic.wait(oldValue, order); 46 | } 47 | 48 | template 49 | void spinAtomicNotifyOne(std::atomic& atomic) 50 | { 51 | atomic.notify_one(); 52 | } 53 | 54 | template 55 | void spinAtomicNotifyAll(std::atomic& atomic) 56 | { 57 | atomic.notify_all(); 58 | } 59 | 60 | } // namespace atk 61 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/ModuleInfrastructure/AudioServer/ChannelRoutingMatrix.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | namespace atk 9 | { 10 | 11 | struct ChannelMappingState 12 | { 13 | std::vector> inputMapping; 14 | std::vector> outputMapping; 15 | std::atomic debugLogged{false}; 16 | }; 17 | 18 | class ChannelRoutingMatrix 19 | { 20 | public: 21 | ChannelRoutingMatrix(); 22 | ~ChannelRoutingMatrix() = default; 23 | 24 | void applyInputRouting( 25 | float* const* obsBuffer, 26 | const juce::AudioBuffer& deviceInputBuffer, 27 | juce::AudioBuffer& targetBuffer, 28 | int numObsChannels, 29 | int numSamples, 30 | int numDeviceInputSubs 31 | ); 32 | 33 | void applyOutputRouting( 34 | const juce::AudioBuffer& sourceBuffer, 35 | float* const* obsBuffer, 36 | juce::AudioBuffer& deviceOutputBuffer, 37 | int numObsChannels, 38 | int numSamples, 39 | int numDeviceOutputSubs 40 | ); 41 | 42 | void setInputMapping(const std::vector>& mapping); 43 | std::vector> getInputMapping() const; 44 | 45 | void setOutputMapping(const std::vector>& mapping); 46 | std::vector> getOutputMapping() const; 47 | 48 | void initializeDefaultMapping(int numChannels); 49 | void resizeMappings(int numChannels); 50 | 51 | private: 52 | // Atomic shared pointer to channel mapping state 53 | atk::AtomicSharedPtr mappingState; 54 | }; 55 | 56 | } // namespace atk 57 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/SandboxedPluginScanner.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace atk 6 | { 7 | 8 | class SandboxedScanner : public juce::KnownPluginList::CustomScanner 9 | { 10 | public: 11 | SandboxedScanner(); 12 | ~SandboxedScanner() override; 13 | 14 | bool findPluginTypesFor( 15 | juce::AudioPluginFormat& format, 16 | juce::OwnedArray& result, 17 | const juce::String& fileOrIdentifier 18 | ) override; 19 | 20 | void scanFinished() override; 21 | 22 | bool isScannerAvailable() const 23 | { 24 | return scannerPath.existsAsFile(); 25 | } 26 | 27 | // Set the format manager to use for fallback scanning 28 | void setFormatManager(juce::AudioPluginFormatManager* manager) 29 | { 30 | formatManager = manager; 31 | } 32 | 33 | // Set the known plugin list for adding fallback-scanned plugins 34 | void setKnownPluginList(juce::KnownPluginList* list) 35 | { 36 | knownPluginList = list; 37 | } 38 | 39 | private: 40 | juce::File scannerPath; 41 | std::atomic shouldCancel{false}; 42 | int timeoutMs = 30000; 43 | 44 | // Track failed plugin scans for fallback option 45 | struct FailedScan 46 | { 47 | juce::String fileOrIdentifier; 48 | juce::String formatName; 49 | }; 50 | 51 | std::vector failedScans; 52 | 53 | // References for fallback scanning 54 | juce::AudioPluginFormatManager* formatManager = nullptr; 55 | juce::KnownPluginList* knownPluginList = nullptr; 56 | 57 | static juce::File findScannerExecutable(); 58 | static void showMissingScannerWarning(); 59 | void offerFallbackScan(); 60 | }; 61 | 62 | } // namespace atk 63 | -------------------------------------------------------------------------------- /lib/atkaudio/cmake/preconfig.cmake: -------------------------------------------------------------------------------- 1 | # Prefer system pkg-config over Linuxbrew on Linux (native builds only) 2 | # Only applies when Linuxbrew is installed and not cross-compiling 3 | if(UNIX AND NOT APPLE AND NOT CMAKE_CROSSCOMPILING) 4 | if(EXISTS "$ENV{HOME}/.linuxbrew" OR EXISTS "/home/linuxbrew/.linuxbrew") 5 | if(EXISTS "/usr/bin/pkg-config") 6 | # Force CMake to use system pkg-config executable 7 | set(PKG_CONFIG_EXECUTABLE "/usr/bin/pkg-config" CACHE FILEPATH "pkg-config executable" FORCE) 8 | # Override PKG_CONFIG_PATH to prioritize system libraries 9 | set(ENV{PKG_CONFIG_PATH} 10 | "/usr/lib/pkgconfig:/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/share/pkgconfig:/usr/local/lib/pkgconfig" 11 | ) 12 | message(STATUS "Using system pkg-config to avoid Linuxbrew library conflicts") 13 | endif() 14 | endif() 15 | endif() 16 | 17 | find_package(Git REQUIRED) 18 | find_program(JQ_EXECUTABLE jq REQUIRED) 19 | 20 | execute_process( 21 | COMMAND 22 | git rev-parse --abbrev-ref HEAD 23 | WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} 24 | OUTPUT_VARIABLE GIT_BRANCH 25 | OUTPUT_STRIP_TRAILING_WHITESPACE 26 | ) 27 | 28 | if(${GIT_BRANCH} MATCHES "release/") 29 | string(REPLACE "release/" "" GIT_BRANCH_STRIPPED "${GIT_BRANCH}") 30 | 31 | file(READ "${CMAKE_SOURCE_DIR}/buildspec.json" DATA) 32 | string( 33 | JSON DATA 34 | SET ${DATA} 35 | version 36 | "\"${GIT_BRANCH_STRIPPED}\"" 37 | ) 38 | file(WRITE "${CMAKE_SOURCE_DIR}/buildspec.json" "${DATA}") 39 | 40 | execute_process( 41 | COMMAND 42 | jq . "${CMAKE_SOURCE_DIR}/buildspec.json" 43 | OUTPUT_VARIABLE FORMATTED_JSON 44 | OUTPUT_STRIP_TRAILING_WHITESPACE 45 | ) 46 | file(WRITE "${CMAKE_SOURCE_DIR}/buildspec.json" "${FORMATTED_JSON}") 47 | endif() 48 | -------------------------------------------------------------------------------- /lib/atkaudio/src/scanner/PluginScanner.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main(int argc, const char* argv[]) 5 | { 6 | if (argc < 2) 7 | { 8 | std::cerr << "Usage: " << argv[0] << " " << std::endl; 9 | return 1; 10 | } 11 | 12 | juce::ScopedJuceInitialiser_GUI juceInit; 13 | 14 | juce::AudioPluginFormatManager formatManager; 15 | juce::addDefaultFormatsToManager(formatManager); 16 | 17 | const juce::String identifier(argv[1]); 18 | 19 | // Find matching format 20 | juce::AudioPluginFormat* format = nullptr; 21 | for (int i = 0; i < formatManager.getNumFormats(); ++i) 22 | { 23 | if (formatManager.getFormat(i)->fileMightContainThisPluginType(identifier)) 24 | { 25 | format = formatManager.getFormat(i); 26 | break; 27 | } 28 | } 29 | 30 | juce::XmlElement xml("SCANRESULT"); 31 | 32 | if (!format) 33 | { 34 | xml.setAttribute("success", false); 35 | xml.setAttribute("error", "Unknown format: " + identifier); 36 | } 37 | else 38 | { 39 | juce::OwnedArray descriptions; 40 | format->findAllTypesForFile(descriptions, identifier); 41 | 42 | if (descriptions.isEmpty()) 43 | { 44 | xml.setAttribute("success", false); 45 | xml.setAttribute("error", "No plugins found: " + identifier); 46 | } 47 | else 48 | { 49 | xml.setAttribute("success", true); 50 | xml.setAttribute("identifier", identifier); 51 | xml.setAttribute("format", format->getName()); 52 | 53 | for (auto* desc : descriptions) 54 | xml.addChildElement(desc->createXml().release()); 55 | } 56 | } 57 | 58 | std::cout << xml.toString().toStdString() << std::endl; 59 | return 0; 60 | } 61 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/AtomicSharedPtr.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace atk 7 | { 8 | 9 | template 10 | class AtomicSharedPtr 11 | { 12 | public: 13 | AtomicSharedPtr() 14 | : ptr(nullptr) 15 | { 16 | } 17 | 18 | explicit AtomicSharedPtr(std::shared_ptr p) 19 | : ptr(nullptr) 20 | { 21 | store(std::move(p), std::memory_order_relaxed); 22 | } 23 | 24 | ~AtomicSharedPtr() 25 | { 26 | auto p = ptr.exchange(nullptr, std::memory_order_relaxed); 27 | delete p; 28 | } 29 | 30 | // Non-copyable and non-movable (like std::atomic) 31 | AtomicSharedPtr(const AtomicSharedPtr&) = delete; 32 | AtomicSharedPtr& operator=(const AtomicSharedPtr&) = delete; 33 | AtomicSharedPtr(AtomicSharedPtr&&) = delete; 34 | AtomicSharedPtr& operator=(AtomicSharedPtr&&) = delete; 35 | 36 | std::shared_ptr load(std::memory_order order = std::memory_order_seq_cst) const 37 | { 38 | auto p = ptr.load(order); 39 | if (p) 40 | return *p; 41 | return std::shared_ptr(); 42 | } 43 | 44 | void store(std::shared_ptr desired, std::memory_order order = std::memory_order_seq_cst) 45 | { 46 | auto newPtr = new std::shared_ptr(std::move(desired)); 47 | auto oldPtr = ptr.exchange(newPtr, order); 48 | delete oldPtr; 49 | } 50 | 51 | std::shared_ptr exchange(std::shared_ptr desired, std::memory_order order = std::memory_order_seq_cst) 52 | { 53 | auto newPtr = new std::shared_ptr(std::move(desired)); 54 | auto oldPtr = ptr.exchange(newPtr, order); 55 | std::shared_ptr result; 56 | if (oldPtr) 57 | { 58 | result = *oldPtr; 59 | delete oldPtr; 60 | } 61 | return result; 62 | } 63 | 64 | private: 65 | mutable std::atomic*> ptr; 66 | }; 67 | 68 | } // namespace atk 69 | -------------------------------------------------------------------------------- /lib/atkaudio/cmake/patches/apply-juce-patches.cmake: -------------------------------------------------------------------------------- 1 | # Apply JUCE patches - skips if already applied 2 | cmake_minimum_required(VERSION 3.16) 3 | 4 | find_package(Git REQUIRED) 5 | 6 | # Get the directory containing the patches (this script's directory) 7 | get_filename_component(PATCH_DIR "${CMAKE_CURRENT_LIST_DIR}" ABSOLUTE) 8 | 9 | # Find all juce*.patch files in this directory 10 | file(GLOB PATCHES "${PATCH_DIR}/juce*.patch") 11 | 12 | foreach(PATCH_FILE ${PATCHES}) 13 | get_filename_component(PATCH_NAME "${PATCH_FILE}" NAME) 14 | 15 | if(NOT EXISTS "${PATCH_FILE}") 16 | message(WARNING "Patch file not found: ${PATCH_FILE}") 17 | continue() 18 | endif() 19 | 20 | # Check if patch is already applied (reverse check succeeds if applied) 21 | execute_process( 22 | COMMAND 23 | ${GIT_EXECUTABLE} apply --reverse --check "${PATCH_FILE}" 24 | RESULT_VARIABLE REVERSE_CHECK_RESULT 25 | OUTPUT_QUIET 26 | ERROR_QUIET 27 | ) 28 | 29 | if(REVERSE_CHECK_RESULT EQUAL 0) 30 | message(STATUS "Patch already applied: ${PATCH_NAME}") 31 | else() 32 | # Check if patch can be applied 33 | execute_process( 34 | COMMAND 35 | ${GIT_EXECUTABLE} apply --check "${PATCH_FILE}" 36 | RESULT_VARIABLE CHECK_RESULT 37 | OUTPUT_QUIET 38 | ERROR_QUIET 39 | ) 40 | 41 | if(CHECK_RESULT EQUAL 0) 42 | # Apply the patch 43 | execute_process( 44 | COMMAND 45 | ${GIT_EXECUTABLE} apply "${PATCH_FILE}" 46 | RESULT_VARIABLE APPLY_RESULT 47 | ) 48 | 49 | if(APPLY_RESULT EQUAL 0) 50 | message(STATUS "Successfully applied patch: ${PATCH_NAME}") 51 | else() 52 | message(WARNING "Failed to apply patch: ${PATCH_NAME}") 53 | endif() 54 | else() 55 | message(STATUS "Patch cannot be applied (may be partially applied or conflict): ${PATCH_NAME}") 56 | endif() 57 | endif() 58 | endforeach() 59 | -------------------------------------------------------------------------------- /lib/atkaudio/cmake/linux/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Portable Linux installer for atkAudio Plugin 3 | # Installs to user's .config/obs-studio directory 4 | 5 | set -e 6 | 7 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 8 | INSTALL_DIR="$HOME/.config/obs-studio/plugins/atkaudio-pluginforobs" 9 | 10 | echo "================================================================================" 11 | echo "atkAudio Plugin - Portable Installer" 12 | echo "================================================================================" 13 | echo "" 14 | echo "This will install the plugin to your user directory:" 15 | echo " $INSTALL_DIR" 16 | echo "" 17 | read -p "Do you wish to continue? [y/N] " -n 1 -r 18 | echo 19 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then 20 | echo "Installation cancelled." 21 | exit 0 22 | fi 23 | 24 | echo "" 25 | echo "Installing atkAudio Plugin..." 26 | 27 | # Create installation directory 28 | mkdir -p "$INSTALL_DIR/bin/64bit" 29 | 30 | # Install plugin binary 31 | if [ -f "$SCRIPT_DIR/obs-plugins/64bit/atkaudio-pluginforobs.so" ]; then 32 | cp "$SCRIPT_DIR/obs-plugins/64bit/atkaudio-pluginforobs.so" "$INSTALL_DIR/bin/64bit/" 33 | echo "✓ Installed plugin binary" 34 | else 35 | echo "✗ Error: Plugin binary not found in $SCRIPT_DIR/obs-plugins/64bit/" 36 | exit 1 37 | fi 38 | 39 | # Install scanner (sandboxed plugin scanner executable) 40 | if [ -f "$SCRIPT_DIR/obs-plugins/64bit/atkaudio-pluginforobs_scanner" ]; then 41 | cp "$SCRIPT_DIR/obs-plugins/64bit/atkaudio-pluginforobs_scanner" "$INSTALL_DIR/bin/64bit/" 42 | chmod +x "$INSTALL_DIR/bin/64bit/atkaudio-pluginforobs_scanner" 43 | echo "✓ Installed plugin scanner" 44 | else 45 | echo "⚠ Warning: Scanner not found (plugin scanning will use in-process mode)" 46 | fi 47 | 48 | echo "" 49 | echo "================================================================================" 50 | echo "Installation complete!" 51 | echo "================================================================================" 52 | echo "Plugin installed to: $INSTALL_DIR" 53 | echo "" 54 | echo "To uninstall, run:" 55 | echo " rm -rf $INSTALL_DIR" 56 | echo "" 57 | 58 | -------------------------------------------------------------------------------- /cmake/windows/compilerconfig.cmake: -------------------------------------------------------------------------------- 1 | # CMake Windows compiler configuration module 2 | 3 | include_guard(GLOBAL) 4 | 5 | include(compiler_common) 6 | 7 | set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT ProgramDatabase) 8 | 9 | message(DEBUG "Current Windows API version: ${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}") 10 | if(CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION_MAXIMUM) 11 | message(DEBUG "Maximum Windows API version: ${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION_MAXIMUM}") 12 | endif() 13 | 14 | if(CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION VERSION_LESS 10.0.20348) 15 | message( 16 | FATAL_ERROR 17 | "OBS requires Windows 10 SDK version 10.0.20348.0 or more recent.\n" 18 | "Please download and install the most recent Windows platform SDK." 19 | ) 20 | endif() 21 | 22 | set(_obs_msvc_c_options 23 | /MP 24 | /Zc:__cplusplus 25 | /Zc:preprocessor 26 | ) 27 | set(_obs_msvc_cpp_options 28 | /MP 29 | /Zc:__cplusplus 30 | /Zc:preprocessor 31 | ) 32 | 33 | if(CMAKE_CXX_STANDARD GREATER_EQUAL 20) 34 | list(APPEND _obs_msvc_cpp_options /Zc:char8_t-) 35 | endif() 36 | 37 | # Configure RelWithDebInfo to behave like Debug (no optimizations) 38 | # This makes debugging easier while still maintaining a separate configuration 39 | set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$,$>:Debug>") 40 | 41 | add_compile_options( 42 | /W3 43 | /utf-8 44 | /Brepro 45 | /permissive- 46 | "$<$:${_obs_msvc_c_options}>" 47 | "$<$:${_obs_msvc_cpp_options}>" 48 | "$<$:${_obs_clang_c_options}>" 49 | "$<$:${_obs_clang_cxx_options}>" 50 | ) 51 | 52 | add_compile_definitions( 53 | UNICODE 54 | _UNICODE 55 | _CRT_SECURE_NO_WARNINGS 56 | _CRT_NONSTDC_NO_WARNINGS 57 | $<$:DEBUG> 58 | $<$:_DEBUG> 59 | $<$:_DEBUG> # Use Debug iterator debug level for RelWithDebInfo 60 | ) 61 | 62 | add_link_options( 63 | /DEBUG 64 | /Brepro 65 | ) 66 | 67 | if(CMAKE_COMPILE_WARNING_AS_ERROR) 68 | add_link_options(/WX) 69 | endif() 70 | -------------------------------------------------------------------------------- /cmake/macos/compilerconfig.cmake: -------------------------------------------------------------------------------- 1 | # CMake macOS compiler configuration module 2 | 3 | include_guard(GLOBAL) 4 | 5 | option(ENABLE_COMPILER_TRACE "Enable clang time-trace" OFF) 6 | mark_as_advanced(ENABLE_COMPILER_TRACE) 7 | 8 | if(NOT XCODE) 9 | message(FATAL_ERROR "Building OBS Studio on macOS requires Xcode generator.") 10 | endif() 11 | 12 | include(compiler_common) 13 | 14 | add_compile_options("$<$>:-fopenmp-simd>") 15 | 16 | # Enable dSYM generator for release builds 17 | string(APPEND CMAKE_C_FLAGS_RELEASE " -g") 18 | string(APPEND CMAKE_CXX_FLAGS_RELEASE " -g") 19 | string(APPEND CMAKE_OBJC_FLAGS_RELEASE " -g") 20 | string(APPEND CMAKE_OBJCXX_FLAGS_RELEASE " -g") 21 | 22 | string(APPEND CMAKE_C_FLAGS_RELWITHDEBINFO " -g") 23 | string(APPEND CMAKE_CXX_FLAGS_RELWITHDEBINFO " -g") 24 | string(APPEND CMAKE_OBJC_FLAGS_RELWITHDEBINFO " -g") 25 | string(APPEND CMAKE_OBJCXX_FLAGS_RELWITHDEBINFO " -g") 26 | 27 | # Default ObjC compiler options used by Xcode: 28 | # 29 | # * -Wno-implicit-atomic-properties 30 | # * -Wno-objc-interface-ivars 31 | # * -Warc-repeated-use-of-weak 32 | # * -Wno-arc-maybe-repeated-use-of-weak 33 | # * -Wimplicit-retain-self 34 | # * -Wduplicate-method-match 35 | # * -Wshadow 36 | # * -Wfloat-conversion 37 | # * -Wobjc-literal-conversion 38 | # * -Wno-selector 39 | # * -Wno-strict-selector-match 40 | # * -Wundeclared-selector 41 | # * -Wdeprecated-implementations 42 | # * -Wprotocol 43 | # * -Werror=block-capture-autoreleasing 44 | # * -Wrange-loop-analysis 45 | 46 | # Default ObjC++ compiler options used by Xcode: 47 | # 48 | # * -Wno-non-virtual-dtor 49 | 50 | add_compile_definitions( 51 | $<$>:$<$:DEBUG>> 52 | $<$>:$<$:_DEBUG>> 53 | $<$>:SIMDE_ENABLE_OPENMP> 54 | ) 55 | 56 | if(ENABLE_COMPILER_TRACE) 57 | add_compile_options( 58 | $<$>:-ftime-trace> 59 | "$<$:SHELL:-Xfrontend -debug-time-expression-type-checking>" 60 | "$<$:SHELL:-Xfrontend -debug-time-function-bodies>" 61 | ) 62 | add_link_options(LINKER:-print_statistics) 63 | endif() 64 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/SharedPluginList.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace atk 6 | { 7 | 8 | class SharedPluginList : public juce::DeletedAtShutdown 9 | { 10 | public: 11 | SharedPluginList() 12 | : fileLock("atkAudioSharedPluginList") 13 | { 14 | juce::PropertiesFile::Options options; 15 | options.applicationName = "atkAudio Shared"; 16 | options.filenameSuffix = "settings"; 17 | options.osxLibrarySubFolder = "Application Support"; 18 | options.folderName = "atkAudio Plugin"; 19 | options.processLock = &fileLock; 20 | appProperties.setStorageParameters(options); 21 | } 22 | 23 | ~SharedPluginList() override 24 | { 25 | clearSingletonInstance(); 26 | } 27 | 28 | juce::PropertiesFile* getPropertiesFile() 29 | { 30 | return appProperties.getUserSettings(); 31 | } 32 | 33 | void loadPluginList(juce::KnownPluginList& list, bool excludeInternalPlugins = false) 34 | { 35 | const juce::ScopedLock sl(lock); 36 | appProperties.getUserSettings()->reload(); 37 | 38 | auto saved = appProperties.getUserSettings()->getXmlValue("pluginList"); 39 | if (!saved) 40 | return; 41 | 42 | if (!excludeInternalPlugins) 43 | { 44 | list.recreateFromXml(*saved); 45 | return; 46 | } 47 | 48 | juce::KnownPluginList fullList; 49 | fullList.recreateFromXml(*saved); 50 | for (const auto& type : fullList.getTypes()) 51 | if (type.pluginFormatName != "Internal") 52 | list.addType(type); 53 | } 54 | 55 | void savePluginList(const juce::KnownPluginList& list) 56 | { 57 | const juce::ScopedLock sl(lock); 58 | if (auto xml = list.createXml()) 59 | { 60 | appProperties.getUserSettings()->setValue("pluginList", xml.get()); 61 | appProperties.saveIfNeeded(); 62 | } 63 | } 64 | 65 | JUCE_DECLARE_SINGLETON_SINGLETHREADED(SharedPluginList, false) 66 | 67 | private: 68 | juce::InterProcessLock fileLock; 69 | juce::CriticalSection lock; 70 | juce::ApplicationProperties appProperties; 71 | }; 72 | 73 | } // namespace atk 74 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/ModuleInfrastructure/MidiServer/MidiServerSettingsComponent.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "MidiServer.h" 4 | 5 | #include 6 | 7 | namespace atk 8 | { 9 | 10 | class MidiServerSettingsComponent 11 | : public juce::Component 12 | , private juce::MidiInputCallback 13 | , private juce::MidiKeyboardState::Listener 14 | , private juce::Timer 15 | { 16 | public: 17 | explicit MidiServerSettingsComponent(MidiClient* client); 18 | ~MidiServerSettingsComponent() override; 19 | 20 | void paint(juce::Graphics& g) override; 21 | void resized() override; 22 | 23 | MidiClientState getSubscriptionState() const; 24 | 25 | void setSubscriptionState(const MidiClientState& state); 26 | 27 | private: 28 | void updateDeviceLists(); 29 | void updateSubscriptions(); 30 | void sendMidiPanic(); 31 | 32 | void handleIncomingMidiMessage(juce::MidiInput* source, const juce::MidiMessage& message) override; 33 | 34 | void handleNoteOn(juce::MidiKeyboardState* source, int midiChannel, int midiNoteNumber, float velocity) override; 35 | void handleNoteOff(juce::MidiKeyboardState* source, int midiChannel, int midiNoteNumber, float velocity) override; 36 | 37 | void timerCallback() override; 38 | 39 | MidiClient* client; 40 | MidiServer* server; 41 | 42 | juce::Label inputsLabel; 43 | std::unique_ptr inputsViewport; 44 | std::unique_ptr inputsContainer; 45 | juce::OwnedArray inputToggles; 46 | 47 | juce::Label outputsLabel; 48 | std::unique_ptr outputsViewport; 49 | std::unique_ptr outputsContainer; 50 | juce::OwnedArray outputToggles; 51 | 52 | juce::Label keyboardLabel; 53 | std::unique_ptr keyboardState; 54 | std::unique_ptr keyboardComponent; 55 | std::unique_ptr panicButton; 56 | 57 | juce::Label monitorLabel; 58 | std::unique_ptr monitorTextEditor; 59 | 60 | juce::CriticalSection monitorMutex; 61 | juce::StringArray pendingMonitorMessages; 62 | int maxMonitorLines = 100; 63 | 64 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MidiServerSettingsComponent) 65 | }; 66 | 67 | } // namespace atk 68 | -------------------------------------------------------------------------------- /cmake/common/compiler_common.cmake: -------------------------------------------------------------------------------- 1 | # CMake common compiler options module 2 | 3 | include_guard(GLOBAL) 4 | 5 | # Set C and C++ language standards to C17 and C++23 6 | set(CMAKE_C_STANDARD 17) 7 | set(CMAKE_C_STANDARD_REQUIRED TRUE) 8 | set(CMAKE_CXX_STANDARD 23) 9 | set(CMAKE_CXX_STANDARD_REQUIRED TRUE) 10 | 11 | # Set symbols to be hidden by default for C and C++ 12 | set(CMAKE_C_VISIBILITY_PRESET hidden) 13 | set(CMAKE_CXX_VISIBILITY_PRESET hidden) 14 | set(CMAKE_VISIBILITY_INLINES_HIDDEN TRUE) 15 | 16 | # clang options for C, C++, ObjC, and ObjC++ 17 | set(_obs_clang_common_options 18 | -fno-strict-aliasing 19 | -Wno-trigraphs 20 | -Wno-missing-field-initializers 21 | -Wno-missing-prototypes 22 | -Werror=return-type 23 | -Wunreachable-code 24 | -Wquoted-include-in-framework-header 25 | -Wno-missing-braces 26 | -Wparentheses 27 | -Wswitch 28 | -Wno-unused-function 29 | -Wno-unused-label 30 | -Wunused-parameter 31 | -Wunused-variable 32 | -Wunused-value 33 | -Wempty-body 34 | -Wuninitialized 35 | -Wno-unknown-pragmas 36 | -Wfour-char-constants 37 | -Wconstant-conversion 38 | -Wno-conversion 39 | -Wint-conversion 40 | -Wbool-conversion 41 | -Wenum-conversion 42 | -Wnon-literal-null-conversion 43 | -Wsign-compare 44 | -Wshorten-64-to-32 45 | -Wpointer-sign 46 | -Wnewline-eof 47 | -Wno-implicit-fallthrough 48 | -Wdeprecated-declarations 49 | -Wno-sign-conversion 50 | -Winfinite-recursion 51 | -Wcomma 52 | -Wno-strict-prototypes 53 | -Wno-semicolon-before-method-body 54 | -Wformat-security 55 | -Wvla 56 | -Wno-error=shorten-64-to-32 57 | ) 58 | 59 | # clang options for C 60 | set(_obs_clang_c_options 61 | ${_obs_clang_common_options} 62 | -Wno-shadow 63 | -Wno-float-conversion 64 | ) 65 | 66 | # clang options for C++ 67 | set(_obs_clang_cxx_options 68 | ${_obs_clang_common_options} 69 | -Wno-non-virtual-dtor 70 | -Wno-overloaded-virtual 71 | -Wno-exit-time-destructors 72 | -Wno-shadow 73 | -Winvalid-offsetof 74 | -Wmove 75 | -Werror=block-capture-autoreleasing 76 | -Wrange-loop-analysis 77 | ) 78 | 79 | if(CMAKE_CXX_STANDARD GREATER_EQUAL 20) 80 | list(APPEND _obs_clang_cxx_options -fno-char8_t) 81 | endif() 82 | 83 | if(NOT DEFINED CMAKE_COMPILE_WARNING_AS_ERROR) 84 | set(CMAKE_COMPILE_WARNING_AS_ERROR OFF) 85 | endif() 86 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/PluginHost/Core/PluginHolder.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "HostAudioProcessor.h" 4 | #include 5 | 6 | class PluginHolder 7 | : private juce::Timer 8 | , private juce::Value::Listener 9 | { 10 | public: 11 | struct PluginInOuts 12 | { 13 | short numIns, numOuts; 14 | }; 15 | 16 | PluginHolder( 17 | juce::PropertySet* settingsToUse, 18 | bool takeOwnershipOfSettings = true, 19 | const juce::String& preferredDefaultDeviceName = juce::String(), 20 | const juce::AudioDeviceManager::AudioDeviceSetup* preferredSetupOptions = nullptr, 21 | const juce::Array& channels = juce::Array(), 22 | bool shouldAutoOpenMidiDevices = false 23 | ); 24 | 25 | ~PluginHolder() override; 26 | 27 | virtual void createPlugin(); 28 | virtual void deletePlugin(); 29 | 30 | int getNumInputChannels() const; 31 | int getNumOutputChannels() const; 32 | 33 | HostAudioProcessorImpl* getHostProcessor() const; 34 | 35 | void savePluginState(); 36 | void reloadPluginState(); 37 | void askUserToSaveState(const juce::String& fileSuffix = juce::String()); 38 | void askUserToLoadState(const juce::String& fileSuffix = juce::String()); 39 | 40 | void startPlaying(); 41 | void stopPlaying(); 42 | 43 | juce::Value& getMuteInputValue(); 44 | bool getProcessorHasPotentialFeedbackLoop() const; 45 | 46 | // Members 47 | juce::OptionalScopedPointer settings; 48 | std::unique_ptr processor; 49 | juce::Array channelConfiguration; 50 | 51 | bool processorHasPotentialFeedbackLoop = true; 52 | std::atomic muteInput{true}; 53 | juce::Value shouldMuteInput; 54 | juce::AudioBuffer emptyBuffer; 55 | bool autoOpenMidiDevices; 56 | 57 | private: 58 | void handleCreatePlugin(); 59 | void handleDeletePlugin(); 60 | void init(bool enableAudioInput, const juce::String& preferredDefaultDeviceName); 61 | 62 | juce::File getLastFile() const; 63 | void setLastFile(const juce::FileChooser& fc); 64 | static juce::String getFilePatterns(const juce::String& fileSuffix); 65 | 66 | void valueChanged(juce::Value& value) override; 67 | 68 | void timerCallback() override; 69 | 70 | std::unique_ptr options; 71 | std::unique_ptr stateFileChooser; 72 | juce::ScopedMessageBox messageBox; 73 | 74 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PluginHolder) 75 | }; 76 | -------------------------------------------------------------------------------- /cmake/windows/resources/installer-Windows.iss.in: -------------------------------------------------------------------------------- 1 | #define MyAppName "@CMAKE_PROJECT_NAME@" 2 | #define MyAppVersion "@CMAKE_PROJECT_VERSION@" 3 | #define MyAppPublisher "@PLUGIN_AUTHOR@" 4 | #define MyAppURL "@PLUGIN_WEBSITE@" 5 | #define MyPluginName "@_displayName@" 6 | #define MyAppId "@_windowsApp@" 7 | 8 | [Setup] 9 | ; NOTE: The value of AppId uniquely identifies this application. 10 | ; Do not use the same AppId value in installers for other applications. 11 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) 12 | AppId={#MyAppId} 13 | AppName={#MyPluginName} 14 | AppVersion={#MyAppVersion} 15 | AppPublisher={#MyAppPublisher} 16 | AppPublisherURL={#MyAppURL} 17 | AppSupportURL={#MyAppURL} 18 | AppUpdatesURL={#MyAppURL} 19 | DefaultDirName={autoappdata}\obs-studio\plugins\ 20 | DefaultGroupName={#MyPluginName} 21 | OutputBaseFilename={#MyAppName}-{#MyAppVersion}-Windows-Installer 22 | Compression=lzma 23 | SolidCompression=yes 24 | DirExistsWarning=no 25 | 26 | [Languages] 27 | Name: "english"; MessagesFile: "compiler:Default.isl" 28 | 29 | [Files] 30 | Source: "..\release\Package\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs 31 | Source: "..\INSTALLER-LICENSE"; Flags: dontcopy 32 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 33 | 34 | [Icons] 35 | Name: "{group}\{cm:ProgramOnTheWeb,{#MyAppName}}"; Filename: "{#MyAppURL}" 36 | Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}" 37 | 38 | [Code] 39 | procedure InitializeWizard(); 40 | var 41 | GPLText: AnsiString; 42 | Page: TOutputMsgMemoWizardPage; 43 | begin 44 | ExtractTemporaryFile('INSTALLER-LICENSE'); 45 | LoadStringFromFile(ExpandConstant('{tmp}\INSTALLER-LICENSE'), GPLText); 46 | Page := CreateOutputMsgMemoPage(wpWelcome, 47 | 'License Information', 'Please review the license terms before installing {#MyAppName}', 48 | 'Press Page Down to see the rest of the agreement. Once you are aware of your rights, click Next to continue.', 49 | String(GPLText) 50 | ); 51 | end; 52 | 53 | // credit where it's due : 54 | // following function come from https://github.com/Xaymar/obs-studio_amf-encoder-plugin/blob/master/%23Resources/Installer.in.iss#L45 55 | function GetDirName(Value: string): string; 56 | var 57 | InstallPath: string; 58 | begin 59 | // initialize default path, which will be returned when the following registry 60 | // key queries fail due to missing keys or for some different reason 61 | Result := '{autopf}\obs-studio'; 62 | // query the first registry value; if this succeeds, return the obtained value 63 | if RegQueryStringValue(HKLM32, 'SOFTWARE\OBS Studio', '', InstallPath) then 64 | Result := InstallPath 65 | end; -------------------------------------------------------------------------------- /src/Delay.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #define FILTER_NAME "atkAudio Delay" 5 | #define FILTER_ID "atkaudio_delay" 6 | #define MAX_DELAY_MS 10000.0 7 | 8 | #define S_GAIN_DB "ms" 9 | 10 | #define MT_ obs_module_text 11 | #define TEXT_GAIN_DB MT_("Delay") 12 | 13 | struct delay_data 14 | { 15 | obs_source_t* context; 16 | size_t channels; 17 | double sample_rate; 18 | 19 | atk::Delay delayProcessor; 20 | 21 | float delay; 22 | }; 23 | 24 | static const char* delay_name(void* unused) 25 | { 26 | UNUSED_PARAMETER(unused); 27 | return obs_module_text(FILTER_NAME); 28 | } 29 | 30 | static void delay_destroy(void* data) 31 | { 32 | struct delay_data* df = (struct delay_data*)data; 33 | delete df; 34 | } 35 | 36 | static void delay_update(void* data, obs_data_t* s) 37 | { 38 | struct delay_data* df = (struct delay_data*)data; 39 | double val = obs_data_get_double(s, S_GAIN_DB); 40 | df->delay = (float)val; 41 | } 42 | 43 | static void* delay_create(obs_data_t* settings, obs_source_t* filter) 44 | { 45 | struct delay_data* df = new delay_data(); 46 | df->context = filter; 47 | 48 | df->channels = audio_output_get_channels(obs_get_audio()); 49 | df->sample_rate = audio_output_get_sample_rate(obs_get_audio()); 50 | 51 | delay_update(df, settings); 52 | 53 | return df; 54 | } 55 | 56 | static struct obs_audio_data* delay_filter_audio(void* data, struct obs_audio_data* audio) 57 | { 58 | struct delay_data* df = (struct delay_data*)data; 59 | const size_t channels = df->channels; 60 | float** adata = (float**)audio->data; 61 | 62 | df->delayProcessor.setDelay(df->delay); 63 | df->delayProcessor.process(adata, (int)channels, audio->frames, df->sample_rate); 64 | 65 | return audio; 66 | } 67 | 68 | static void delay_defaults(obs_data_t* s) 69 | { 70 | obs_data_set_default_double(s, S_GAIN_DB, 0.0f); 71 | } 72 | 73 | static obs_properties_t* delay_properties(void* data) 74 | { 75 | obs_properties_t* ppts = obs_properties_create(); 76 | 77 | obs_property_t* p = obs_properties_add_float_slider(ppts, S_GAIN_DB, TEXT_GAIN_DB, 0.0, MAX_DELAY_MS, 0.1); 78 | obs_property_float_set_suffix(p, " ms"); 79 | 80 | UNUSED_PARAMETER(data); 81 | return ppts; 82 | } 83 | 84 | struct obs_source_info delay_filter = { 85 | .id = FILTER_ID, 86 | .type = OBS_SOURCE_TYPE_FILTER, 87 | .output_flags = OBS_SOURCE_AUDIO, 88 | .get_name = delay_name, 89 | .create = delay_create, 90 | .destroy = delay_destroy, 91 | .get_defaults = delay_defaults, 92 | .get_properties = delay_properties, 93 | .update = delay_update, 94 | .filter_audio = delay_filter_audio, 95 | }; -------------------------------------------------------------------------------- /cmake/linux/FindLibObs.cmake: -------------------------------------------------------------------------------- 1 | # FindLibObs.cmake - Find OBS Studio libraries 2 | # This module finds the OBS Studio libraries, particularly useful for cross-compilation 3 | # where CMake config files may not be available in multiarch directories. 4 | # 5 | # This module defines: 6 | # LibObs_FOUND - System has OBS Studio libraries 7 | # LibObs_INCLUDE_DIRS - The OBS Studio include directories 8 | # LibObs_LIBRARIES - The libraries needed to use OBS Studio 9 | # LibObs_DEFINITIONS - Compiler switches required for using OBS Studio 10 | # 11 | # And creates the following imported targets: 12 | # libobs - The main OBS library 13 | 14 | include(FindPackageHandleStandardArgs) 15 | 16 | # Try pkg-config first 17 | find_package(PkgConfig QUIET) 18 | if(PKG_CONFIG_FOUND) 19 | pkg_check_modules(PC_LIBOBS QUIET libobs) 20 | set(LibObs_DEFINITIONS ${PC_LIBOBS_CFLAGS_OTHER}) 21 | endif() 22 | 23 | # Find the include directory 24 | find_path( 25 | LibObs_INCLUDE_DIR 26 | NAMES 27 | obs.h 28 | obs-module.h 29 | PATHS 30 | ${PC_LIBOBS_INCLUDE_DIRS} 31 | ${LIBOBS_INCLUDE_PATH} 32 | /usr/include 33 | /usr/local/include 34 | ${CMAKE_SYSROOT}/usr/include 35 | PATH_SUFFIXES 36 | obs 37 | libobs 38 | ) 39 | 40 | # Find the library 41 | find_library( 42 | LibObs_LIBRARY 43 | NAMES 44 | obs 45 | libobs 46 | PATHS 47 | ${PC_LIBOBS_LIBRARY_DIRS} 48 | ${LIBOBS_LIB_PATH} 49 | /usr/lib/${CMAKE_LIBRARY_ARCHITECTURE} 50 | /usr/lib 51 | /usr/local/lib 52 | ${CMAKE_SYSROOT}/usr/lib/${CMAKE_LIBRARY_ARCHITECTURE} 53 | ${CMAKE_SYSROOT}/usr/lib 54 | ) 55 | 56 | # Handle the QUIETLY and REQUIRED arguments 57 | find_package_handle_standard_args( 58 | LibObs 59 | REQUIRED_VARS 60 | LibObs_LIBRARY 61 | LibObs_INCLUDE_DIR 62 | VERSION_VAR PC_LIBOBS_VERSION 63 | ) 64 | 65 | if(LibObs_FOUND) 66 | set(LibObs_LIBRARIES ${LibObs_LIBRARY}) 67 | set(LibObs_INCLUDE_DIRS ${LibObs_INCLUDE_DIR}) 68 | 69 | # Create imported target 70 | if(NOT TARGET libobs) 71 | add_library(libobs UNKNOWN IMPORTED) 72 | set_target_properties( 73 | libobs 74 | PROPERTIES 75 | IMPORTED_LOCATION 76 | "${LibObs_LIBRARY}" 77 | INTERFACE_INCLUDE_DIRECTORIES 78 | "${LibObs_INCLUDE_DIR}" 79 | INTERFACE_COMPILE_OPTIONS 80 | "${LibObs_DEFINITIONS}" 81 | ) 82 | endif() 83 | 84 | # Also create OBS::libobs alias for compatibility 85 | if(NOT TARGET OBS::libobs) 86 | add_library(OBS::libobs ALIAS libobs) 87 | endif() 88 | 89 | mark_as_advanced( 90 | LibObs_INCLUDE_DIR 91 | LibObs_LIBRARY 92 | ) 93 | endif() 94 | -------------------------------------------------------------------------------- /cmake/linux/compilerconfig.cmake: -------------------------------------------------------------------------------- 1 | # CMake Linux compiler configuration module 2 | 3 | include_guard(GLOBAL) 4 | 5 | include(compiler_common) 6 | 7 | option(ENABLE_COMPILER_TRACE "Enable Clang time-trace (required Clang and Ninja)" OFF) 8 | mark_as_advanced(ENABLE_COMPILER_TRACE) 9 | 10 | # gcc options for C 11 | set(_obs_gcc_c_options 12 | -fno-strict-aliasing 13 | -fopenmp-simd 14 | -Wdeprecated-declarations 15 | -Wempty-body 16 | -Wenum-conversion 17 | -Werror=return-type 18 | -Wextra 19 | -Wformat 20 | -Wformat-security 21 | -Wno-conversion 22 | -Wno-float-conversion 23 | -Wno-implicit-fallthrough 24 | -Wno-missing-braces 25 | -Wno-missing-field-initializers 26 | -Wno-shadow 27 | -Wno-sign-conversion 28 | -Wno-trigraphs 29 | -Wno-unknown-pragmas 30 | -Wno-unused-function 31 | -Wno-unused-label 32 | -Wparentheses 33 | -Wuninitialized 34 | -Wunreachable-code 35 | -Wunused-parameter 36 | -Wunused-value 37 | -Wunused-variable 38 | -Wvla 39 | ) 40 | 41 | add_compile_options( 42 | -fopenmp-simd 43 | "$<$:${_obs_gcc_c_options}>" 44 | "$<$:-Wint-conversion;-Wno-missing-prototypes;-Wno-strict-prototypes;-Wpointer-sign>" 45 | "$<$:${_obs_gcc_c_options}>" 46 | "$<$:-Winvalid-offsetof;-Wno-overloaded-virtual>" 47 | "$<$:${_obs_clang_c_options}>" 48 | "$<$:${_obs_clang_cxx_options}>" 49 | ) 50 | 51 | if(CMAKE_CXX_COMPILER_ID STREQUAL GNU) 52 | # * Disable false-positive warning in GCC 12.1.0 and later 53 | # * https://gcc.gnu.org/bugzilla/show_bug.cgi?id=105562 54 | if(CMAKE_C_COMPILER_VERSION VERSION_GREATER_EQUAL 12.1.0) 55 | add_compile_options(-Wno-error=maybe-uninitialized) 56 | endif() 57 | 58 | # * Add warning for infinite recursion (added in GCC 12) 59 | # * Also disable warnings for stringop-overflow due to https://gcc.gnu.org/bugzilla/show_bug.cgi?id=106297 60 | if(CMAKE_C_COMPILER_VERSION VERSION_GREATER_EQUAL 12.0.0) 61 | add_compile_options( 62 | -Winfinite-recursion 63 | -Wno-stringop-overflow 64 | ) 65 | endif() 66 | endif() 67 | 68 | # Enable compiler and build tracing (requires Ninja generator) 69 | if(ENABLE_COMPILER_TRACE AND CMAKE_GENERATOR STREQUAL "Ninja") 70 | add_compile_options( 71 | $<$:-ftime-trace> 72 | $<$:-ftime-trace> 73 | ) 74 | else() 75 | set(ENABLE_COMPILER_TRACE OFF CACHE STRING "Enable Clang time-trace (required Clang and Ninja)" FORCE) 76 | endif() 77 | 78 | add_compile_definitions( 79 | $<$:DEBUG> 80 | $<$:_DEBUG> 81 | ) 82 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/PluginHost2/Core/DelayLinePlugin.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | class DelayLinePlugin; 6 | 7 | class DelayLineEditor final : public juce::AudioProcessorEditor 8 | { 9 | public: 10 | explicit DelayLineEditor(DelayLinePlugin& p); 11 | void resized() override; 12 | 13 | private: 14 | DelayLinePlugin& processor; 15 | 16 | juce::Label delayLabel; 17 | juce::Slider delaySlider; 18 | juce::AudioProcessorValueTreeState::SliderAttachment delayAttachment; 19 | 20 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(DelayLineEditor) 21 | }; 22 | 23 | class DelayLinePlugin final : public juce::AudioProcessor 24 | { 25 | public: 26 | DelayLinePlugin(); 27 | 28 | juce::AudioProcessorValueTreeState& getApvts() 29 | { 30 | return *apvts; 31 | } 32 | 33 | void prepareToPlay(double sampleRate, int samplesPerBlock) override; 34 | void releaseResources() override; 35 | void processBlock(juce::AudioBuffer& buffer, juce::MidiBuffer&) override; 36 | 37 | juce::AudioProcessorEditor* createEditor() override; 38 | 39 | bool hasEditor() const override 40 | { 41 | return true; 42 | } 43 | 44 | const juce::String getName() const override 45 | { 46 | return "Delay Line"; 47 | } 48 | 49 | bool acceptsMidi() const override 50 | { 51 | return false; 52 | } 53 | 54 | bool producesMidi() const override 55 | { 56 | return false; 57 | } 58 | 59 | double getTailLengthSeconds() const override 60 | { 61 | return 10.0; 62 | } 63 | 64 | int getNumPrograms() override 65 | { 66 | return 1; 67 | } 68 | 69 | int getCurrentProgram() override 70 | { 71 | return 0; 72 | } 73 | 74 | void setCurrentProgram(int) override 75 | { 76 | } 77 | 78 | const juce::String getProgramName(int) override 79 | { 80 | return "None"; 81 | } 82 | 83 | void changeProgramName(int, const juce::String&) override 84 | { 85 | } 86 | 87 | void getStateInformation(juce::MemoryBlock& destData) override; 88 | void setStateInformation(const void* data, int sizeInBytes) override; 89 | 90 | bool isBusesLayoutSupported(const BusesLayout& layouts) const override; 91 | 92 | private: 93 | static juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout(); 94 | 95 | std::unique_ptr apvts; 96 | std::atomic* delayMsValue = nullptr; 97 | 98 | juce::dsp::DelayLine delayLine; 99 | juce::LinearSmoothedValue delaySmoothed; 100 | int maxDelaySamples = 0; 101 | 102 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(DelayLinePlugin) 103 | }; 104 | -------------------------------------------------------------------------------- /cmake/linux/defaults.cmake: -------------------------------------------------------------------------------- 1 | # CMake Linux defaults module 2 | 3 | include_guard(GLOBAL) 4 | 5 | # Set default installation directories 6 | include(GNUInstallDirs) 7 | 8 | if(CMAKE_INSTALL_LIBDIR MATCHES "(CMAKE_SYSTEM_PROCESSOR)") 9 | string(REPLACE "CMAKE_SYSTEM_PROCESSOR" "${CMAKE_SYSTEM_PROCESSOR}" CMAKE_INSTALL_LIBDIR "${CMAKE_INSTALL_LIBDIR}") 10 | endif() 11 | 12 | # Set default install prefix to /usr for Linux (where OBS looks for plugins) 13 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 14 | set(CMAKE_INSTALL_PREFIX "/usr" CACHE PATH "Default install prefix" FORCE) 15 | endif() 16 | 17 | # Enable find_package targets to become globally available targets 18 | set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL TRUE) 19 | 20 | find_package(libobs QUIET) 21 | 22 | if(NOT TARGET OBS::libobs) 23 | find_package(LibObs REQUIRED) 24 | add_library(OBS::libobs ALIAS libobs) 25 | 26 | if(ENABLE_FRONTEND_API) 27 | find_path( 28 | obs-frontend-api_INCLUDE_DIR 29 | NAMES 30 | obs-frontend-api.h 31 | PATHS 32 | /usr/include 33 | /usr/local/include 34 | PATH_SUFFIXES 35 | obs 36 | ) 37 | 38 | find_library( 39 | obs-frontend-api_LIBRARY 40 | NAMES 41 | obs-frontend-api 42 | PATHS 43 | /usr/lib 44 | /usr/local/lib 45 | ) 46 | 47 | if(obs-frontend-api_LIBRARY) 48 | if(NOT TARGET OBS::obs-frontend-api) 49 | if(IS_ABSOLUTE "${obs-frontend-api_LIBRARY}") 50 | add_library(OBS::obs-frontend-api UNKNOWN IMPORTED) 51 | set_property( 52 | TARGET 53 | OBS::obs-frontend-api 54 | PROPERTY 55 | IMPORTED_LOCATION 56 | "${obs-frontend-api_LIBRARY}" 57 | ) 58 | else() 59 | add_library(OBS::obs-frontend-api INTERFACE IMPORTED) 60 | set_property( 61 | TARGET 62 | OBS::obs-frontend-api 63 | PROPERTY 64 | IMPORTED_LIBNAME 65 | "${obs-frontend-api_LIBRARY}" 66 | ) 67 | endif() 68 | 69 | set_target_properties( 70 | OBS::obs-frontend-api 71 | PROPERTIES 72 | INTERFACE_INCLUDE_DIRECTORIES 73 | "${obs-frontend-api_INCLUDE_DIR}" 74 | ) 75 | endif() 76 | endif() 77 | endif() 78 | 79 | macro(find_package) 80 | if(NOT "${ARGV0}" STREQUAL libobs AND NOT "${ARGV0}" STREQUAL obs-frontend-api) 81 | _find_package(${ARGV}) 82 | endif() 83 | endmacro() 84 | endif() 85 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/atkAudioModule.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "atkaudio.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace atk 11 | { 12 | 13 | class atkAudioModule 14 | { 15 | public: 16 | atkAudioModule() = default; 17 | virtual ~atkAudioModule() = default; 18 | 19 | atkAudioModule(const atkAudioModule&) = delete; 20 | atkAudioModule& operator=(const atkAudioModule&) = delete; 21 | 22 | virtual void process(float** buffer, int numChannels, int numSamples, double sampleRate) = 0; 23 | virtual void getState(std::string& state) = 0; 24 | virtual void setState(std::string& state) = 0; 25 | 26 | virtual void setVisible(bool visible) 27 | { 28 | auto doUi = [this, visible]() 29 | { 30 | auto* window = getWindowComponent(); 31 | if (!window) 32 | return; 33 | 34 | if (visible) 35 | { 36 | if (!window->isOnDesktop()) 37 | { 38 | if (auto* tlw = dynamic_cast(window)) 39 | tlw->addToDesktop(); 40 | else 41 | window->addToDesktop(0); 42 | } 43 | 44 | window->setVisible(true); 45 | window->toFront(true); 46 | 47 | if (auto* docWindow = dynamic_cast(window)) 48 | if (docWindow->isMinimised()) 49 | docWindow->setMinimised(false); 50 | } 51 | else 52 | { 53 | window->setVisible(false); 54 | } 55 | }; 56 | 57 | if (juce::MessageManager::getInstance()->isThisTheMessageThread()) 58 | doUi(); 59 | else 60 | juce::MessageManager::callAsync(doUi); 61 | } 62 | 63 | template 64 | static void destroyOnMessageThread(DestroyFunc&& destroyer, int timeoutMs = 200) 65 | { 66 | auto* mm = juce::MessageManager::getInstanceWithoutCreating(); 67 | if (mm == nullptr) 68 | { 69 | destroyer(); 70 | return; 71 | } 72 | 73 | if (mm->isThisTheMessageThread()) 74 | { 75 | destroyer(); 76 | } 77 | else 78 | { 79 | auto completionEvent = std::make_shared(true); 80 | mm->callAsync( 81 | [destroyer = std::forward(destroyer), completionEvent]() mutable 82 | { 83 | destroyer(); 84 | completionEvent->signal(); 85 | } 86 | ); 87 | completionEvent->wait(timeoutMs); 88 | } 89 | } 90 | 91 | protected: 92 | virtual juce::Component* getWindowComponent() = 0; 93 | }; 94 | 95 | } // namespace atk 96 | -------------------------------------------------------------------------------- /lib/atkaudio/src/scanner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Plugin Scanner - Sandboxed executable for safe plugin scanning 2 | cmake_minimum_required(VERSION 3.28) 3 | 4 | if(NOT TARGET juce::juce_audio_processors) 5 | message(FATAL_ERROR "JUCE must be available. Build from parent CMakeLists.txt") 6 | endif() 7 | 8 | add_executable(${CMAKE_PROJECT_NAME}_scanner PluginScanner.cpp) 9 | 10 | target_link_libraries( 11 | ${CMAKE_PROJECT_NAME}_scanner 12 | PRIVATE 13 | juce::juce_audio_utils 14 | juce::juce_recommended_config_flags 15 | juce::juce_recommended_lto_flags 16 | ) 17 | 18 | target_compile_definitions( 19 | ${CMAKE_PROJECT_NAME}_scanner 20 | PRIVATE 21 | JUCE_STANDALONE_APPLICATION=1 22 | JUCE_PLUGINHOST_VST3=1 23 | JUCE_PLUGINHOST_VST=0 24 | JUCE_PLUGINHOST_AU=1 25 | JUCE_USE_CURL=0 26 | JUCE_WEB_BROWSER=0 27 | ) 28 | 29 | if(UNIX AND NOT APPLE) 30 | target_compile_definitions( 31 | ${CMAKE_PROJECT_NAME}_scanner 32 | PRIVATE 33 | JUCE_PLUGINHOST_LV2=1 34 | JUCE_PLUGINHOST_LADSPA=1 35 | ) 36 | target_link_libraries(${CMAKE_PROJECT_NAME}_scanner PRIVATE pthread) 37 | endif() 38 | 39 | if(WIN32) 40 | set_target_properties( 41 | ${CMAKE_PROJECT_NAME}_scanner 42 | PROPERTIES 43 | WIN32_EXECUTABLE 44 | FALSE 45 | ) 46 | if(MSVC) 47 | target_compile_options( 48 | ${CMAKE_PROJECT_NAME}_scanner 49 | PRIVATE 50 | /wd4244 51 | /wd4267 52 | /wd4390 53 | /wd5105 54 | ) 55 | endif() 56 | endif() 57 | 58 | if(APPLE) 59 | set_target_properties( 60 | ${CMAKE_PROJECT_NAME}_scanner 61 | PROPERTIES 62 | MACOSX_BUNDLE 63 | FALSE 64 | ) 65 | # Ensure scanner builds before main plugin (copy happens in root CMakeLists.txt) 66 | add_dependencies(${CMAKE_PROJECT_NAME} ${CMAKE_PROJECT_NAME}_scanner) 67 | endif() 68 | 69 | # Copy scanner next to plugin after build (for development/local testing) 70 | # Installation is handled by cpack.cmake for all platforms 71 | if(NOT APPLE) 72 | add_custom_command( 73 | TARGET ${CMAKE_PROJECT_NAME}_scanner 74 | POST_BUILD 75 | COMMAND 76 | ${CMAKE_COMMAND} -E copy_if_different $ 77 | $ 78 | COMMENT "Copying scanner to plugin directory" 79 | ) 80 | endif() 81 | 82 | # Strip debug symbols on Linux (scanner binary can be very large with JUCE) 83 | if(UNIX AND NOT APPLE) 84 | if(NOT CMAKE_STRIP) 85 | find_program(CMAKE_STRIP strip) 86 | endif() 87 | 88 | if(CMAKE_STRIP) 89 | add_custom_command( 90 | TARGET ${CMAKE_PROJECT_NAME}_scanner 91 | POST_BUILD 92 | COMMAND 93 | ${CMAKE_STRIP} --strip-debug $ 94 | COMMENT "Stripping debug symbols from scanner" 95 | ) 96 | endif() 97 | endif() 98 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/PluginHost2/Core/InternalPlugins.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | using namespace juce; 5 | 6 | #include "DeviceIo2Plugin.h" 7 | #include "ObsOutput.h" 8 | #include "ObsSource.h" 9 | #include "PluginGraph.h" 10 | 11 | class InternalPluginFormat final : public AudioPluginFormat 12 | { 13 | public: 14 | InternalPluginFormat(); 15 | 16 | const std::vector& getAllTypes() const; 17 | 18 | static String getIdentifier() 19 | { 20 | return "Internal"; 21 | } 22 | 23 | String getName() const override 24 | { 25 | return getIdentifier(); 26 | } 27 | 28 | bool fileMightContainThisPluginType(const String&) override 29 | { 30 | return true; 31 | } 32 | 33 | FileSearchPath getDefaultLocationsToSearch() override 34 | { 35 | return {}; 36 | } 37 | 38 | bool canScanForPlugins() const override 39 | { 40 | return false; 41 | } 42 | 43 | bool isTrivialToScan() const override 44 | { 45 | return true; 46 | } 47 | 48 | void findAllTypesForFile(OwnedArray&, const String&) override 49 | { 50 | } 51 | 52 | bool doesPluginStillExist(const PluginDescription&) override 53 | { 54 | return true; 55 | } 56 | 57 | String getNameOfPluginFromIdentifier(const String& fileOrIdentifier) override 58 | { 59 | return fileOrIdentifier; 60 | } 61 | 62 | bool pluginNeedsRescanning(const PluginDescription&) override 63 | { 64 | return false; 65 | } 66 | 67 | StringArray searchPathsForPlugins(const FileSearchPath&, bool, bool) override 68 | { 69 | return {}; 70 | } 71 | 72 | private: 73 | class InternalPluginFactory 74 | { 75 | public: 76 | using Constructor = std::function()>; 77 | 78 | explicit InternalPluginFactory(const std::initializer_list& constructorsIn); 79 | 80 | const std::vector& getDescriptions() const 81 | { 82 | return descriptions; 83 | } 84 | 85 | std::unique_ptr createInstance(const String& name) const; 86 | 87 | private: 88 | const std::vector constructors; 89 | const std::vector descriptions; 90 | }; 91 | 92 | void createPluginInstance( 93 | const PluginDescription&, 94 | double initialSampleRate, 95 | int initialBufferSize, 96 | PluginCreationCallback 97 | ) override; 98 | 99 | std::unique_ptr createInstance(const String& name); 100 | 101 | bool requiresUnblockedMessageThreadDuringCreation(const PluginDescription&) const override; 102 | 103 | InternalPluginFactory factory; 104 | }; 105 | 106 | // Helper to set parent source UUID on internal plugins that need it (e.g., ObsSourceAudioProcessor) 107 | void setParentSourceUuidOnInternalPlugin(AudioPluginInstance* plugin, const std::string& parentUuid); 108 | -------------------------------------------------------------------------------- /cmake/common/bootstrap.cmake: -------------------------------------------------------------------------------- 1 | # Plugin bootstrap module 2 | 3 | include_guard(GLOBAL) 4 | 5 | # Map fallback configurations for optimized build configurations 6 | # gersemi: off 7 | set( 8 | CMAKE_MAP_IMPORTED_CONFIG_RELWITHDEBINFO 9 | RelWithDebInfo 10 | Release 11 | MinSizeRel 12 | None 13 | "" 14 | ) 15 | set( 16 | CMAKE_MAP_IMPORTED_CONFIG_MINSIZEREL 17 | MinSizeRel 18 | Release 19 | RelWithDebInfo 20 | None 21 | "" 22 | ) 23 | set( 24 | CMAKE_MAP_IMPORTED_CONFIG_RELEASE 25 | Release 26 | RelWithDebInfo 27 | MinSizeRel 28 | None 29 | "" 30 | ) 31 | # gersemi: on 32 | 33 | # Prohibit in-source builds 34 | if("${CMAKE_CURRENT_BINARY_DIR}" STREQUAL "${CMAKE_CURRENT_SOURCE_DIR}") 35 | message( 36 | FATAL_ERROR 37 | "In-source builds are not supported. " 38 | "Specify a build directory via 'cmake -S -B ' instead." 39 | ) 40 | file( 41 | REMOVE_RECURSE 42 | "${CMAKE_CURRENT_SOURCE_DIR}/CMakeCache.txt" 43 | "${CMAKE_CURRENT_SOURCE_DIR}/CMakeFiles" 44 | ) 45 | endif() 46 | 47 | # Add common module directories to default search path 48 | list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/common") 49 | 50 | file(READ "${CMAKE_CURRENT_SOURCE_DIR}/buildspec.json" buildspec) 51 | 52 | string(JSON _name GET ${buildspec} name) 53 | string(JSON _website GET ${buildspec} website) 54 | string(JSON _author GET ${buildspec} author) 55 | string(JSON _email GET ${buildspec} email) 56 | string(JSON _version GET ${buildspec} version) 57 | string( 58 | JSON _bundleId 59 | GET ${buildspec} 60 | platformConfig 61 | macos 62 | bundleId 63 | ) 64 | 65 | set(PLUGIN_AUTHOR ${_author}) 66 | set(PLUGIN_WEBSITE ${_website}) 67 | set(PLUGIN_EMAIL ${_email}) 68 | set(PLUGIN_VERSION ${_version}) 69 | set(MACOS_BUNDLEID ${_bundleId}) 70 | 71 | string(REPLACE "." ";" _version_canonical "${_version}") 72 | list(GET _version_canonical 0 PLUGIN_VERSION_MAJOR) 73 | list(GET _version_canonical 1 PLUGIN_VERSION_MINOR) 74 | list(GET _version_canonical 2 PLUGIN_VERSION_PATCH) 75 | unset(_version_canonical) 76 | 77 | include(osconfig) 78 | 79 | # Allow selection of common build types via UI 80 | if(NOT CMAKE_GENERATOR MATCHES "(Xcode|Visual Studio .+)") 81 | if(NOT CMAKE_BUILD_TYPE) 82 | set(CMAKE_BUILD_TYPE 83 | "RelWithDebInfo" 84 | CACHE STRING 85 | "OBS build type [Release, RelWithDebInfo, Debug, MinSizeRel]" 86 | FORCE 87 | ) 88 | set_property( 89 | CACHE 90 | CMAKE_BUILD_TYPE 91 | PROPERTY 92 | STRINGS 93 | Release 94 | RelWithDebInfo 95 | Debug 96 | MinSizeRel 97 | ) 98 | endif() 99 | endif() 100 | 101 | # Disable exports automatically going into the CMake package registry 102 | set(CMAKE_EXPORT_PACKAGE_REGISTRY FALSE) 103 | # Enable default inclusion of targets' source and binary directory 104 | set(CMAKE_INCLUDE_CURRENT_DIR TRUE) 105 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/Delay/Delay.cpp: -------------------------------------------------------------------------------- 1 | #include "../Delay.h" 2 | 3 | #include 4 | #include 5 | 6 | struct atk::Delay::Impl : public juce::Timer 7 | { 8 | std::atomic_bool isPrepared{false}; 9 | 10 | int numChannels = 2; 11 | int numSamples = 256; 12 | double sampleRate = 48000.0; 13 | 14 | std::vector> delayLine; 15 | std::vector> delayTimeSmooth; 16 | 17 | Impl() 18 | { 19 | startTimerHz(30); 20 | } 21 | 22 | ~Impl() 23 | { 24 | stopTimer(); 25 | } 26 | 27 | void timerCallback() override 28 | { 29 | if (!isPrepared.load(std::memory_order_acquire)) 30 | { 31 | prepare(this->numChannels, this->numSamples, this->sampleRate); 32 | isPrepared.store(true, std::memory_order_release); 33 | } 34 | } 35 | 36 | void prepare(int newNumChannels, int newNumSamples, double newSampleRate) 37 | { 38 | delayLine.clear(); 39 | delayLine.resize(newNumChannels); 40 | for (auto& i : delayLine) 41 | { 42 | i.prepare(juce::dsp::ProcessSpec({newSampleRate, (uint32_t)newNumSamples, (uint32_t)1})); 43 | i.reset(); 44 | i.setMaximumDelayInSamples(10 * (int)newSampleRate); 45 | i.setDelay(0.0f); 46 | } 47 | 48 | delayTimeSmooth.clear(); 49 | delayTimeSmooth.resize(newNumChannels); 50 | for (auto& i : delayTimeSmooth) 51 | i.reset(newSampleRate, 0.4f); 52 | 53 | isPrepared.store(true, std::memory_order_release); 54 | } 55 | 56 | void process(float** buffer, int newNumChannels, int newNumSamples, double newSampleRate) 57 | { 58 | if (this->numChannels != newNumChannels 59 | || this->numSamples != newNumSamples 60 | || this->sampleRate != newSampleRate) 61 | { 62 | this->numChannels = newNumChannels; 63 | this->numSamples = newNumSamples; 64 | this->sampleRate = newSampleRate; 65 | 66 | isPrepared.store(false, std::memory_order_release); 67 | 68 | return; 69 | } 70 | 71 | if (!isPrepared.load(std::memory_order_acquire)) 72 | return; 73 | 74 | for (int i = 0; i < newNumChannels; ++i) 75 | { 76 | for (int j = 0; j < newNumSamples; ++j) 77 | { 78 | delayLine[i].pushSample(0, buffer[i][j]); 79 | buffer[i][j] = delayLine[i].popSample(0, delayTimeSmooth[i].getNextValue()); 80 | } 81 | } 82 | } 83 | 84 | void setDelay(float delay) 85 | { 86 | for (auto& i : delayTimeSmooth) 87 | i.setTargetValue(delay / 1000.0f * (float)sampleRate); 88 | } 89 | }; 90 | 91 | void atk::Delay::process(float** buffer, int numChannels, int numSamples, double sampleRate) 92 | { 93 | pImpl->process(buffer, numChannels, numSamples, sampleRate); 94 | } 95 | 96 | void atk::Delay::setDelay(float delay) 97 | { 98 | pImpl->setDelay(delay); 99 | } 100 | 101 | atk::Delay::Delay() 102 | : pImpl(new Impl()) 103 | { 104 | } 105 | 106 | atk::Delay::~Delay() 107 | { 108 | delete pImpl; 109 | } 110 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/PluginHost/UI/HostEditorWindow.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../../LookAndFeel.h" 4 | 5 | #include "../Core/HostAudioProcessor.h" 6 | #include "../Core/PluginHolder.h" 7 | #include "PluginEditorComponent.h" 8 | #include "PluginLoaderComponent.h" 9 | #include "UICommon.h" 10 | 11 | #include 12 | #include 13 | 14 | class HostAudioProcessorEditor final : public juce::AudioProcessorEditor 15 | { 16 | public: 17 | explicit HostAudioProcessorEditor(HostAudioProcessorImpl& owner); 18 | 19 | void paint(juce::Graphics& g) override; 20 | void resized() override; 21 | void childBoundsChanged(juce::Component* child) override; 22 | 23 | void setFooterVisible(bool visible); 24 | 25 | juce::ComponentBoundsConstrainer* getPluginConstrainer() const; 26 | 27 | private: 28 | void pluginChanged(); 29 | void clearPlugin(); 30 | 31 | class SimpleDocumentWindow final : public juce::DocumentWindow 32 | { 33 | public: 34 | SimpleDocumentWindow(juce::Colour bg) 35 | : juce::DocumentWindow("Editor", bg, juce::DocumentWindow::allButtons) 36 | { 37 | setTitleBarButtonsRequired(juce::DocumentWindow::closeButton, false); 38 | } 39 | }; 40 | 41 | HostAudioProcessorImpl& hostProcessor; 42 | PluginLoaderComponent loader; 43 | std::unique_ptr editor; 44 | PluginEditorComponent* currentEditorComponent = nullptr; 45 | juce::ScopedValueSetter> scopedCallback; 46 | bool resizingFromChild = false; 47 | bool pendingFooterVisible = true; // Footer visibility to apply when plugin loads 48 | juce::SharedResourcePointer lookAndFeel; 49 | 50 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(HostAudioProcessorEditor) 51 | }; 52 | 53 | class HostEditorComponent final 54 | : public juce::Component 55 | , private juce::ComponentListener 56 | { 57 | public: 58 | HostEditorComponent(std::unique_ptr pluginHolderIn); 59 | 60 | ~HostEditorComponent() override; 61 | 62 | void paint(juce::Graphics& g) override; 63 | void resized() override; 64 | void childBoundsChanged(juce::Component* child) override; 65 | 66 | juce::AudioProcessor* getAudioProcessor() const noexcept; 67 | HostAudioProcessorImpl* getHostProcessor() const noexcept; 68 | juce::CriticalSection& getPluginHolderLock(); 69 | 70 | PluginHolder* getPluginHolder(); 71 | 72 | juce::ComponentBoundsConstrainer* getEditorConstrainer() const; 73 | 74 | void destroyUI(); 75 | void recreateUI(); 76 | 77 | void setFooterVisible(bool visible); 78 | 79 | void setIsDockedCallback(std::function callback) 80 | { 81 | getIsDocked = std::move(callback); 82 | } 83 | 84 | std::unique_ptr pluginHolder; 85 | 86 | private: 87 | class MainContentComponent; 88 | 89 | void updateContent(); 90 | void componentMovedOrResized(juce::Component& component, bool wasMoved, bool wasResized) override; 91 | 92 | juce::CriticalSection pluginHolderLock; 93 | std::unique_ptr contentComponent; 94 | juce::AudioProcessorEditor* editorToWatch = nullptr; 95 | bool resizingFromEditor = false; 96 | std::function getIsDocked; 97 | 98 | juce::SharedResourcePointer lookAndFeel; 99 | 100 | JUCE_DECLARE_NON_COPYABLE(HostEditorComponent) 101 | }; 102 | -------------------------------------------------------------------------------- /src/plugin-main.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Plugin Name 3 | Copyright (C) 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 2 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License along 16 | with this program. If not, see 17 | */ 18 | 19 | #include "CompareVersionStrings.h" 20 | #include "config.h" 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | 31 | OBS_DECLARE_MODULE() 32 | OBS_MODULE_USE_DEFAULT_LOCALE(PLUGIN_NAME, "en-US") 33 | 34 | const char* plugin_version = PLUGIN_VERSION; 35 | const char* plugin_name = PLUGIN_NAME; 36 | 37 | extern struct obs_source_info delay_filter; 38 | extern struct obs_source_info device_io_filter; 39 | extern struct obs_source_info device_io2_filter; 40 | extern struct obs_source_info pluginhost_filter; 41 | extern struct obs_source_info pluginhost2_filter; 42 | extern struct obs_source_info source_mixer; 43 | extern struct obs_source_info ph2helper_source_info; 44 | 45 | void obs_log(int log_level, const char* format, ...); 46 | 47 | #include 48 | 49 | bool obs_module_load(void) 50 | { 51 | std::string obsCurrentVersion = obs_get_version_string(); 52 | std::string requiredVersion = PLUGIN_OBS_VERSION_REQUIRED; 53 | 54 | if (CompareVersionStrings(obsCurrentVersion, requiredVersion) < 0) 55 | { 56 | obs_log( 57 | LOG_ERROR, 58 | "Incompatible OBS version: %s (required: %s)", 59 | obsCurrentVersion.c_str(), 60 | requiredVersion.c_str() 61 | ); 62 | return false; 63 | } 64 | 65 | obs_log(LOG_INFO, "plugin loaded successfully (version %s)", plugin_version); 66 | 67 | atk::create(); 68 | 69 | auto* mainWindow = (QObject*)obs_frontend_get_main_window(); 70 | atk::startMessagePump(mainWindow); 71 | 72 | atk::update(); 73 | 74 | obs_register_source(&delay_filter); 75 | obs_register_source(&device_io_filter); 76 | obs_register_source(&device_io2_filter); 77 | obs_register_source(&pluginhost2_filter); 78 | obs_register_source(&pluginhost_filter); 79 | obs_register_source(&source_mixer); 80 | obs_register_source(&ph2helper_source_info); 81 | 82 | return true; 83 | } 84 | 85 | void obs_module_unload(void) 86 | { 87 | obs_log(LOG_INFO, "Plugin unload started"); 88 | 89 | atk::destroy(); 90 | 91 | obs_log(LOG_INFO, "Plugin unloaded successfully"); 92 | } 93 | 94 | void obs_log(int log_level, const char* format, ...) 95 | { 96 | size_t length = 4 + strlen(plugin_name) + strlen(format); 97 | 98 | char* templ = (char*)malloc(length + 1); 99 | 100 | snprintf(templ, length, "[%s] %s", plugin_name, format); 101 | 102 | va_list args; 103 | 104 | va_start(args, format); 105 | blogva(log_level, templ, args); 106 | va_end(args); 107 | 108 | free(templ); 109 | } 110 | -------------------------------------------------------------------------------- /lib/atkaudio/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | file(GLOB_RECURSE _sources CONFIGURE_DEPENDS ./src/*.c*) 2 | 3 | target_sources(${PROJECT_NAME} PRIVATE ${_sources}) 4 | 5 | target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) 6 | 7 | # CMP0177 is only available in CMake 3.31+ 8 | if(POLICY CMP0177) 9 | cmake_policy(SET CMP0177 NEW) 10 | endif() 11 | 12 | set(target ${PROJECT_NAME}) 13 | 14 | # Always use JUCE modules only (no helper tools) 15 | set(JUCE_MODULES_ONLY ON CACHE BOOL "Only build JUCE modules" FORCE) 16 | 17 | # Store FetchContent downloads in _deps to persist across cache clears 18 | set(FETCHCONTENT_BASE_DIR "${CMAKE_SOURCE_DIR}/_deps" CACHE PATH "" FORCE) 19 | 20 | include(FetchContent) 21 | FetchContent_Declare( 22 | juce 23 | EXCLUDE_FROM_ALL 24 | GIT_REPOSITORY https://github.com/juce-framework/JUCE.git 25 | GIT_TAG 8.0.11 26 | PATCH_COMMAND 27 | ${CMAKE_COMMAND} -P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/patches/apply-juce-patches.cmake" 28 | ) 29 | FetchContent_MakeAvailable(juce) 30 | # _juce_initialise_target(${target} ${target}) 31 | 32 | target_link_libraries( 33 | ${PROJECT_NAME} 34 | PRIVATE 35 | juce::juce_audio_utils 36 | juce::juce_gui_extra 37 | juce::juce_dsp 38 | juce::juce_recommended_config_flags 39 | juce::juce_recommended_lto_flags 40 | # juce::juce_recommended_warning_flags 41 | OBS::obs-frontend-api 42 | ) 43 | 44 | target_compile_definitions( 45 | ${PROJECT_NAME} 46 | PRIVATE 47 | JUCE_STRICT_REFCOUNTEDPOINTER=1 48 | JUCE_STANDALONE_APPLICATION=1 49 | JUCE_USE_CUSTOM_PLUGIN_STANDALONE_APP=1 50 | JUCE_WIN_PER_MONITOR_DPI_AWARE=1 51 | JUCE_PLUGINHOST_VST3=1 52 | JUCE_PLUGINHOST_VST=0 53 | JUCE_PLUGINHOST_AU=1 54 | JUCE_USE_CURL=1 55 | JUCE_WEB_BROWSER=0 56 | JUCE_MODAL_LOOPS_PERMITTED=1 # we use QT event loop, so this is needed 57 | # Disable DBG() output in CI/GitHub Actions RelWithDebInfo builds (keeps debug symbols but no console spam in CI) 58 | $<$,$,$>>:JUCE_DISABLE_ASSERTIONS> 59 | # SIMULATE_UPDATE_CHECK # Uncomment to test update dialog without network 60 | ) 61 | 62 | set_target_properties( 63 | ${PROJECT_NAME} 64 | PROPERTIES 65 | JUCE_NEEDS_WEB_BROWSER 66 | FALSE 67 | JUCE_NEEDS_CURL 68 | TRUE 69 | JUCE_IS_PLUGIN 70 | TRUE 71 | ) 72 | 73 | if(UNIX AND NOT APPLE) 74 | target_compile_definitions( 75 | ${PROJECT_NAME} 76 | PRIVATE 77 | JUCE_PLUGINHOST_LV2=1 78 | JUCE_PLUGINHOST_LADSPA=1 79 | ) 80 | endif() 81 | 82 | if(MSVC) 83 | target_compile_options( 84 | ${PROJECT_NAME} 85 | PRIVATE 86 | /wd4244 87 | /wd4267 88 | /wd4390 89 | /wd5105 90 | ) 91 | endif() 92 | 93 | if(NOT WIN32) 94 | target_compile_options( 95 | ${PROJECT_NAME} 96 | PRIVATE 97 | -Wno-unused-parameter 98 | -Wno-sign-compare 99 | -Wno-unused-variable 100 | -Wno-newline-eof 101 | ) 102 | endif() 103 | 104 | if(WIN32) 105 | file(TO_CMAKE_PATH "${CMAKE_INSTALL_PREFIX}" _sanitized_prefix) 106 | string(REGEX REPLACE "/$" "" _sanitized_prefix "${_sanitized_prefix}") 107 | set(CMAKE_INSTALL_PREFIX "${_sanitized_prefix}" CACHE PATH "Sanitized install prefix" FORCE) 108 | endif() 109 | 110 | target_compile_definitions(${PROJECT_NAME} PRIVATE JUCE_ASIO=1) 111 | 112 | target_compile_definitions(${PROJECT_NAME} PRIVATE JUCE_ASIO=1) 113 | 114 | # Enable Qt integration for dockable windows 115 | target_compile_definitions(${PROJECT_NAME} PRIVATE ENABLE_QT) 116 | 117 | file(COPY_FILE ${CMAKE_SOURCE_DIR}/LICENSE ${CMAKE_BINARY_DIR}/INSTALLER-LICENSE) 118 | 119 | # Build the sandboxed plugin scanner executable 120 | add_subdirectory(src/scanner) 121 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/RealtimeThread.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 atkAudio 2 | 3 | #pragma once 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #ifdef _WIN32 10 | #ifndef NOMINMAX 11 | #define NOMINMAX 12 | #endif 13 | #include 14 | #elif defined(__linux__) || defined(__APPLE__) 15 | #include 16 | #include 17 | #ifdef __APPLE__ 18 | #include 19 | #include 20 | #endif 21 | #endif 22 | 23 | namespace atk 24 | { 25 | 26 | // Try to pin a thread to a specific CPU core. 27 | // Returns true if successful, false otherwise. 28 | // coreId should be in range [0, hardware_concurrency-1] 29 | inline bool tryPinThreadToCore(std::thread& t, int coreId) noexcept 30 | { 31 | // Clamp coreId into a valid logical core range; wrap if out-of-range 32 | const auto logical = (std::max)(1u, std::thread::hardware_concurrency()); 33 | if (coreId < 0) 34 | coreId = 0; 35 | if (coreId >= static_cast(logical)) 36 | coreId = coreId % static_cast(logical); 37 | 38 | bool ok = false; 39 | #ifdef _WIN32 40 | // Windows: Set thread affinity to a single core 41 | auto handle = t.native_handle(); 42 | if (coreId < static_cast(sizeof(DWORD_PTR) * 8)) 43 | { 44 | DWORD_PTR mask = static_cast(1) << coreId; 45 | ok = SetThreadAffinityMask(handle, mask) != 0; 46 | } 47 | 48 | #elif defined(__linux__) 49 | // Linux: Use pthread_setaffinity_np 50 | if (coreId < CPU_SETSIZE) 51 | { 52 | cpu_set_t cpuset; 53 | CPU_ZERO(&cpuset); 54 | CPU_SET(coreId, &cpuset); 55 | ok = pthread_setaffinity_np(t.native_handle(), sizeof(cpu_set_t), &cpuset) == 0; 56 | } 57 | 58 | #elif defined(__APPLE__) 59 | // macOS: Use thread_policy_set with THREAD_AFFINITY_POLICY 60 | // Note: macOS affinity is more of a hint, not a hard binding 61 | thread_affinity_policy_data_t policy = {coreId}; 62 | ok = thread_policy_set( 63 | pthread_mach_thread_np(t.native_handle()), 64 | THREAD_AFFINITY_POLICY, 65 | (thread_policy_t)&policy, 66 | THREAD_AFFINITY_POLICY_COUNT 67 | ) 68 | == KERN_SUCCESS; 69 | 70 | #else 71 | (void)t; 72 | (void)coreId; 73 | #endif 74 | 75 | if (!ok) 76 | std::cerr << "[atk::pin] Failed to pin thread to core " << coreId << std::endl; 77 | return ok; 78 | } 79 | 80 | // Try to set realtime/high priority on a thread. 81 | // Returns true if successful, false otherwise. 82 | // On failure, the thread continues with normal priority. 83 | inline bool trySetRealtimePriority(std::thread& t) noexcept 84 | { 85 | #ifdef _WIN32 86 | // Windows: Try TIME_CRITICAL first, fall back to HIGHEST 87 | auto handle = t.native_handle(); 88 | if (SetThreadPriority(handle, THREAD_PRIORITY_TIME_CRITICAL)) 89 | return true; 90 | return SetThreadPriority(handle, THREAD_PRIORITY_HIGHEST) != 0; 91 | 92 | #elif defined(__linux__) 93 | // Linux: Try SCHED_RR (realtime), requires CAP_SYS_NICE or root 94 | sched_param param{}; 95 | param.sched_priority = sched_get_priority_max(SCHED_RR); 96 | if (param.sched_priority > 0) 97 | { 98 | if (pthread_setschedparam(t.native_handle(), SCHED_RR, ¶m) == 0) 99 | return true; 100 | } 101 | // Fallback: try lower realtime priority 102 | param.sched_priority = sched_get_priority_min(SCHED_RR); 103 | return pthread_setschedparam(t.native_handle(), SCHED_RR, ¶m) == 0; 104 | 105 | #elif defined(__APPLE__) 106 | // macOS: Set highest SCHED_OTHER priority 107 | // True realtime on macOS requires Audio Unit workgroups 108 | sched_param param{}; 109 | param.sched_priority = sched_get_priority_max(SCHED_OTHER); 110 | return pthread_setschedparam(t.native_handle(), SCHED_OTHER, ¶m) == 0; 111 | 112 | #else 113 | (void)t; 114 | return false; 115 | #endif 116 | } 117 | 118 | } // namespace atk 119 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/PluginHost2/Core/PluginGraph.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | using namespace juce; 5 | 6 | #include "../UI/PluginWindow.h" 7 | #include "../../AudioProcessorGraphMT/AudioProcessorGraphMT.h" 8 | 9 | // Using AudioProcessorGraphMT from atk namespace 10 | using atk::AudioProcessorGraphMT; 11 | 12 | struct PluginDescriptionAndPreference 13 | { 14 | enum class UseARA 15 | { 16 | no, 17 | yes 18 | }; 19 | 20 | PluginDescriptionAndPreference() = default; 21 | 22 | explicit PluginDescriptionAndPreference(PluginDescription pd) 23 | : pluginDescription(std::move(pd)) 24 | , useARA( 25 | pluginDescription.hasARAExtension ? PluginDescriptionAndPreference::UseARA::yes 26 | : PluginDescriptionAndPreference::UseARA::no 27 | ) 28 | { 29 | } 30 | 31 | PluginDescriptionAndPreference(PluginDescription pd, UseARA ara) 32 | : pluginDescription(std::move(pd)) 33 | , useARA(ara) 34 | { 35 | } 36 | 37 | PluginDescription pluginDescription; 38 | UseARA useARA = UseARA::no; 39 | }; 40 | 41 | class PluginGraph final 42 | : public FileBasedDocument 43 | , public AudioProcessorListener 44 | , private ChangeListener 45 | { 46 | public: 47 | PluginGraph(MainHostWindow&, AudioPluginFormatManager&, KnownPluginList&); 48 | ~PluginGraph() override; 49 | 50 | using NodeID = AudioProcessorGraphMT::NodeID; 51 | 52 | void addPlugin(const PluginDescriptionAndPreference&, Point); 53 | 54 | AudioProcessorGraphMT::Node::Ptr getNodeForName(const String& name) const; 55 | 56 | void setNodePosition(NodeID, Point); 57 | Point getNodePosition(NodeID) const; 58 | 59 | void clear(); 60 | 61 | PluginWindow* getOrCreateWindowFor(AudioProcessorGraphMT::Node*, PluginWindow::Type); 62 | bool closeAnyOpenPluginWindows(); 63 | 64 | void audioProcessorParameterChanged(AudioProcessor*, int, float) override 65 | { 66 | } 67 | 68 | void audioProcessorChanged(AudioProcessor*, const ChangeDetails&) override 69 | { 70 | changed(); 71 | } 72 | 73 | std::unique_ptr createXml() const; 74 | void restoreFromXml(const XmlElement&); 75 | 76 | static const char* getFilenameSuffix() 77 | { 78 | return ".filtergraph"; 79 | } 80 | 81 | static const char* getFilenameWildcard() 82 | { 83 | return "*.filtergraph"; 84 | } 85 | 86 | void newDocument(); 87 | String getDocumentTitle() override; 88 | Result loadDocument(const File& file) override; 89 | Result saveDocument(const File& file) override; 90 | File getLastDocumentOpened() override; 91 | void setLastDocumentOpened(const File& file) override; 92 | 93 | static File getDefaultGraphDocumentOnMobile(); 94 | 95 | AudioProcessorGraphMT graph; 96 | 97 | static std::atomic activeInstanceCount; 98 | 99 | static bool hasActiveInstances() 100 | { 101 | return activeInstanceCount.load() > 0; 102 | } 103 | 104 | void processMidiInput(juce::MidiBuffer& midiMessages, int numSamples, double sampleRate); 105 | void processMidiOutput(const juce::MidiBuffer& midiMessages); 106 | 107 | MainHostWindow& getMainHostWindow() 108 | { 109 | return mainHostWindow; 110 | } 111 | 112 | private: 113 | MainHostWindow& mainHostWindow; 114 | AudioPluginFormatManager& formatManager; 115 | KnownPluginList& knownPlugins; 116 | OwnedArray activePluginWindows; 117 | ScopedMessageBox messageBox; 118 | 119 | NodeID lastUID; 120 | NodeID getNextUID() noexcept; 121 | 122 | void createNodeFromXml(const XmlElement&); 123 | void addPluginCallback( 124 | std::unique_ptr, 125 | const String& error, 126 | Point, 127 | PluginDescriptionAndPreference::UseARA useARA 128 | ); 129 | void changeListenerCallback(ChangeBroadcaster*) override; 130 | 131 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PluginGraph) 132 | }; 133 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/DeviceIo2/DeviceIo2App.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../FifoBuffer2.h" 4 | #include "../LookAndFeel.h" 5 | 6 | #include "DeviceIo2SettingsComponent.h" 7 | 8 | #include 9 | 10 | using namespace juce; 11 | 12 | class DeviceIo2App final : public AudioAppComponent 13 | { 14 | public: 15 | DeviceIo2App(AudioDeviceManager& deviceManager, int numInputChannels, int numOutputChannels, double obsSampleRate) 16 | : AudioAppComponent(deviceManager) 17 | , deviceManager(deviceManager) 18 | , settingsComponent(deviceManager, numInputChannels, numOutputChannels) 19 | { 20 | setAudioChannels(numInputChannels, numOutputChannels); 21 | settingsComponent.setSize(700, 600); // Larger size to accommodate routing matrix UI 22 | settingsComponent.setToRecommendedSize(); 23 | addAndMakeVisible(settingsComponent); 24 | 25 | deviceManager.initialise(0, 0, nullptr, false); 26 | 27 | setSize(settingsComponent.getWidth(), settingsComponent.getHeight()); 28 | (void)obsSampleRate; 29 | } 30 | 31 | ~DeviceIo2App() override 32 | { 33 | shutdownAudio(); 34 | } 35 | 36 | void prepareToPlay(int samplesPerBlockExpected, double newSampleRate) override 37 | { 38 | inputChannels = deviceManager.getCurrentAudioDevice()->getActiveInputChannels().countNumberOfSetBits(); 39 | outputChannels = deviceManager.getCurrentAudioDevice()->getActiveOutputChannels().countNumberOfSetBits(); 40 | sampleRate = newSampleRate; 41 | 42 | deviceInputBuffer.clearPrepared(); 43 | deviceOutputBuffer.clearPrepared(); 44 | } 45 | 46 | void getNextAudioBlock(const AudioSourceChannelInfo& bufferToFill) override 47 | { 48 | if (inputChannels > 0) 49 | deviceInputBuffer.write( 50 | bufferToFill.buffer->getArrayOfReadPointers(), 51 | inputChannels, 52 | bufferToFill.numSamples, 53 | sampleRate 54 | ); 55 | 56 | if (outputChannels > 0) 57 | deviceOutputBuffer.read( 58 | bufferToFill.buffer->getArrayOfWritePointers(), 59 | outputChannels, 60 | bufferToFill.numSamples, 61 | sampleRate 62 | ); 63 | } 64 | 65 | void releaseResources() override 66 | { 67 | } 68 | 69 | void paint(Graphics& g) override 70 | { 71 | (void)g; 72 | } 73 | 74 | void resized() override 75 | { 76 | } 77 | 78 | auto& getDeviceInputBuffer() 79 | { 80 | return deviceInputBuffer; 81 | } 82 | 83 | auto& getDeviceOutputBuffer() 84 | { 85 | return deviceOutputBuffer; 86 | } 87 | 88 | private: 89 | juce::AudioDeviceManager& deviceManager; 90 | juce::AudioDeviceManager::AudioDeviceSetup audioSetup; 91 | int inputChannels = 0; 92 | int outputChannels = 0; 93 | double sampleRate = 0.0; 94 | 95 | SyncBuffer deviceInputBuffer; 96 | SyncBuffer deviceOutputBuffer; 97 | 98 | DeviceIo2SettingsComponent settingsComponent; 99 | 100 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(DeviceIo2App) 101 | }; 102 | 103 | class AudioAppMainWindow final : public juce::DocumentWindow 104 | { 105 | public: 106 | AudioAppMainWindow(DeviceIo2App& demo) 107 | : juce::DocumentWindow("", Colours::lightgrey, DocumentWindow::allButtons) 108 | , audioApp(demo) 109 | { 110 | setTitleBarButtonsRequired(DocumentWindow::minimiseButton | DocumentWindow::closeButton, false); 111 | setContentOwned(&demo, true); 112 | setResizable(true, false); 113 | centreWithSize(demo.getWidth(), demo.getHeight()); 114 | removeFromDesktop(); 115 | } 116 | 117 | ~AudioAppMainWindow() override 118 | { 119 | } 120 | 121 | void closeButtonPressed() override 122 | { 123 | setVisible(false); 124 | } 125 | 126 | private: 127 | DeviceIo2App& audioApp; 128 | juce::SharedResourcePointer lookAndFeel; 129 | 130 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AudioAppMainWindow) 131 | }; 132 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/FifoBuffer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace atk 9 | { 10 | 11 | // Simple lock-free FIFO buffer for audio 12 | class FifoBuffer 13 | { 14 | public: 15 | FifoBuffer() 16 | : numChannels(0) 17 | , totalSize(0) 18 | , readPos(0) 19 | , writePos(0) 20 | { 21 | } 22 | 23 | void reset() 24 | { 25 | readPos.store(0, std::memory_order_release); 26 | writePos.store(0, std::memory_order_release); 27 | for (auto& channel : buffer) 28 | std::fill(channel.begin(), channel.end(), 0.0f); 29 | } 30 | 31 | void read(float* dest, int channel, int numSamples, bool advance = true) 32 | { 33 | if (channel >= numChannels || numSamples <= 0) 34 | return; 35 | 36 | const int readPosition = readPos.load(std::memory_order_acquire); 37 | const int available = getNumReady(); 38 | const int toRead = std::min(numSamples, available); 39 | 40 | if (toRead <= 0) 41 | return; 42 | 43 | const int start1 = readPosition; 44 | const int size1 = std::min(toRead, totalSize - start1); 45 | const int size2 = toRead - size1; 46 | 47 | const float* src = buffer[channel].data(); 48 | if (size1 > 0) 49 | std::copy(src + start1, src + start1 + size1, dest); 50 | 51 | if (size2 > 0) 52 | std::copy(src, src + size2, dest + size1); 53 | 54 | if (advance) 55 | readPos.store((readPosition + toRead) % totalSize, std::memory_order_release); 56 | } 57 | 58 | void write(const float* data, int channel, int numSamples, bool advance = true) 59 | { 60 | if (channel >= numChannels || numSamples <= 0) 61 | return; 62 | 63 | const int writePosition = writePos.load(std::memory_order_acquire); 64 | const int freeSpace = getFreeSpace(); 65 | const int toWrite = std::min(numSamples, freeSpace); 66 | 67 | if (toWrite <= 0) 68 | return; 69 | 70 | const int start1 = writePosition; 71 | const int size1 = std::min(toWrite, totalSize - start1); 72 | const int size2 = toWrite - size1; 73 | 74 | float* dst = buffer[channel].data(); 75 | if (size1 > 0) 76 | std::copy(data, data + size1, dst + start1); 77 | 78 | if (size2 > 0) 79 | std::copy(data + size1, data + size1 + size2, dst); 80 | 81 | if (advance) 82 | writePos.store((writePosition + toWrite) % totalSize, std::memory_order_release); 83 | } 84 | 85 | void advanceRead(int numSamples) 86 | { 87 | const int readPosition = readPos.load(std::memory_order_acquire); 88 | readPos.store((readPosition + numSamples) % totalSize, std::memory_order_release); 89 | } 90 | 91 | int getNumReady() const 92 | { 93 | const int writePosition = writePos.load(std::memory_order_acquire); 94 | const int readPosition = readPos.load(std::memory_order_acquire); 95 | if (writePosition >= readPosition) 96 | return writePosition - readPosition; 97 | else 98 | return totalSize - readPosition + writePosition; 99 | } 100 | 101 | int getTotalSize() const 102 | { 103 | return totalSize; 104 | } 105 | 106 | int getFreeSpace() const 107 | { 108 | // Keep one sample as guard to distinguish full from empty 109 | return totalSize - getNumReady() - 1; 110 | } 111 | 112 | int getNumChannels() const 113 | { 114 | return numChannels; 115 | } 116 | 117 | void setSize(int newNumChannels, int numSamples) 118 | { 119 | if (newNumChannels == numChannels && numSamples == totalSize) 120 | return; 121 | 122 | numChannels = newNumChannels; 123 | totalSize = numSamples; 124 | buffer.resize(numChannels); 125 | for (auto& channel : buffer) 126 | channel.resize(totalSize, 0.0f); 127 | 128 | reset(); 129 | } 130 | 131 | private: 132 | int numChannels; 133 | int totalSize; 134 | std::atomic readPos; 135 | std::atomic writePos; 136 | std::vector> buffer; 137 | }; 138 | 139 | } // namespace atk 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # atkAudio Plugin for OBS 2 | 3 | ## PluginHost 4 | 5 | - VST3 plugin host for OBS 6 | - MIDI, e.g. for using MIDI keyboard and a sampler plugin as soundboard 7 | - Sidechain support 8 | - Optional multithreading for improved performance with multi-core CPUs 9 | - Direct interfacing with audio and MIDI hardware devices 10 | - AU plugins on Apple macOS 11 | - LADSPA and LV2 plugins on Linux 12 | 13 | ## PluginHost2 14 | 15 | - Includes all features of regular PluginHost plus: 16 | - Use multiple plugins to create complex audio processing chains and graphs from OBS sources and audio devices 17 | - Always internally multithreading (no extra latency penalty) 18 | - Saving and loading of graphs as files 19 | - Route audio and MIDI between sources, plugins and hardware (ASIO/CoreAudio included) 20 | - Sample rate converting and drift compensating internal buffering for seamless audio between OBS sources and audio devices 21 | - etc 22 | 23 | PluginHost2 can interface directly with audio and MIDI hardware, OBS audio sources, and output audio as new OBS sources, allowing for complex audio processing setups. E.g. use ASIO interface as audio device, take additional audio from OBS sources, route monitoring to ASIO outputs and/or different audio drivers/hardware, use plugins and create final mix, and output the processed audio as a new OBS source for recording and streaming. Or just create a simple soundboard with a sampler plugin and a MIDI keyboard. 24 | 25 | Develop your own audio processing plugins and integrate them into `PluginHost2` using the [JUCE framework](https://juce.com/) AudioProcessor class. See `InternalPlugins.cpp` how `GainPlugin` is loaded. See `GainPlugin.h` for implementation. Optionally include OBS headers to use the [OBS API](https://docs.obsproject.com/) for more advanced integration with [OBS Studio](https://obsproject.com/) 26 | 27 | ## DeviceIo(2) 28 | 29 | - Send and receive audio directly into and from audio devices 30 | - "Anything from/to anywhere" device routing 31 | - ASIO, CoreAudio and Windows Audio devices 32 | 33 | ## Audio Source Mixer (OBS Source) 34 | 35 | - Mix audio from OBS sources into a new OBS audio source 36 | - Can be used as 'dummy' source to host filters, e.g. PluginHost2 37 | 38 | ## Usage examples 39 | 40 | - Use Delay filter to manually delay/sync individual audio sources 41 | - Source Mixer can create submixes (or one main mix) from multiple OBS audio sources 42 | - Mute original sources to prevent double/parallel audio 43 | - Use DeviceIo2 to route audio directly between OBS and audio devices (e.g. ASIO) 44 | - Put CPU intensive plugins into PluginHost and enable MT for better performance (multi-core, one buffer extra latency) 45 | - Use a sampler plugin with a MIDI keyboard in PluginHost as a soundboard 46 | - Do all of the above and more with PluginHost2 47 | - MIDI control of OBS audio source volume & mute 48 | 49 | ## Build instructions 50 | 51 | Project is (now loosely) based on [OBS Plugin Template](https://github.com/obsproject/obs-plugintemplate) and depends on [JUCE Framework](https://github.com/juce-framework/JUCE). Install JUCE Framework [Minimum System Requirements](https://github.com/juce-framework/JUCE#minimum-system-requirements) and OBS Plugin Template [Supported Build Environment](https://github.com/obsproject/obs-plugintemplate#supported-build-environments) and follow OBS Plugin Template [Quick Start Guide](https://github.com/obsproject/obs-plugintemplate/wiki/Quick-Start-Guide). 52 | 53 | In short, after installing all dependencies (Ubuntu example): 54 | 55 | ```console 56 | git clone https://github.com/atkaudio/pluginforobsrelease 57 | cd pluginforobsrelease 58 | cmake --preset ubuntu-x86_64 59 | cmake --build --preset ubuntu-x86_64 60 | ``` 61 | 62 | Find `atkaudio-pluginforobs.so` and copy it to OBS plugins directory. 63 | See `CMakePresets.json` for Windows, macOS and other build presets. 64 | 65 | ## Donation 66 | 67 | [![PayPal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/donate/?hosted_button_id=ERBKC76F55HZW) 68 | 69 | If you find this project useful, please consider making [a donation](https://www.paypal.com/donate/?hosted_button_id=ERBKC76F55HZW) to support its future development and maintenance. 70 | 71 | ## Installation 72 | 73 | - Download and install [latest release](https://github.com/atkAudio/PluginForObsRelease/releases/latest) using the appropriate installer for your OS. 74 | - Manual/portable installations e.g. on major Linux distros: extract portable `.zip` file and copy the directory `atkaudio-pluginforobs` into `~/.config/obs-studio/plugins/`. 75 | -------------------------------------------------------------------------------- /cmake/windows/buildspec.cmake: -------------------------------------------------------------------------------- 1 | # CMake Windows build dependencies module 2 | 3 | include_guard(GLOBAL) 4 | 5 | include(buildspec_common) 6 | 7 | # _check_dependencies_windows: Set up Windows slice for _check_dependencies 8 | function(_check_dependencies_windows) 9 | set(arch ${CMAKE_VS_PLATFORM_NAME}) 10 | set(platform windows-${arch}) 11 | 12 | # Use source-directory-based dependency directory to persist across cache clears 13 | set(dependencies_dir "${CMAKE_SOURCE_DIR}/_deps/${arch}") 14 | set(prebuilt_filename "windows-deps-VERSION-ARCH-REVISION.zip") 15 | set(prebuilt_destination "obs-deps-VERSION-ARCH") 16 | set(qt6_filename "windows-deps-qt6-VERSION-ARCH-REVISION.zip") 17 | set(qt6_destination "obs-deps-qt6-VERSION-ARCH") 18 | set(obs-studio_filename "VERSION.zip") 19 | set(obs-studio_destination "obs-studio-VERSION") 20 | set(dependencies_list prebuilt qt6 obs-studio) 21 | 22 | _check_dependencies() 23 | 24 | # For ARM64 cross-compilation, we also need x64 Qt host tools 25 | if(CMAKE_VS_PLATFORM_NAME STREQUAL "ARM64") 26 | message(STATUS "ARM64 build detected - also downloading x64 Qt host tools for cross-compilation") 27 | 28 | # Call a separate function to download x64 Qt tools 29 | _download_x64_qt_for_arm64() 30 | endif() 31 | endfunction() 32 | 33 | # Helper function to download x64 Qt tools for ARM64 cross-compilation 34 | function(_download_x64_qt_for_arm64) 35 | set(arch "x64") 36 | set(platform "windows-x64") 37 | # Store x64 tools in a persistent deps directory for cross-compilation 38 | set(dependencies_dir "${CMAKE_SOURCE_DIR}/_deps/x64") 39 | set(qt6_filename "windows-deps-qt6-VERSION-ARCH-REVISION.zip") 40 | set(qt6_destination "obs-deps-qt6-VERSION-ARCH") 41 | set(dependencies_list qt6) 42 | 43 | file(READ "${CMAKE_CURRENT_SOURCE_DIR}/buildspec.json" buildspec) 44 | string(JSON dependency_data GET ${buildspec} dependencies) 45 | 46 | # Process only qt6 dependency for x64 47 | string(JSON data GET ${dependency_data} qt6) 48 | string(JSON version GET ${data} version) 49 | string(JSON hash GET ${data} hashes ${platform}) 50 | string(JSON url GET ${data} baseUrl) 51 | string(JSON label GET ${data} label) 52 | string(JSON revision ERROR_VARIABLE error GET ${data} revision ${platform}) 53 | 54 | message(STATUS "Setting up ${label} x64 host tools for ARM64 cross-compilation") 55 | 56 | set(file "${qt6_filename}") 57 | set(destination "${qt6_destination}") 58 | string(REPLACE "VERSION" "${version}" file "${file}") 59 | string(REPLACE "VERSION" "${version}" destination "${destination}") 60 | string(REPLACE "ARCH" "${arch}" file "${file}") 61 | string(REPLACE "ARCH" "${arch}" destination "${destination}") 62 | if(revision) 63 | string(REPLACE "_REVISION" "_v${revision}" file "${file}") 64 | string(REPLACE "-REVISION" "-v${revision}" file "${file}") 65 | else() 66 | string(REPLACE "_REVISION" "" file "${file}") 67 | string(REPLACE "-REVISION" "" file "${file}") 68 | endif() 69 | 70 | if(EXISTS "${dependencies_dir}/.dependency_qt6_${arch}.sha256") 71 | file(READ "${dependencies_dir}/.dependency_qt6_${arch}.sha256" OBS_DEPENDENCY_qt6_x64_HASH) 72 | endif() 73 | 74 | set(skip FALSE) 75 | if(OBS_DEPENDENCY_qt6_x64_HASH STREQUAL ${hash}) 76 | set(skip TRUE) 77 | message(STATUS "Setting up ${label} x64 host tools - skipped") 78 | endif() 79 | 80 | if(NOT skip) 81 | set(url "${url}/${version}/${file}") 82 | set(destination "${dependencies_dir}/${destination}") 83 | 84 | message(STATUS "Downloading x64 Qt host tools: ${file}") 85 | file(DOWNLOAD "${url}" "${dependencies_dir}/${file}" EXPECTED_HASH SHA256=${hash} STATUS download_status) 86 | 87 | list(GET download_status 0 status_code) 88 | if(NOT status_code EQUAL 0) 89 | list(GET download_status 1 error_message) 90 | message(FATAL_ERROR "Failed to download x64 Qt host tools: ${error_message}") 91 | endif() 92 | 93 | message(STATUS "Extracting x64 Qt host tools to: ${destination}") 94 | file(ARCHIVE_EXTRACT INPUT "${dependencies_dir}/${file}" DESTINATION "${destination}") 95 | file(REMOVE "${dependencies_dir}/${file}") 96 | file(WRITE "${dependencies_dir}/.dependency_qt6_x64.sha256" "${hash}") 97 | 98 | # Verify extraction was successful 99 | if(EXISTS "${destination}/bin/moc.exe") 100 | message(STATUS "Setting up ${label} x64 host tools - done") 101 | else() 102 | message(FATAL_ERROR "Failed to extract x64 Qt host tools to ${destination} - moc.exe not found") 103 | endif() 104 | endif() 105 | 106 | message(STATUS "ARM64 cross-compilation ready: x64 host tools + ARM64 target libraries available") 107 | endfunction() 108 | 109 | _check_dependencies_windows() 110 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/ModuleInfrastructure/Bridge/ModuleAudioIODeviceType.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ModuleAudioDevice.h" 4 | #include "ModuleAudioServerDevice.h" 5 | #include 6 | #include 7 | 8 | namespace atk 9 | { 10 | 11 | class ModuleAudioIODeviceType : public juce::AudioIODeviceType 12 | { 13 | public: 14 | ModuleAudioIODeviceType(const juce::String& typeName = "Module Audio") 15 | : juce::AudioIODeviceType(typeName) 16 | , coordinator(std::make_shared()) 17 | { 18 | } 19 | 20 | ~ModuleAudioIODeviceType() override = default; 21 | 22 | void scanForDevices() override 23 | { 24 | deviceNames.clear(); 25 | audioServerDevices.clear(); 26 | 27 | if (shouldIncludeOBSAudio()) 28 | deviceNames.add("OBS Audio"); 29 | 30 | if (auto* audioServer = AudioServer::getInstanceWithoutCreating()) 31 | { 32 | auto inputDevicesByType = audioServer->getInputDevicesByType(); 33 | auto outputDevicesByType = audioServer->getOutputDevicesByType(); 34 | 35 | std::set deviceTypes; 36 | for (const auto& pair : inputDevicesByType) 37 | deviceTypes.insert(pair.first); 38 | for (const auto& pair : outputDevicesByType) 39 | deviceTypes.insert(pair.first); 40 | 41 | const juce::StringArray allowedTypes = {"ASIO", "CoreAudio", "ALSA", "Windows Audio"}; 42 | 43 | for (const auto& deviceType : deviceTypes) 44 | { 45 | if (!allowedTypes.contains(deviceType)) 46 | continue; 47 | 48 | std::set devicesForType; 49 | 50 | auto inputIt = inputDevicesByType.find(deviceType); 51 | if (inputIt != inputDevicesByType.end()) 52 | for (const auto& device : inputIt->second) 53 | devicesForType.insert(device); 54 | 55 | auto outputIt = outputDevicesByType.find(deviceType); 56 | if (outputIt != outputDevicesByType.end()) 57 | for (const auto& device : outputIt->second) 58 | devicesForType.insert(device); 59 | 60 | for (const auto& deviceName : devicesForType) 61 | { 62 | AudioServerDeviceInfo info; 63 | info.deviceName = deviceName; 64 | info.deviceType = deviceType; 65 | 66 | audioServerDevices.add(info); 67 | deviceNames.add(info.getDisplayName()); 68 | } 69 | } 70 | } 71 | } 72 | 73 | juce::StringArray getDeviceNames(bool /*forInput*/) const override 74 | { 75 | return deviceNames; 76 | } 77 | 78 | int getDefaultDeviceIndex(bool /*forInput*/) const override 79 | { 80 | return -1; // No default device - never auto-select 81 | } 82 | 83 | int getIndexOfDevice(juce::AudioIODevice* device, bool /*forInput*/) const override 84 | { 85 | if (device == nullptr) 86 | return -1; 87 | return deviceNames.indexOf(device->getName()); 88 | } 89 | 90 | bool hasSeparateInputsAndOutputs() const override 91 | { 92 | return false; 93 | } 94 | 95 | juce::AudioIODevice* 96 | createDevice(const juce::String& outputDeviceName, const juce::String& inputDeviceName) override 97 | { 98 | const juce::String deviceName = outputDeviceName.isNotEmpty() ? outputDeviceName : inputDeviceName; 99 | 100 | if (deviceName == "OBS Audio") 101 | return createOBSDevice(deviceName); 102 | 103 | for (const auto& info : audioServerDevices) 104 | if (info.getDisplayName() == deviceName) 105 | return createAudioServerDevice(deviceName, info); 106 | 107 | return nullptr; 108 | } 109 | 110 | protected: 111 | virtual bool shouldIncludeOBSAudio() const 112 | { 113 | return true; 114 | } 115 | 116 | virtual juce::AudioIODevice* createOBSDevice(const juce::String& deviceName) 117 | { 118 | return new ModuleOBSAudioDevice(deviceName, coordinator, getTypeName()); 119 | } 120 | 121 | virtual juce::AudioIODevice* 122 | createAudioServerDevice(const juce::String& displayName, const AudioServerDeviceInfo& info) 123 | { 124 | return new ModuleAudioServerDevice(displayName, info.deviceName, info.deviceType, coordinator, getTypeName()); 125 | } 126 | 127 | private: 128 | std::shared_ptr coordinator; 129 | juce::StringArray deviceNames; 130 | juce::Array audioServerDevices; 131 | }; 132 | 133 | } // namespace atk 134 | -------------------------------------------------------------------------------- /cmake/linux/helpers.cmake.backup: -------------------------------------------------------------------------------- 1 | # CMake Linux helper functions module 2 | 3 | include_guard(GLOBAL) 4 | 5 | include(helpers_common) 6 | 7 | # set_target_properties_plugin: Set target properties for use in obs-studio 8 | function(set_target_properties_plugin target) 9 | set(options "") 10 | set(oneValueArgs "") 11 | set(multiValueArgs PROPERTIES) 12 | cmake_parse_arguments(PARSE_ARGV 0 _STPO "${options}" "${oneValueArgs}" "${multiValueArgs}") 13 | 14 | message(DEBUG "Setting additional properties for target ${target}...") 15 | 16 | while(_STPO_PROPERTIES) 17 | list(POP_FRONT _STPO_PROPERTIES key value) 18 | set_property(TARGET ${target} PROPERTY ${key} "${value}") 19 | endwhile() 20 | 21 | set_target_properties( 22 | ${target} 23 | PROPERTIES VERSION ${PLUGIN_VERSION} SOVERSION ${PLUGIN_VERSION_MAJOR} PREFIX "" 24 | ) 25 | 26 | message(STATUS "Installing target ${target} to:") 27 | message(STATUS " - LIBRARY: ${CMAKE_INSTALL_LIBDIR}/obs-plugins") 28 | message(STATUS " - RUNTIME: ${CMAKE_INSTALL_BINDIR}") 29 | 30 | install( 31 | TARGETS ${target} 32 | RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} 33 | LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/obs-plugins 34 | ) 35 | 36 | # Additional install for portable component with custom directory structure 37 | if(CMAKE_SIZEOF_VOID_P EQUAL 8) 38 | set(_portable_arch "64bit") 39 | else() 40 | set(_portable_arch "32bit") 41 | endif() 42 | 43 | install( 44 | TARGETS ${target} 45 | RUNTIME DESTINATION obs-plugins/${_portable_arch} 46 | LIBRARY DESTINATION obs-plugins/${_portable_arch} 47 | ) 48 | 49 | if(TARGET plugin-support) 50 | target_link_libraries(${target} PRIVATE plugin-support) 51 | endif() 52 | 53 | add_custom_command( 54 | TARGET ${target} 55 | POST_BUILD 56 | COMMAND "${CMAKE_COMMAND}" -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/rundir/$" 57 | COMMAND 58 | "${CMAKE_COMMAND}" -E copy_if_different "$" "${CMAKE_CURRENT_BINARY_DIR}/rundir/$" 59 | COMMENT "Copy ${target} to rundir" 60 | VERBATIM 61 | ) 62 | 63 | target_install_resources(${target}) 64 | 65 | get_target_property(target_sources ${target} SOURCES) 66 | set(target_ui_files ${target_sources}) 67 | list(FILTER target_ui_files INCLUDE REGEX ".+\\.(ui|qrc)") 68 | source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" PREFIX "UI Files" FILES ${target_ui_files}) 69 | endfunction() 70 | 71 | # Helper function to add resources into bundle 72 | function(target_install_resources target) 73 | message(DEBUG "Installing resources for target ${target}...") 74 | if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/data") 75 | file(GLOB_RECURSE data_files "${CMAKE_CURRENT_SOURCE_DIR}/data/*") 76 | foreach(data_file IN LISTS data_files) 77 | cmake_path( 78 | RELATIVE_PATH 79 | data_file 80 | BASE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/data/" 81 | OUTPUT_VARIABLE relative_path 82 | ) 83 | cmake_path(GET relative_path PARENT_PATH relative_path) 84 | target_sources(${target} PRIVATE "${data_file}") 85 | source_group("Resources/${relative_path}" FILES "${data_file}") 86 | endforeach() 87 | 88 | message(STATUS "Installing data directory for ${target} to:") 89 | message(STATUS " - DESTINATION: ${CMAKE_INSTALL_DATAROOTDIR}/obs/obs-plugins/${target}") 90 | 91 | install( 92 | DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/data/" 93 | DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/obs/obs-plugins/${target} 94 | USE_SOURCE_PERMISSIONS 95 | ) 96 | 97 | add_custom_command( 98 | TARGET ${target} 99 | POST_BUILD 100 | COMMAND "${CMAKE_COMMAND}" -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/rundir/$/${target}" 101 | COMMAND 102 | "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/data" 103 | "${CMAKE_CURRENT_BINARY_DIR}/rundir/$/${target}" 104 | COMMENT "Copy ${target} resources to rundir" 105 | VERBATIM 106 | ) 107 | endif() 108 | endfunction() 109 | 110 | # Helper function to add a specific resource to a bundle 111 | function(target_add_resource target resource) 112 | message(DEBUG "Add resource '${resource}' to target ${target} at destination '${target_destination}'...") 113 | 114 | install(FILES "${resource}" DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/obs/obs-plugins/${target}) 115 | 116 | add_custom_command( 117 | TARGET ${target} 118 | POST_BUILD 119 | COMMAND "${CMAKE_COMMAND}" -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/rundir/$/${target}" 120 | COMMAND "${CMAKE_COMMAND}" -E copy "${resource}" "${CMAKE_CURRENT_BINARY_DIR}/rundir/$/${target}" 121 | COMMENT "Copy ${target} resource ${resource} to rundir" 122 | VERBATIM 123 | ) 124 | 125 | source_group("Resources" FILES "${resource}") 126 | endfunction() 127 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/PluginHost/Core/HostAudioProcessor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | enum class EditorStyle 10 | { 11 | thisWindow, 12 | newWindow 13 | }; 14 | 15 | class HostAudioProcessorImpl 16 | : public juce::AudioProcessor 17 | , private juce::ChangeListener 18 | { 19 | public: 20 | explicit HostAudioProcessorImpl(int numChannels = 2); 21 | ~HostAudioProcessorImpl() override; 22 | 23 | // AudioProcessor interface 24 | bool isBusesLayoutSupported(const juce::AudioProcessor::BusesLayout& layouts) const final; 25 | void prepareToPlay(double sampleRate, int samplesPerBlock) final; 26 | void releaseResources() final; 27 | void reset() final; 28 | void processBlock(juce::AudioBuffer& buffer, juce::MidiBuffer& midiBuffer) final; 29 | void processBlock(juce::AudioBuffer&, juce::MidiBuffer&) final; 30 | 31 | bool hasEditor() const override; 32 | juce::AudioProcessorEditor* createEditor() override; 33 | 34 | const juce::String getName() const final; 35 | bool acceptsMidi() const final; 36 | bool producesMidi() const final; 37 | double getTailLengthSeconds() const final; 38 | 39 | int getNumPrograms() final; 40 | int getCurrentProgram() final; 41 | void setCurrentProgram(int index) final; 42 | const juce::String getProgramName(int index) final; 43 | void changeProgramName(int index, const juce::String& newName) final; 44 | 45 | void getStateInformation(juce::MemoryBlock& destData) final; 46 | void setStateInformation(const void* data, int sizeInBytes) final; 47 | 48 | // Plugin management 49 | void setNewPlugin(const juce::PluginDescription& pd, EditorStyle where, const juce::MemoryBlock& mb = {}); 50 | void clearPlugin(); 51 | bool isPluginLoaded() const; 52 | std::unique_ptr createInnerEditor() const; 53 | EditorStyle getEditorStyle() const noexcept; 54 | 55 | juce::AudioPluginInstance* getInnerPlugin() const; 56 | 57 | void setInputChannelMapping(const std::vector>& mapping); 58 | 59 | std::vector> getInputChannelMapping() const; 60 | 61 | void setOutputChannelMapping(const std::vector>& mapping); 62 | 63 | std::vector> getOutputChannelMapping() const; 64 | 65 | // Public members for UI access 66 | juce::ApplicationProperties appProperties; 67 | juce::AudioPluginFormatManager pluginFormatManager; 68 | 69 | // Own plugin list instance, loads from/saves to shared file 70 | juce::KnownPluginList pluginList; 71 | 72 | std::function pluginChanged; 73 | 74 | atk::MidiClient midiClient; 75 | atk::AudioClient audioClient; 76 | 77 | std::function getMultiCoreEnabled; 78 | std::function setMultiCoreEnabled; 79 | 80 | std::function getCpuLoad; 81 | std::function getLatencyMs; 82 | 83 | private: 84 | class AtkAudioPlayHead : public juce::AudioPlayHead 85 | { 86 | public: 87 | juce::AudioPlayHead::PositionInfo positionInfo; 88 | juce::Optional getPosition() const override; 89 | }; 90 | 91 | static juce::AudioChannelSet getChannelSetForCount(int numChannels); 92 | void changeListenerCallback(juce::ChangeBroadcaster* source) final; 93 | 94 | static inline juce::InterProcessLock appPropertiesLock{"atkAudioPluginHostLock"}; 95 | 96 | juce::CriticalSection innerMutex; 97 | std::unique_ptr inner; 98 | EditorStyle editorStyle = EditorStyle::thisWindow; 99 | bool active = false; 100 | juce::ScopedMessageBox messageBox; 101 | AtkAudioPlayHead atkPlayHead; 102 | 103 | atk::ChannelRoutingMatrix routingMatrix; 104 | 105 | juce::AudioBuffer internalBuffer; 106 | 107 | juce::AudioBuffer deviceInputBuffer; 108 | juce::AudioBuffer deviceOutputBuffer; 109 | 110 | juce::MidiBuffer inputMidiCopy; 111 | 112 | static constexpr const char* innerStateTag = "inner_state"; 113 | static constexpr const char* editorStyleTag = "editor_style"; 114 | 115 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(HostAudioProcessorImpl) 116 | }; 117 | 118 | class HostAudioProcessor final : public HostAudioProcessorImpl 119 | { 120 | public: 121 | HostAudioProcessor() 122 | : HostAudioProcessorImpl() 123 | { 124 | } 125 | 126 | bool hasEditor() const override; 127 | juce::AudioProcessorEditor* createEditor() override; 128 | 129 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(HostAudioProcessor) 130 | }; 131 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/PluginHost/UI/PluginHostFooter.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class PluginHostFooter final 6 | : public juce::Component 7 | , private juce::Timer 8 | { 9 | public: 10 | PluginHostFooter(const juce::String& actionButtonText, bool showLink = false) 11 | : showLinkButton(showLink) 12 | { 13 | actionButton.setButtonText(actionButtonText); 14 | 15 | // MT button for enabling secondary job queue processing 16 | addAndMakeVisible(multiToggle); 17 | addAndMakeVisible(audioButton); 18 | addAndMakeVisible(midiButton); 19 | addAndMakeVisible(actionButton); 20 | 21 | multiToggle.setButtonText("MT"); 22 | multiToggle.setTooltip("Multi-threading (extra buffer latency)"); 23 | multiToggle.setClickingTogglesState(true); 24 | 25 | statsLabel.setFont(juce::FontOptions(10.0f)); 26 | statsLabel.setJustificationType(juce::Justification::centredLeft); 27 | statsLabel.setColour(juce::Label::textColourId, juce::Colours::grey); 28 | statsLabel.setBorderSize(juce::BorderSize(0, 4, 0, 0)); 29 | addAndMakeVisible(statsLabel); 30 | 31 | if (showLinkButton) 32 | { 33 | linkButton.setFont(juce::FontOptions(11.0f), false); 34 | addAndMakeVisible(linkButton); 35 | } 36 | 37 | startTimerHz(10); 38 | } 39 | 40 | ~PluginHostFooter() override 41 | { 42 | stopTimer(); 43 | } 44 | 45 | void setMultiCoreCallbacks(std::function getEnabledCallback, std::function setEnabledCallback) 46 | { 47 | getMultiCoreEnabled = getEnabledCallback; 48 | 49 | if (getEnabledCallback) 50 | multiToggle.setToggleState(getEnabledCallback(), juce::dontSendNotification); 51 | 52 | if (setEnabledCallback) 53 | multiToggle.onClick = [this, setEnabledCallback] { setEnabledCallback(multiToggle.getToggleState()); }; 54 | } 55 | 56 | void setStatsCallbacks(std::function getCpuLoadFn, std::function getLatencyMsFn) 57 | { 58 | getCpuLoad = getCpuLoadFn; 59 | getLatencyMs = getLatencyMsFn; 60 | } 61 | 62 | void timerCallback() override 63 | { 64 | if (getMultiCoreEnabled) 65 | { 66 | bool currentState = getMultiCoreEnabled(); 67 | if (multiToggle.getToggleState() != currentState) 68 | multiToggle.setToggleState(currentState, juce::dontSendNotification); 69 | } 70 | 71 | float cpuLoad = getCpuLoad ? getCpuLoad() : 0.0f; 72 | int latencyMs = getLatencyMs ? getLatencyMs() : 0; 73 | 74 | statsLabel.setText( 75 | juce::String(latencyMs) + "ms " + juce::String(cpuLoad, 2).replace("0.", "."), 76 | juce::dontSendNotification 77 | ); 78 | } 79 | 80 | void resized() override 81 | { 82 | auto bounds = getLocalBounds(); 83 | const int statsHeight = 14; 84 | auto statsArea = bounds.removeFromBottom(statsHeight); 85 | 86 | statsLabel.setBounds(statsArea.removeFromLeft(60)); 87 | 88 | juce::Grid grid; 89 | grid.autoFlow = juce::Grid::AutoFlow::column; 90 | grid.setGap(juce::Grid::Px{5}); 91 | grid.autoRows = grid.autoColumns = juce::Grid::TrackInfo{juce::Grid::Fr{1}}; 92 | 93 | grid.items = { 94 | juce::GridItem{multiToggle}.withSize(60.0f, (float)bounds.getHeight()), 95 | juce::GridItem{audioButton} 96 | .withSize((float)audioButton.getBestWidthForHeight(40), (float)bounds.getHeight()), 97 | juce::GridItem{midiButton}.withSize((float)midiButton.getBestWidthForHeight(40), (float)bounds.getHeight()), 98 | juce::GridItem{actionButton} 99 | .withSize((float)actionButton.getBestWidthForHeight(40), (float)bounds.getHeight()) 100 | }; 101 | 102 | if (showLinkButton) 103 | grid.items.add(juce::GridItem{linkButton}); 104 | 105 | grid.performLayout(bounds); 106 | 107 | if (showLinkButton) 108 | { 109 | linkButton.changeWidthToFitText(); 110 | linkButton.setTopRightPosition(getWidth(), 0); 111 | } 112 | } 113 | 114 | juce::ToggleButton multiToggle; 115 | juce::TextButton audioButton{"Audio..."}; 116 | juce::TextButton midiButton{"MIDI..."}; 117 | juce::TextButton actionButton; 118 | juce::Label statsLabel; 119 | juce::HyperlinkButton linkButton{"atkAudio", juce::URL("http://www.atkaudio.com")}; 120 | 121 | private: 122 | bool showLinkButton = false; 123 | std::function getMultiCoreEnabled; 124 | std::function getCpuLoad; 125 | std::function getLatencyMs; 126 | juce::SharedResourcePointer tooltipWindow; 127 | 128 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PluginHostFooter) 129 | }; 130 | -------------------------------------------------------------------------------- /cmake/macos/helpers.cmake: -------------------------------------------------------------------------------- 1 | # CMake macOS helper functions module 2 | 3 | include_guard(GLOBAL) 4 | 5 | include(helpers_common) 6 | 7 | # set_target_properties_obs: Set target properties for use in obs-studio 8 | function(set_target_properties_plugin target) 9 | set(options "") 10 | set(oneValueArgs "") 11 | set(multiValueArgs PROPERTIES) 12 | cmake_parse_arguments(PARSE_ARGV 0 _STPO "${options}" "${oneValueArgs}" "${multiValueArgs}") 13 | 14 | message(DEBUG "Setting additional properties for target ${target}...") 15 | 16 | while(_STPO_PROPERTIES) 17 | list( 18 | POP_FRONT _STPO_PROPERTIES 19 | key 20 | value 21 | ) 22 | set_property( 23 | TARGET 24 | ${target} 25 | PROPERTY 26 | ${key} 27 | "${value}" 28 | ) 29 | endwhile() 30 | 31 | string(TIMESTAMP CURRENT_YEAR "%Y") 32 | set_target_properties( 33 | ${target} 34 | PROPERTIES 35 | BUNDLE 36 | TRUE 37 | BUNDLE_EXTENSION 38 | plugin 39 | XCODE_ATTRIBUTE_PRODUCT_NAME 40 | ${target} 41 | XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER 42 | ${MACOS_BUNDLEID} 43 | XCODE_ATTRIBUTE_MARKETING_VERSION 44 | ${PLUGIN_VERSION} 45 | XCODE_ATTRIBUTE_GENERATE_INFOPLIST_FILE 46 | YES 47 | XCODE_ATTRIBUTE_INFOPLIST_FILE 48 | "" 49 | XCODE_ATTRIBUTE_INFOPLIST_KEY_CFBundleDisplayName 50 | ${target} 51 | XCODE_ATTRIBUTE_INFOPLIST_KEY_NSHumanReadableCopyright 52 | "(c) ${CURRENT_YEAR} ${PLUGIN_AUTHOR}" 53 | XCODE_ATTRIBUTE_INSTALL_PATH 54 | "$(USER_LIBRARY_DIR)/Application Support/obs-studio/plugins" 55 | ) 56 | 57 | if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/macos/entitlements.plist") 58 | set_target_properties( 59 | ${target} 60 | PROPERTIES 61 | XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS 62 | "${CMAKE_CURRENT_SOURCE_DIR}/cmake/macos/entitlements.plist" 63 | ) 64 | endif() 65 | 66 | if(TARGET plugin-support) 67 | target_link_libraries(${target} PRIVATE plugin-support) 68 | endif() 69 | 70 | target_install_resources(${target}) 71 | 72 | add_custom_command( 73 | TARGET ${target} 74 | POST_BUILD 75 | COMMAND 76 | "${CMAKE_COMMAND}" -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/rundir/$" 77 | COMMAND 78 | "${CMAKE_COMMAND}" -E copy_directory "$" 79 | "${CMAKE_CURRENT_BINARY_DIR}/rundir/$/$" 80 | COMMENT "Copy ${target} to rundir" 81 | VERBATIM 82 | ) 83 | 84 | get_target_property(target_sources ${target} SOURCES) 85 | set(target_ui_files ${target_sources}) 86 | list(FILTER target_ui_files INCLUDE REGEX ".+\\.(ui|qrc)") 87 | source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" PREFIX "UI Files" FILES ${target_ui_files}) 88 | 89 | install(TARGETS ${target} LIBRARY DESTINATION .) 90 | install(FILES "$.dsym" CONFIGURATIONS Release DESTINATION .) 91 | 92 | # Additional install for portable component with custom directory structure 93 | if(CMAKE_SIZEOF_VOID_P EQUAL 8) 94 | set(_portable_arch "64bit") 95 | else() 96 | set(_portable_arch "32bit") 97 | endif() 98 | 99 | install(TARGETS ${target} LIBRARY DESTINATION obs-plugins/${_portable_arch}) 100 | endfunction() 101 | 102 | # target_install_resources: Helper function to add resources into bundle 103 | function(target_install_resources target) 104 | message(DEBUG "Installing resources for target ${target}...") 105 | if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/data") 106 | file(GLOB_RECURSE data_files "${CMAKE_CURRENT_SOURCE_DIR}/data/*") 107 | foreach(data_file IN LISTS data_files) 108 | cmake_path( 109 | RELATIVE_PATH data_file 110 | BASE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/data/" 111 | OUTPUT_VARIABLE relative_path 112 | ) 113 | cmake_path(GET relative_path PARENT_PATH relative_path) 114 | target_sources(${target} PRIVATE "${data_file}") 115 | set_property( 116 | SOURCE 117 | "${data_file}" 118 | PROPERTY 119 | MACOSX_PACKAGE_LOCATION 120 | "Resources/${relative_path}" 121 | ) 122 | source_group("Resources/${relative_path}" FILES "${data_file}") 123 | endforeach() 124 | endif() 125 | endfunction() 126 | 127 | # target_add_resource: Helper function to add a specific resource to a bundle 128 | function(target_add_resource target resource) 129 | message(DEBUG "Add resource ${resource} to target ${target} at destination ${destination}...") 130 | target_sources(${target} PRIVATE "${resource}") 131 | set_property( 132 | SOURCE 133 | "${resource}" 134 | PROPERTY 135 | MACOSX_PACKAGE_LOCATION 136 | Resources 137 | ) 138 | source_group("Resources" FILES "${resource}") 139 | endfunction() 140 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/PluginHost2/Core/DeviceIo2Plugin.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | class DeviceIo2Plugin final : public juce::AudioProcessor 7 | { 8 | public: 9 | DeviceIo2Plugin() 10 | : AudioProcessor( 11 | BusesProperties() 12 | .withInput("Input", juce::AudioChannelSet::stereo()) 13 | .withOutput("Output", juce::AudioChannelSet::stereo()) 14 | ) 15 | { 16 | deviceIo2 = std::make_unique(); 17 | // Note: DeviceIo2 internal routing matrix is initialized with default diagonal routing 18 | // It will auto-resize based on the actual OBS channel count during processing 19 | } 20 | 21 | ~DeviceIo2Plugin() override 22 | { 23 | deviceIo2.reset(); 24 | } 25 | 26 | const juce::String getName() const override 27 | { 28 | return "DeviceIo2"; 29 | } 30 | 31 | bool acceptsMidi() const override 32 | { 33 | return false; 34 | } 35 | 36 | bool producesMidi() const override 37 | { 38 | return false; 39 | } 40 | 41 | double getTailLengthSeconds() const override 42 | { 43 | return 0.0; 44 | } 45 | 46 | int getNumPrograms() override 47 | { 48 | return 1; 49 | } 50 | 51 | int getCurrentProgram() override 52 | { 53 | return 0; 54 | } 55 | 56 | void setCurrentProgram(int) override 57 | { 58 | } 59 | 60 | const juce::String getProgramName(int) override 61 | { 62 | return "Default"; 63 | } 64 | 65 | void changeProgramName(int, const juce::String&) override 66 | { 67 | } 68 | 69 | void prepareToPlay(double sampleRate, int samplesPerBlock) override 70 | { 71 | juce::ignoreUnused(sampleRate, samplesPerBlock); 72 | // DeviceIo2 handles its own preparation internally during process() 73 | } 74 | 75 | void releaseResources() override 76 | { 77 | // DeviceIo2 handles its own resource management 78 | } 79 | 80 | void processBlock(juce::AudioBuffer& buffer, juce::MidiBuffer&) override 81 | { 82 | if (!deviceIo2) 83 | return; 84 | 85 | // Convert JUCE buffer to raw pointer format expected by DeviceIo2 86 | const int numChannels = buffer.getNumChannels(); 87 | const int numSamples = buffer.getNumSamples(); 88 | 89 | // Create array of channel pointers 90 | std::vector channelPointers(numChannels); 91 | for (int ch = 0; ch < numChannels; ++ch) 92 | channelPointers[ch] = buffer.getWritePointer(ch); 93 | 94 | // Process through DeviceIo2 95 | // INPUT routing: Hardware inputs (selected in INPUT matrix) → mixed into plugin output 96 | // OUTPUT routing: Plugin input → sent to hardware (selected in OUTPUT matrix) 97 | deviceIo2->process(channelPointers.data(), numChannels, numSamples, getSampleRate()); 98 | } 99 | 100 | juce::AudioProcessorEditor* createEditor() override 101 | { 102 | // DeviceIo2 provides a method to create an embeddable settings component 103 | if (deviceIo2) 104 | { 105 | if (auto* settingsComponent = deviceIo2->createEmbeddableSettingsComponent()) 106 | { 107 | // Wrap the settings component in a generic editor 108 | class DeviceIo2Editor : public juce::AudioProcessorEditor 109 | { 110 | public: 111 | DeviceIo2Editor(juce::AudioProcessor& p, juce::Component* content) 112 | : AudioProcessorEditor(p) 113 | { 114 | // Take ownership of the content component 115 | contentComponent.reset(content); 116 | if (contentComponent) 117 | { 118 | addAndMakeVisible(contentComponent.get()); 119 | setSize(900, 700); 120 | } 121 | } 122 | 123 | void resized() override 124 | { 125 | if (contentComponent) 126 | contentComponent->setBounds(getLocalBounds()); 127 | } 128 | 129 | private: 130 | std::unique_ptr contentComponent; 131 | 132 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(DeviceIo2Editor) 133 | }; 134 | 135 | return new DeviceIo2Editor(*this, settingsComponent); 136 | } 137 | } 138 | return nullptr; 139 | } 140 | 141 | bool hasEditor() const override 142 | { 143 | return true; 144 | } 145 | 146 | void getStateInformation(juce::MemoryBlock& destData) override 147 | { 148 | if (!deviceIo2) 149 | return; 150 | 151 | std::string state; 152 | deviceIo2->getState(state); 153 | 154 | if (!state.empty()) 155 | destData.replaceAll(state.data(), state.size()); 156 | } 157 | 158 | void setStateInformation(const void* data, int sizeInBytes) override 159 | { 160 | if (!deviceIo2 || sizeInBytes <= 0) 161 | return; 162 | 163 | std::string state(static_cast(data), sizeInBytes); 164 | deviceIo2->setState(state); 165 | } 166 | 167 | private: 168 | std::unique_ptr deviceIo2; 169 | 170 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(DeviceIo2Plugin) 171 | }; 172 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/ModuleInfrastructure/Bridge/ModuleDeviceManager.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ModuleAudioIODeviceType.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace atk 10 | { 11 | 12 | class ModuleDeviceManager : public juce::ChangeListener 13 | { 14 | public: 15 | ModuleDeviceManager( 16 | std::unique_ptr deviceType, 17 | juce::AudioDeviceManager& deviceManager, 18 | MidiClient* externalMidiClient = nullptr 19 | ) 20 | : customDeviceType(deviceType.release()) 21 | , audioDeviceManager(deviceManager) 22 | , externalMidiClientPtr(externalMidiClient) 23 | { 24 | if (!externalMidiClientPtr) 25 | internalMidiClient = std::make_unique(); 26 | } 27 | 28 | ~ModuleDeviceManager() 29 | { 30 | cleanup(); 31 | } 32 | 33 | bool initialize() 34 | { 35 | audioDeviceManager.addAudioDeviceType(std::unique_ptr(customDeviceType.release())); 36 | 37 | auto error = audioDeviceManager.initialise(256, 256, nullptr, true); 38 | if (error.isNotEmpty()) 39 | return false; 40 | 41 | audioDeviceManager.addChangeListener(this); 42 | return true; 43 | } 44 | 45 | bool openOBSDevice() 46 | { 47 | juce::AudioDeviceManager::AudioDeviceSetup setup; 48 | setup.outputDeviceName = "OBS Audio"; 49 | setup.inputDeviceName = "OBS Audio"; 50 | setup.useDefaultInputChannels = true; 51 | setup.useDefaultOutputChannels = true; 52 | 53 | auto error = audioDeviceManager.setAudioDeviceSetup(setup, true); 54 | if (error.isEmpty()) 55 | { 56 | if (auto* currentDevice = audioDeviceManager.getCurrentAudioDevice()) 57 | { 58 | if (currentDevice->getName() == "OBS Audio") 59 | obsDevice.store(dynamic_cast(currentDevice), std::memory_order_release); 60 | } 61 | return true; 62 | } 63 | 64 | return false; 65 | } 66 | 67 | void processExternalAudio(float** buffer, int numChannels, int numSamples, double sampleRate) 68 | { 69 | ModuleOBSAudioDevice* device = obsDevice.load(std::memory_order_acquire); 70 | 71 | if (device != nullptr && device == audioDeviceManager.getCurrentAudioDevice()) 72 | { 73 | try 74 | { 75 | outputBuffer.setSize(numChannels, numSamples, false, false, true); 76 | 77 | device->processExternalAudio( 78 | const_cast(buffer), 79 | numChannels, 80 | outputBuffer.getArrayOfWritePointers(), 81 | numChannels, 82 | numSamples, 83 | sampleRate 84 | ); 85 | 86 | for (int ch = 0; ch < numChannels; ++ch) 87 | std::copy( 88 | outputBuffer.getReadPointer(ch), 89 | outputBuffer.getReadPointer(ch) + numSamples, 90 | buffer[ch] 91 | ); 92 | 93 | return; 94 | } 95 | catch (...) 96 | { 97 | } 98 | } 99 | 100 | for (int ch = 0; ch < numChannels; ++ch) 101 | std::fill(buffer[ch], buffer[ch] + numSamples, 0.0f); 102 | } 103 | 104 | MidiClient& getMidiClient() 105 | { 106 | return externalMidiClientPtr ? *externalMidiClientPtr : *internalMidiClient; 107 | } 108 | 109 | juce::AudioDeviceManager& getAudioDeviceManager() 110 | { 111 | return audioDeviceManager; 112 | } 113 | 114 | ModuleOBSAudioDevice* getOBSDevice() 115 | { 116 | return obsDevice.load(std::memory_order_acquire); 117 | } 118 | 119 | void cleanup() 120 | { 121 | if (cleanedUp) 122 | return; 123 | cleanedUp = true; 124 | 125 | obsDevice.store(nullptr, std::memory_order_release); 126 | 127 | auto* mm = juce::MessageManager::getInstance(); 128 | if (!mm) 129 | return; 130 | 131 | if (mm->isThisTheMessageThread()) 132 | audioDeviceManager.removeChangeListener(this); 133 | } 134 | 135 | private: 136 | void changeListenerCallback(juce::ChangeBroadcaster* source) override 137 | { 138 | juce::ignoreUnused(source); 139 | 140 | // Update obsDevice pointer based on current device 141 | auto* currentDevice = audioDeviceManager.getCurrentAudioDevice(); 142 | 143 | if (currentDevice != nullptr && currentDevice->getName() == "OBS Audio") 144 | { 145 | auto* obsPtr = dynamic_cast(currentDevice); 146 | obsDevice.store(obsPtr, std::memory_order_release); 147 | } 148 | else 149 | { 150 | obsDevice.store(nullptr, std::memory_order_release); 151 | } 152 | } 153 | 154 | std::unique_ptr customDeviceType; 155 | juce::AudioDeviceManager& audioDeviceManager; 156 | 157 | // MIDI client can be external (referenced) or internal (owned) 158 | MidiClient* externalMidiClientPtr = nullptr; 159 | std::unique_ptr internalMidiClient; 160 | 161 | std::atomic obsDevice{nullptr}; 162 | juce::AudioBuffer outputBuffer{16, 8192}; 163 | bool cleanedUp = false; 164 | }; 165 | 166 | } // namespace atk 167 | -------------------------------------------------------------------------------- /cmake/linux/helpers.cmake: -------------------------------------------------------------------------------- 1 | # CMake Linux helper functions module 2 | 3 | include_guard(GLOBAL) 4 | 5 | include(helpers_common) 6 | 7 | # set_target_properties_plugin: Set target properties for use in obs-studio 8 | function(set_target_properties_plugin target) 9 | set(options "") 10 | set(oneValueArgs "") 11 | set(multiValueArgs PROPERTIES) 12 | cmake_parse_arguments(PARSE_ARGV 0 _STPO "${options}" "${oneValueArgs}" "${multiValueArgs}") 13 | 14 | message(DEBUG "Setting additional properties for target ${target}...") 15 | 16 | while(_STPO_PROPERTIES) 17 | list( 18 | POP_FRONT _STPO_PROPERTIES 19 | key 20 | value 21 | ) 22 | set_property( 23 | TARGET 24 | ${target} 25 | PROPERTY 26 | ${key} 27 | "${value}" 28 | ) 29 | endwhile() 30 | 31 | set_target_properties( 32 | ${target} 33 | PROPERTIES 34 | VERSION 35 | ${PLUGIN_VERSION} 36 | SOVERSION 37 | ${PLUGIN_VERSION_MAJOR} 38 | PREFIX 39 | "" 40 | ) 41 | 42 | message(STATUS "Installing target ${target} to:") 43 | message(STATUS " - LIBRARY: ${CMAKE_INSTALL_LIBDIR}/obs-plugins") 44 | message(STATUS " - RUNTIME: ${CMAKE_INSTALL_BINDIR}") 45 | 46 | install( 47 | TARGETS 48 | ${target} 49 | RUNTIME 50 | DESTINATION ${CMAKE_INSTALL_BINDIR} 51 | LIBRARY 52 | DESTINATION ${CMAKE_INSTALL_LIBDIR}/obs-plugins 53 | ) 54 | 55 | # Additional install for portable component with custom directory structure 56 | if(CMAKE_SIZEOF_VOID_P EQUAL 8) 57 | set(_portable_arch "64bit") 58 | else() 59 | set(_portable_arch "32bit") 60 | endif() 61 | 62 | install( 63 | TARGETS 64 | ${target} 65 | RUNTIME 66 | DESTINATION obs-plugins/${_portable_arch} 67 | LIBRARY 68 | DESTINATION obs-plugins/${_portable_arch} 69 | ) 70 | 71 | if(TARGET plugin-support) 72 | target_link_libraries(${target} PRIVATE plugin-support) 73 | endif() 74 | 75 | add_custom_command( 76 | TARGET ${target} 77 | POST_BUILD 78 | COMMAND 79 | "${CMAKE_COMMAND}" -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/rundir/$" 80 | COMMAND 81 | "${CMAKE_COMMAND}" -E copy_if_different "$" 82 | "${CMAKE_CURRENT_BINARY_DIR}/rundir/$" 83 | COMMENT "Copy ${target} to rundir" 84 | VERBATIM 85 | ) 86 | 87 | target_install_resources(${target}) 88 | 89 | get_target_property(target_sources ${target} SOURCES) 90 | set(target_ui_files ${target_sources}) 91 | list(FILTER target_ui_files INCLUDE REGEX ".+\\.(ui|qrc)") 92 | source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" PREFIX "UI Files" FILES ${target_ui_files}) 93 | endfunction() 94 | 95 | # Helper function to add resources into bundle 96 | function(target_install_resources target) 97 | message(DEBUG "Installing resources for target ${target}...") 98 | if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/data") 99 | file(GLOB_RECURSE data_files "${CMAKE_CURRENT_SOURCE_DIR}/data/*") 100 | foreach(data_file IN LISTS data_files) 101 | cmake_path( 102 | RELATIVE_PATH data_file 103 | BASE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/data/" 104 | OUTPUT_VARIABLE relative_path 105 | ) 106 | cmake_path(GET relative_path PARENT_PATH relative_path) 107 | target_sources(${target} PRIVATE "${data_file}") 108 | source_group("Resources/${relative_path}" FILES "${data_file}") 109 | endforeach() 110 | 111 | message(STATUS "Installing data directory for ${target} to:") 112 | message(STATUS " - DESTINATION: ${CMAKE_INSTALL_DATAROOTDIR}/obs/obs-plugins/${target}") 113 | 114 | install( 115 | DIRECTORY 116 | "${CMAKE_CURRENT_SOURCE_DIR}/data/" 117 | DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/obs/obs-plugins/${target} 118 | USE_SOURCE_PERMISSIONS 119 | ) 120 | 121 | add_custom_command( 122 | TARGET ${target} 123 | POST_BUILD 124 | COMMAND 125 | "${CMAKE_COMMAND}" -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/rundir/$/${target}" 126 | COMMAND 127 | "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/data" 128 | "${CMAKE_CURRENT_BINARY_DIR}/rundir/$/${target}" 129 | COMMENT "Copy ${target} resources to rundir" 130 | VERBATIM 131 | ) 132 | endif() 133 | endfunction() 134 | 135 | # Helper function to add a specific resource to a bundle 136 | function(target_add_resource target resource) 137 | message(DEBUG "Add resource '${resource}' to target ${target} at destination '${target_destination}'...") 138 | 139 | install(FILES "${resource}" DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/obs/obs-plugins/${target}) 140 | 141 | add_custom_command( 142 | TARGET ${target} 143 | POST_BUILD 144 | COMMAND 145 | "${CMAKE_COMMAND}" -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/rundir/$/${target}" 146 | COMMAND 147 | "${CMAKE_COMMAND}" -E copy "${resource}" "${CMAKE_CURRENT_BINARY_DIR}/rundir/$/${target}" 148 | COMMENT "Copy ${target} resource ${resource} to rundir" 149 | VERBATIM 150 | ) 151 | 152 | source_group("Resources" FILES "${resource}") 153 | endfunction() 154 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/PluginHost2/Core/DelayLinePlugin.cpp: -------------------------------------------------------------------------------- 1 | #include "DelayLinePlugin.h" 2 | 3 | DelayLinePlugin::DelayLinePlugin() 4 | : AudioProcessor( 5 | BusesProperties() 6 | .withInput("Input", juce::AudioChannelSet::stereo()) 7 | .withOutput("Output", juce::AudioChannelSet::stereo()) 8 | ) 9 | { 10 | apvts = std::make_unique(*this, nullptr, "state", createParameterLayout()); 11 | delayMsValue = apvts->getRawParameterValue("delay"); 12 | } 13 | 14 | void DelayLinePlugin::prepareToPlay(double sampleRate, int samplesPerBlock) 15 | { 16 | juce::dsp::ProcessSpec spec{ 17 | sampleRate, 18 | static_cast(samplesPerBlock), 19 | static_cast(getTotalNumOutputChannels()) 20 | }; 21 | 22 | maxDelaySamples = static_cast(sampleRate * 10.0); 23 | delayLine.setMaximumDelayInSamples(maxDelaySamples); 24 | delayLine.prepare(spec); 25 | 26 | const auto delayMs = delayMsValue->load(std::memory_order_relaxed); 27 | const auto delaySamples = static_cast(delayMs * sampleRate * 0.001); 28 | delaySmoothed.reset(sampleRate, 0.4); 29 | delaySmoothed.setCurrentAndTargetValue(delaySamples); 30 | } 31 | 32 | void DelayLinePlugin::releaseResources() 33 | { 34 | delayLine.reset(); 35 | } 36 | 37 | void DelayLinePlugin::processBlock(juce::AudioBuffer& buffer, juce::MidiBuffer&) 38 | { 39 | const auto delayMs = delayMsValue->load(std::memory_order_relaxed); 40 | const auto targetDelaySamples = 41 | juce::jlimit(0.0f, static_cast(maxDelaySamples), static_cast(delayMs * getSampleRate() * 0.001)); 42 | 43 | delaySmoothed.setTargetValue(targetDelaySamples); 44 | 45 | const int numSamples = buffer.getNumSamples(); 46 | const int numChannels = buffer.getNumChannels(); 47 | 48 | if (delaySmoothed.isSmoothing()) 49 | { 50 | for (int sample = 0; sample < numSamples; ++sample) 51 | { 52 | const auto currentDelay = 53 | juce::jlimit(0.0f, static_cast(maxDelaySamples), delaySmoothed.getNextValue()); 54 | delayLine.setDelay(currentDelay); 55 | for (int channel = 0; channel < numChannels; ++channel) 56 | { 57 | auto* data = buffer.getWritePointer(channel); 58 | delayLine.pushSample(channel, data[sample]); 59 | data[sample] = delayLine.popSample(channel); 60 | } 61 | } 62 | } 63 | else 64 | { 65 | delayLine.setDelay(targetDelaySamples); 66 | for (int sample = 0; sample < numSamples; ++sample) 67 | { 68 | for (int channel = 0; channel < numChannels; ++channel) 69 | { 70 | auto* data = buffer.getWritePointer(channel); 71 | delayLine.pushSample(channel, data[sample]); 72 | data[sample] = delayLine.popSample(channel); 73 | } 74 | } 75 | } 76 | } 77 | 78 | juce::AudioProcessorEditor* DelayLinePlugin::createEditor() 79 | { 80 | return new DelayLineEditor(*this); 81 | } 82 | 83 | void DelayLinePlugin::getStateInformation(juce::MemoryBlock& destData) 84 | { 85 | if (auto xml = apvts->copyState().createXml()) 86 | copyXmlToBinary(*xml, destData); 87 | } 88 | 89 | void DelayLinePlugin::setStateInformation(const void* data, int sizeInBytes) 90 | { 91 | if (auto xml = std::unique_ptr(getXmlFromBinary(data, sizeInBytes))) 92 | apvts->replaceState(juce::ValueTree::fromXml(*xml)); 93 | } 94 | 95 | bool DelayLinePlugin::isBusesLayoutSupported(const BusesLayout& layouts) const 96 | { 97 | const auto& mainInLayout = layouts.getChannelSet(true, 0); 98 | const auto& mainOutLayout = layouts.getChannelSet(false, 0); 99 | return (mainInLayout == mainOutLayout && (!mainInLayout.isDisabled())); 100 | } 101 | 102 | juce::AudioProcessorValueTreeState::ParameterLayout DelayLinePlugin::createParameterLayout() 103 | { 104 | using namespace juce; 105 | std::vector> params; 106 | 107 | auto delayRange = NormalisableRange(0.0f, 10000.0f, 0.1f); 108 | delayRange.setSkewForCentre(1000.0f); 109 | params.push_back( 110 | std::make_unique( 111 | ParameterID{"delay", 1}, 112 | "Delay (ms)", 113 | delayRange, 114 | 0.0f, 115 | AudioParameterFloatAttributes() 116 | .withStringFromValueFunction([](float value, int) { return String(value, 1) + " ms"; }) 117 | .withValueFromStringFunction([](const String& text) 118 | { return text.trimCharactersAtEnd(" ms").getFloatValue(); }) 119 | ) 120 | ); 121 | 122 | return {params.begin(), params.end()}; 123 | } 124 | 125 | DelayLineEditor::DelayLineEditor(DelayLinePlugin& p) 126 | : AudioProcessorEditor(p) 127 | , processor(p) 128 | , delayAttachment(processor.getApvts(), "delay", delaySlider) 129 | { 130 | setSize(300, 60); 131 | 132 | delayLabel.setText("Delay (ms):", juce::dontSendNotification); 133 | delayLabel.attachToComponent(&delaySlider, true); 134 | addAndMakeVisible(delayLabel); 135 | 136 | delaySlider.setSliderStyle(juce::Slider::LinearHorizontal); 137 | delaySlider.setTextBoxStyle(juce::Slider::TextBoxRight, false, 80, 20); 138 | addAndMakeVisible(delaySlider); 139 | } 140 | 141 | void DelayLineEditor::resized() 142 | { 143 | auto area = getLocalBounds().reduced(8); 144 | auto sliderArea = area.removeFromTop(24); 145 | delaySlider.setBounds(sliderArea.withTrimmedLeft(80)); 146 | } 147 | -------------------------------------------------------------------------------- /cmake/windows/helpers.cmake: -------------------------------------------------------------------------------- 1 | # CMake Windows helper functions module 2 | 3 | include_guard(GLOBAL) 4 | 5 | include(helpers_common) 6 | 7 | # set_target_properties_plugin: Set target properties for use in obs-studio 8 | function(set_target_properties_plugin target) 9 | set(options "") 10 | set(oneValueArgs "") 11 | set(multiValueArgs PROPERTIES) 12 | cmake_parse_arguments(PARSE_ARGV 0 _STPO "${options}" "${oneValueArgs}" "${multiValueArgs}") 13 | 14 | message(DEBUG "Setting additional properties for target ${target}...") 15 | 16 | while(_STPO_PROPERTIES) 17 | list( 18 | POP_FRONT _STPO_PROPERTIES 19 | key 20 | value 21 | ) 22 | set_property( 23 | TARGET 24 | ${target} 25 | PROPERTY 26 | ${key} 27 | "${value}" 28 | ) 29 | endwhile() 30 | 31 | string(TIMESTAMP CURRENT_YEAR "%Y") 32 | 33 | set_target_properties( 34 | ${target} 35 | PROPERTIES 36 | VERSION 37 | 0 38 | SOVERSION 39 | ${PLUGIN_VERSION} 40 | ) 41 | 42 | install(TARGETS ${target} RUNTIME DESTINATION "${target}/bin/64bit" LIBRARY DESTINATION "${target}/bin/64bit") 43 | 44 | install( 45 | FILES 46 | "$" 47 | CONFIGURATIONS 48 | RelWithDebInfo 49 | Debug 50 | DESTINATION "${target}/bin/64bit" 51 | OPTIONAL 52 | ) 53 | 54 | # Additional install for portable component with custom directory structure 55 | if(CMAKE_SIZEOF_VOID_P EQUAL 8) 56 | set(_portable_arch "64bit") 57 | else() 58 | set(_portable_arch "32bit") 59 | endif() 60 | 61 | install( 62 | TARGETS 63 | ${target} 64 | RUNTIME 65 | DESTINATION obs-plugins/${_portable_arch} 66 | LIBRARY 67 | DESTINATION obs-plugins/${_portable_arch} 68 | ) 69 | 70 | if(TARGET plugin-support) 71 | target_link_libraries(${target} PRIVATE plugin-support) 72 | endif() 73 | 74 | add_custom_command( 75 | TARGET ${target} 76 | POST_BUILD 77 | COMMAND 78 | "${CMAKE_COMMAND}" -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/rundir/$" 79 | COMMAND 80 | "${CMAKE_COMMAND}" -E copy_if_different "$" 81 | "$<$:$>" 82 | "${CMAKE_CURRENT_BINARY_DIR}/rundir/$" 83 | COMMENT "Copy ${target} to rundir" 84 | VERBATIM 85 | ) 86 | 87 | target_install_resources(${target}) 88 | 89 | get_target_property(target_sources ${target} SOURCES) 90 | set(target_ui_files ${target_sources}) 91 | list(FILTER target_ui_files INCLUDE REGEX ".+\\.(ui|qrc)") 92 | source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" PREFIX "UI Files" FILES ${target_ui_files}) 93 | 94 | configure_file( 95 | cmake/windows/resources/installer-Windows.iss.in 96 | "${CMAKE_CURRENT_BINARY_DIR}/installer-Windows.generated.iss" 97 | ) 98 | 99 | configure_file(cmake/windows/resources/resource.rc.in "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_PROJECT_NAME}.rc") 100 | target_sources(${CMAKE_PROJECT_NAME} PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_PROJECT_NAME}.rc") 101 | endfunction() 102 | 103 | # Helper function to add resources into bundle 104 | function(target_install_resources target) 105 | message(DEBUG "Installing resources for target ${target}...") 106 | if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/data") 107 | file(GLOB_RECURSE data_files "${CMAKE_CURRENT_SOURCE_DIR}/data/*") 108 | foreach(data_file IN LISTS data_files) 109 | cmake_path( 110 | RELATIVE_PATH data_file 111 | BASE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/data/" 112 | OUTPUT_VARIABLE relative_path 113 | ) 114 | cmake_path(GET relative_path PARENT_PATH relative_path) 115 | target_sources(${target} PRIVATE "${data_file}") 116 | source_group("Resources/${relative_path}" FILES "${data_file}") 117 | endforeach() 118 | 119 | install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/data/" DESTINATION "${target}/data" USE_SOURCE_PERMISSIONS) 120 | 121 | add_custom_command( 122 | TARGET ${target} 123 | POST_BUILD 124 | COMMAND 125 | "${CMAKE_COMMAND}" -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/rundir/$/${target}" 126 | COMMAND 127 | "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/data" 128 | "${CMAKE_CURRENT_BINARY_DIR}/rundir/$/${target}" 129 | COMMENT "Copy ${target} resources to rundir" 130 | VERBATIM 131 | ) 132 | endif() 133 | endfunction() 134 | 135 | # Helper function to add a specific resource to a bundle 136 | function(target_add_resource target resource) 137 | message(DEBUG "Add resource '${resource}' to target ${target} at destination '${target_destination}'...") 138 | 139 | install(FILES "${resource}" DESTINATION "${target}/data") 140 | 141 | add_custom_command( 142 | TARGET ${target} 143 | POST_BUILD 144 | COMMAND 145 | "${CMAKE_COMMAND}" -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/rundir/$/${target}" 146 | COMMAND 147 | "${CMAKE_COMMAND}" -E copy "${resource}" "${CMAKE_CURRENT_BINARY_DIR}/rundir/$/${target}" 148 | COMMENT "Copy ${target} resource ${resource} to rundir" 149 | VERBATIM 150 | ) 151 | source_group("Resources" FILES "${resource}") 152 | endfunction() 153 | -------------------------------------------------------------------------------- /src/plugin_host2.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #define FILTER_NAME "atkAudio PluginHost2" 13 | #define FILTER_ID "atkaudio_plugin_host2" 14 | 15 | #define OPEN_PLUGIN_SETTINGS "open_filter_graph" 16 | #define OPEN_PLUGIN_TEXT "Open Filter Graph" 17 | #define CLOSE_PLUGIN_SETTINGS "close_filter_graph" 18 | #define CLOSE_PLUGIN_TEXT "Close Filter Graph" 19 | 20 | struct pluginhost2_data 21 | { 22 | obs_source_t* context; 23 | obs_source_t* parent; 24 | 25 | atk::PluginHost2 pluginHost2; 26 | 27 | size_t num_channels; 28 | size_t sample_rate; 29 | 30 | bool hasLoadedState = false; 31 | }; 32 | 33 | static const char* pluginhost2_name(void* unused) 34 | { 35 | UNUSED_PARAMETER(unused); 36 | return obs_module_text(FILTER_NAME); 37 | } 38 | 39 | static void save(void* data, obs_data_t* settings) 40 | { 41 | auto* ph = (struct pluginhost2_data*)data; 42 | std::string s; 43 | ph->pluginHost2.getState(s); 44 | 45 | obs_data_set_string(settings, FILTER_ID, s.c_str()); 46 | } 47 | 48 | static void load(void* data, obs_data_t* settings) 49 | { 50 | auto* ph = (struct pluginhost2_data*)data; 51 | if (ph->hasLoadedState) 52 | return; 53 | ph->hasLoadedState = true; 54 | 55 | const char* chunkData = obs_data_get_string(settings, FILTER_ID); 56 | std::string stateStr = chunkData ? chunkData : ""; 57 | ph->pluginHost2.setState(stateStr); 58 | } 59 | 60 | static void pluginhost2_update(void* data, obs_data_t* s) 61 | { 62 | struct pluginhost2_data* ph = (struct pluginhost2_data*)data; 63 | 64 | const uint32_t sample_rate = audio_output_get_sample_rate(obs_get_audio()); 65 | const size_t num_channels = audio_output_get_channels(obs_get_audio()); 66 | 67 | ph->num_channels = num_channels; 68 | ph->sample_rate = sample_rate; 69 | } 70 | 71 | static void* pluginhost2_create(obs_data_t* settings, obs_source_t* filter) 72 | { 73 | struct pluginhost2_data* ph = new pluginhost2_data(); 74 | ph->context = filter; 75 | ph->parent = obs_filter_get_parent(filter); 76 | ph->pluginHost2.setParentSource(ph->parent); 77 | 78 | pluginhost2_update(ph, settings); 79 | 80 | // Load state from settings if present (OBS load callback may not be called for all source types) 81 | const char* chunkData = obs_data_get_string(settings, FILTER_ID); 82 | if (chunkData && strlen(chunkData) > 0) 83 | { 84 | std::string stateStr = chunkData; 85 | ph->pluginHost2.setState(stateStr); 86 | ph->hasLoadedState = true; 87 | } 88 | 89 | return ph; 90 | } 91 | 92 | static void pluginhost2_destroy(void* data) 93 | { 94 | struct pluginhost2_data* ph = (struct pluginhost2_data*)data; 95 | 96 | delete ph; 97 | } 98 | 99 | static struct obs_audio_data* pluginhost2_filter_audio(void* data, struct obs_audio_data* audio) 100 | { 101 | struct pluginhost2_data* ph = (struct pluginhost2_data*)data; 102 | 103 | int num_samples = audio->frames; 104 | if (num_samples == 0) 105 | return audio; 106 | 107 | float** samples = (float**)audio->data; 108 | 109 | ph->pluginHost2.process(samples, (int)ph->num_channels, num_samples, (double)ph->sample_rate); 110 | 111 | return audio; 112 | } 113 | 114 | static bool open_editor_button_clicked(obs_properties_t* props, obs_property_t* property, void* data) 115 | { 116 | obs_property_set_visible(obs_properties_get(props, OPEN_PLUGIN_SETTINGS), false); 117 | obs_property_set_visible(obs_properties_get(props, CLOSE_PLUGIN_SETTINGS), true); 118 | 119 | pluginhost2_data* ph = (pluginhost2_data*)data; 120 | ph->pluginHost2.setVisible(true); 121 | 122 | return true; 123 | } 124 | 125 | static bool close_editor_button_clicked(obs_properties_t* props, obs_property_t* property, void* data) 126 | { 127 | obs_property_set_visible(obs_properties_get(props, OPEN_PLUGIN_SETTINGS), true); 128 | obs_property_set_visible(obs_properties_get(props, CLOSE_PLUGIN_SETTINGS), false); 129 | 130 | pluginhost2_data* ph = (pluginhost2_data*)data; 131 | ph->pluginHost2.setVisible(false); 132 | 133 | return true; 134 | } 135 | 136 | static obs_properties_t* pluginhost2_properties(void* data) 137 | { 138 | struct pluginhost2_data* ph = (struct pluginhost2_data*)data; 139 | obs_properties_t* props = obs_properties_create(); 140 | obs_source_t* parent = NULL; 141 | 142 | if (ph) 143 | parent = obs_filter_get_parent(ph->context); 144 | 145 | obs_properties_add_button(props, OPEN_PLUGIN_SETTINGS, OPEN_PLUGIN_TEXT, open_editor_button_clicked); 146 | obs_properties_add_button(props, CLOSE_PLUGIN_SETTINGS, CLOSE_PLUGIN_TEXT, close_editor_button_clicked); 147 | 148 | bool open_settings_vis = true; 149 | bool close_settings_vis = false; 150 | 151 | obs_property_set_visible(obs_properties_get(props, OPEN_PLUGIN_SETTINGS), open_settings_vis); 152 | obs_property_set_visible(obs_properties_get(props, CLOSE_PLUGIN_SETTINGS), close_settings_vis); 153 | 154 | return props; 155 | } 156 | 157 | static void pluginhost2_filter_add(void* data, obs_source_t* source) 158 | { 159 | struct pluginhost2_data* ph = (struct pluginhost2_data*)data; 160 | ph->parent = source; 161 | ph->pluginHost2.setParentSource(source); 162 | } 163 | 164 | struct obs_source_info pluginhost2_filter = { 165 | .id = FILTER_ID, 166 | .type = OBS_SOURCE_TYPE_FILTER, 167 | .output_flags = OBS_SOURCE_AUDIO, 168 | .get_name = pluginhost2_name, 169 | .create = pluginhost2_create, 170 | .destroy = pluginhost2_destroy, 171 | .get_properties = pluginhost2_properties, 172 | .update = pluginhost2_update, 173 | .filter_audio = pluginhost2_filter_audio, 174 | .save = save, 175 | .load = load, 176 | .filter_add = pluginhost2_filter_add, 177 | }; -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/CpuInfo.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 atkAudio 2 | 3 | #pragma once 4 | 5 | #include 6 | #include 7 | 8 | #ifdef _WIN32 9 | #ifndef NOMINMAX 10 | #define NOMINMAX 11 | #endif 12 | #include 13 | #elif defined(__linux__) 14 | #include 15 | #include 16 | #include 17 | #elif defined(__APPLE__) 18 | #include 19 | #endif 20 | 21 | namespace atk 22 | { 23 | 24 | // Returns the number of physical CPU cores (not hyper-threaded logical cores) 25 | inline int getNumPhysicalCpus() noexcept 26 | { 27 | #ifdef _WIN32 28 | DWORD bufferSize = 0; 29 | GetLogicalProcessorInformation(nullptr, &bufferSize); 30 | 31 | const auto numBuffers = bufferSize / sizeof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION); 32 | if (numBuffers == 0) 33 | return static_cast(std::thread::hardware_concurrency()); 34 | 35 | std::vector buffer(numBuffers); 36 | if (!GetLogicalProcessorInformation(buffer.data(), &bufferSize)) 37 | return static_cast(std::thread::hardware_concurrency()); 38 | 39 | int count = 0; 40 | for (const auto& info : buffer) 41 | if (info.Relationship == RelationProcessorCore) 42 | ++count; 43 | 44 | return count > 0 ? count : static_cast(std::thread::hardware_concurrency()); 45 | 46 | #elif defined(__linux__) 47 | FILE* f = fopen("/proc/cpuinfo", "r"); 48 | if (!f) 49 | return static_cast(std::thread::hardware_concurrency()); 50 | 51 | char line[256]; 52 | int cpuCores = 0; 53 | int physicalId = -1; 54 | 55 | while (fgets(line, sizeof(line), f)) 56 | { 57 | if (strncmp(line, "cpu cores", 9) == 0) 58 | { 59 | const char* colon = strchr(line, ':'); 60 | if (colon) 61 | cpuCores = atoi(colon + 1); 62 | } 63 | else if (strncmp(line, "physical id", 11) == 0) 64 | { 65 | const char* colon = strchr(line, ':'); 66 | if (colon) 67 | { 68 | int id = atoi(colon + 1); 69 | if (id > physicalId) 70 | physicalId = id; 71 | } 72 | } 73 | } 74 | fclose(f); 75 | 76 | int result = cpuCores * (physicalId + 1); 77 | return result > 0 ? result : static_cast(std::thread::hardware_concurrency()); 78 | 79 | #elif defined(__APPLE__) 80 | int count = 0; 81 | size_t size = sizeof(count); 82 | if (sysctlbyname("hw.physicalcpu", &count, &size, nullptr, 0) == 0 && count > 0) 83 | return count; 84 | return static_cast(std::thread::hardware_concurrency()); 85 | 86 | #else 87 | return static_cast(std::thread::hardware_concurrency()); 88 | #endif 89 | } 90 | 91 | // Returns mapping of physical core indices to their primary logical core IDs 92 | // On SMT/HT systems, returns the first logical core of each physical core 93 | inline std::vector getPhysicalCoreMapping() noexcept 94 | { 95 | std::vector mapping; 96 | 97 | #ifdef _WIN32 98 | DWORD bufferSize = 0; 99 | GetLogicalProcessorInformation(nullptr, &bufferSize); 100 | 101 | const auto numBuffers = bufferSize / sizeof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION); 102 | if (numBuffers == 0) 103 | { 104 | const auto logical = std::thread::hardware_concurrency(); 105 | for (unsigned i = 0; i < logical; ++i) 106 | mapping.push_back(static_cast(i)); 107 | return mapping; 108 | } 109 | 110 | std::vector buffer(numBuffers); 111 | if (!GetLogicalProcessorInformation(buffer.data(), &bufferSize)) 112 | { 113 | const auto logical = std::thread::hardware_concurrency(); 114 | for (unsigned i = 0; i < logical; ++i) 115 | mapping.push_back(static_cast(i)); 116 | return mapping; 117 | } 118 | 119 | for (const auto& info : buffer) 120 | { 121 | if (info.Relationship == RelationProcessorCore) 122 | { 123 | DWORD_PTR mask = info.ProcessorMask; 124 | for (int bit = 0; bit < static_cast(sizeof(DWORD_PTR) * 8); ++bit) 125 | { 126 | if (mask & (static_cast(1) << bit)) 127 | { 128 | mapping.push_back(bit); 129 | break; 130 | } 131 | } 132 | } 133 | } 134 | 135 | #elif defined(__linux__) 136 | FILE* f = fopen("/sys/devices/system/cpu/present", "r"); 137 | int maxCpu = static_cast(std::thread::hardware_concurrency()) - 1; 138 | if (f) 139 | { 140 | if (fscanf(f, "%*d-%d", &maxCpu) != 1) 141 | maxCpu = static_cast(std::thread::hardware_concurrency()) - 1; 142 | fclose(f); 143 | } 144 | 145 | std::vector coreIds(maxCpu + 1, -1); 146 | for (int cpu = 0; cpu <= maxCpu; ++cpu) 147 | { 148 | char path[256]; 149 | snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%d/topology/core_id", cpu); 150 | FILE* cf = fopen(path, "r"); 151 | if (cf) 152 | { 153 | int coreId = -1; 154 | if (fscanf(cf, "%d", &coreId) == 1) 155 | coreIds[cpu] = coreId; 156 | fclose(cf); 157 | } 158 | } 159 | 160 | std::vector seen(maxCpu + 1, false); 161 | for (int cpu = 0; cpu <= maxCpu; ++cpu) 162 | { 163 | int coreId = coreIds[cpu]; 164 | if (coreId >= 0 && !seen[coreId]) 165 | { 166 | seen[coreId] = true; 167 | mapping.push_back(cpu); 168 | } 169 | } 170 | 171 | #elif defined(__APPLE__) 172 | const auto logical = std::thread::hardware_concurrency(); 173 | int physicalCount = 0; 174 | size_t size = sizeof(physicalCount); 175 | 176 | if (sysctlbyname("hw.physicalcpu", &physicalCount, &size, nullptr, 0) == 0 && physicalCount > 0) 177 | { 178 | const int stride = static_cast(logical) / physicalCount; 179 | for (int i = 0; i < physicalCount; ++i) 180 | mapping.push_back(i * stride); 181 | } 182 | else 183 | { 184 | for (unsigned i = 0; i < logical; ++i) 185 | mapping.push_back(static_cast(i)); 186 | } 187 | 188 | #else 189 | const auto logical = std::thread::hardware_concurrency(); 190 | for (unsigned i = 0; i < logical; ++i) 191 | mapping.push_back(static_cast(i)); 192 | #endif 193 | 194 | if (mapping.empty()) 195 | { 196 | const auto logical = std::thread::hardware_concurrency(); 197 | for (unsigned i = 0; i < logical; ++i) 198 | mapping.push_back(static_cast(i)); 199 | } 200 | 201 | return mapping; 202 | } 203 | 204 | } // namespace atk 205 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/UpdateCheck.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "LookAndFeel.h" 3 | 4 | #include 5 | #include 6 | 7 | constexpr auto OWNER = "atkAudio"; 8 | constexpr auto DISPLAY_NAME = PLUGIN_DISPLAY_NAME; 9 | constexpr auto REPO = "PluginForObsRelease"; 10 | constexpr auto VERSION = PLUGIN_VERSION; 11 | constexpr auto JSON_VALUE = "tag_name"; 12 | constexpr auto FILENAME = "atkaudio-pluginforobs.zip"; 13 | 14 | // Define to always check for updates and simulate newer version available 15 | // #define SIMULATE_UPDATE_CHECK 16 | 17 | class UpdateCheck 18 | : public juce::ModalComponentManager::Callback 19 | , public juce::DeletedAtShutdown 20 | { 21 | public: 22 | UpdateCheck(const juce::String& repoOwner = OWNER, const juce::String& repoName = REPO) 23 | : DeletedAtShutdown() 24 | , owner(repoOwner) 25 | , repo(repoName) 26 | { 27 | checkForUpdate(); 28 | } 29 | 30 | juce::String getValueFromJson(const juce::String& jsonString, const juce::String& key) 31 | { 32 | juce::var json = juce::JSON::parse(jsonString); 33 | if (json.isObject()) 34 | { 35 | auto jsonObject = json.getDynamicObject(); 36 | if (jsonObject->hasProperty(key)) 37 | return jsonObject->getProperty(key).toString(); 38 | } 39 | return {}; 40 | } 41 | 42 | bool isNewerVersionThanCurrent( 43 | const juce::String& remoteVersion, // 44 | const juce::String& localVersion = VERSION 45 | ) 46 | { 47 | jassert(remoteVersion.isNotEmpty()); 48 | 49 | auto remoteTokens = juce::StringArray::fromTokens(remoteVersion, ".", {}); 50 | auto localTokens = juce::StringArray::fromTokens(localVersion, ".", {}); 51 | 52 | if (remoteTokens[0].getIntValue() == localTokens[0].getIntValue()) 53 | { 54 | if (remoteTokens[1].getIntValue() == localTokens[1].getIntValue()) 55 | return remoteTokens[2].getIntValue() > localTokens[2].getIntValue(); 56 | 57 | return remoteTokens[1].getIntValue() > localTokens[1].getIntValue(); 58 | } 59 | 60 | return remoteTokens[0].getIntValue() > localTokens[0].getIntValue(); 61 | } 62 | 63 | void checkForUpdate() 64 | { 65 | auto appDir = 66 | juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory).getChildFile(PLUGIN_DISPLAY_NAME); 67 | appDir.createDirectory(); 68 | 69 | juce::File lastVersionFile = appDir.getChildFile("version_check"); 70 | 71 | #ifndef SIMULATE_UPDATE_CHECK 72 | if (lastVersionFile.existsAsFile()) 73 | { 74 | auto creationTime = lastVersionFile.getCreationTime(); 75 | auto now = juce::Time::getCurrentTime(); 76 | auto threeMonthsMs = (long long)3 * 30 * 24 * 60 * 60 * 1000; 77 | if (now.toMilliseconds() - creationTime.toMilliseconds() > threeMonthsMs) 78 | lastVersionFile.deleteFile(); 79 | } 80 | 81 | if (!lastVersionFile.existsAsFile()) 82 | { 83 | lastVersionFile.create(); 84 | } 85 | else if (juce::Time::getCurrentTime().toMilliseconds() 86 | - lastVersionFile.getLastModificationTime().toMilliseconds() 87 | < 7 * 24 * 60 * 60 * 1000) 88 | { 89 | DBG("last modification time: " << lastVersionFile.getLastModificationTime().toString(true, true)); 90 | return; 91 | } 92 | #endif 93 | 94 | // Past 7 days - do the check 95 | juce::URL versionURL("https://api.github.com/repos/" + owner + "/" + repo + "/releases/latest"); 96 | 97 | std::unique_ptr inStream(versionURL.createInputStream( 98 | juce::URL::InputStreamOptions(juce::URL::ParameterHandling::inAddress).withConnectionTimeoutMs(5000) 99 | )); 100 | 101 | if (inStream == nullptr) 102 | return; 103 | 104 | auto jsonResponse = inStream->readEntireStreamAsString().trim(); 105 | 106 | if (jsonResponse.isEmpty()) 107 | return; 108 | 109 | auto remoteVersionString = getValueFromJson(jsonResponse, JSON_VALUE); 110 | releaseNotes = getValueFromJson(jsonResponse, "body"); 111 | 112 | latestRemoteVersion = remoteVersionString; 113 | 114 | #ifdef SIMULATE_UPDATE_CHECK 115 | latestRemoteVersion = "99.99.99"; 116 | #endif 117 | 118 | #ifndef SIMULATE_UPDATE_CHECK 119 | // Update mod time now that we've checked 120 | lastVersionFile.setLastModificationTime(juce::Time::getCurrentTime()); 121 | 122 | // If this version was previously skipped, don't show the alert again 123 | auto skippedVersion = lastVersionFile.loadFileAsString().trim(); 124 | if (skippedVersion.isNotEmpty() && skippedVersion == latestRemoteVersion) 125 | return; 126 | #endif 127 | 128 | auto isRemoteVersionNewer = isNewerVersionThanCurrent(latestRemoteVersion); 129 | 130 | if (isRemoteVersionNewer) 131 | { 132 | juce::String message = "A new version is available: " + latestRemoteVersion; 133 | if (releaseNotes.isNotEmpty()) 134 | { 135 | auto formattedNotes = releaseNotes.replace("\n", "\n\n"); 136 | message += "\n\n" + formattedNotes; 137 | } 138 | 139 | juce::AlertWindow::showYesNoCancelBox( 140 | juce::AlertWindow::InfoIcon, 141 | PLUGIN_DISPLAY_NAME, 142 | message, 143 | "Download", 144 | "Skip this version", 145 | "Cancel", 146 | nullptr, 147 | this 148 | ); 149 | } 150 | } 151 | 152 | private: 153 | void modalStateFinished(int returnValue) override 154 | { 155 | if (returnValue == 1) 156 | { 157 | juce::URL("https://github.com/" + owner + "/" + repo + "/releases/latest/download/" + FILENAME) 158 | .launchInDefaultBrowser(); 159 | } 160 | else if (returnValue == 2) 161 | { 162 | // User clicked "Skip this version" 163 | auto appDir = juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory) 164 | .getChildFile(PLUGIN_DISPLAY_NAME); 165 | appDir.createDirectory(); 166 | auto lastVersionFile = appDir.getChildFile("version_check"); 167 | if (!latestRemoteVersion.isEmpty()) 168 | lastVersionFile.replaceWithText(latestRemoteVersion); 169 | } 170 | } 171 | 172 | juce::String owner; 173 | juce::String repo; 174 | juce::String latestRemoteVersion; 175 | juce::String releaseNotes; 176 | 177 | JUCE_DECLARE_SINGLETON(UpdateCheck, true) 178 | }; -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/ModuleInfrastructure/MidiServer/MidiServer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace atk 10 | { 11 | 12 | class MidiServer; 13 | 14 | struct MidiClientState 15 | { 16 | juce::StringArray subscribedInputDevices; 17 | juce::StringArray subscribedOutputDevices; 18 | 19 | juce::String serialize() const; 20 | void deserialize(const juce::String& data); 21 | }; 22 | 23 | class MidiMessageQueue; 24 | 25 | class MidiClient 26 | { 27 | public: 28 | explicit MidiClient(int queueSize = 65536); 29 | ~MidiClient(); 30 | 31 | MidiClient(const MidiClient&) = delete; 32 | MidiClient& operator=(const MidiClient&) = delete; 33 | MidiClient(MidiClient&&) noexcept; 34 | MidiClient& operator=(MidiClient&&) noexcept; 35 | 36 | void getPendingMidi(juce::MidiBuffer& outBuffer, int numSamples, double sampleRate); 37 | 38 | void sendMidi(const juce::MidiBuffer& messages); 39 | 40 | void injectMidi(const juce::MidiBuffer& messages); 41 | 42 | void setSubscriptions(const MidiClientState& state); 43 | 44 | MidiClientState getSubscriptions() const; 45 | 46 | void* getClientId() const 47 | { 48 | return clientId; 49 | } 50 | 51 | private: 52 | void* clientId; 53 | AtomicSharedPtr incomingQueue; 54 | AtomicSharedPtr outgoingQueue; 55 | }; 56 | 57 | class MidiMessageQueue 58 | { 59 | public: 60 | static constexpr int kDefaultQueueSize = 65536; 61 | 62 | struct TimestampedMidiMessage 63 | { 64 | juce::MidiMessage message; 65 | int samplePosition = 0; 66 | 67 | TimestampedMidiMessage() = default; 68 | 69 | TimestampedMidiMessage(const juce::MidiMessage& msg, int pos) 70 | : message(msg) 71 | , samplePosition(pos) 72 | { 73 | } 74 | }; 75 | 76 | explicit MidiMessageQueue(int queueSize = kDefaultQueueSize) 77 | : fifo(queueSize) 78 | { 79 | messages.resize(queueSize); 80 | } 81 | 82 | bool push(const juce::MidiMessage& message, int samplePosition) 83 | { 84 | juce::SpinLock::ScopedLockType lock(producerLock); 85 | 86 | int start1, size1, start2, size2; 87 | fifo.prepareToWrite(1, start1, size1, start2, size2); 88 | 89 | if (size1 > 0) 90 | { 91 | messages[start1].message = message; 92 | messages[start1].samplePosition = 93 | (samplePosition == 0) ? autoIncrementPosition.fetch_add(1, std::memory_order_relaxed) : samplePosition; 94 | fifo.finishedWrite(1); 95 | return true; 96 | } 97 | return false; 98 | } 99 | 100 | void popAll(juce::MidiBuffer& outBuffer, int maxSamples = 65536) 101 | { 102 | int start1, size1, start2, size2; 103 | const int numReady = fifo.getNumReady(); 104 | 105 | if (numReady > 0) 106 | { 107 | fifo.prepareToRead(numReady, start1, size1, start2, size2); 108 | const int maxPos = (maxSamples > 0) ? maxSamples - 1 : 0; 109 | 110 | for (int i = 0; i < size1; ++i) 111 | { 112 | const auto& msg = messages[start1 + i]; 113 | outBuffer.addEvent(msg.message, std::clamp(msg.samplePosition, 0, maxPos)); 114 | } 115 | 116 | for (int i = 0; i < size2; ++i) 117 | { 118 | const auto& msg = messages[start2 + i]; 119 | outBuffer.addEvent(msg.message, std::clamp(msg.samplePosition, 0, maxPos)); 120 | } 121 | 122 | fifo.finishedRead(size1 + size2); 123 | autoIncrementPosition.store(0, std::memory_order_relaxed); 124 | } 125 | } 126 | 127 | void clear() 128 | { 129 | int start1, size1, start2, size2; 130 | const int numReady = fifo.getNumReady(); 131 | if (numReady > 0) 132 | { 133 | fifo.prepareToRead(numReady, start1, size1, start2, size2); 134 | fifo.finishedRead(size1 + size2); 135 | } 136 | } 137 | 138 | int getNumReady() const 139 | { 140 | return fifo.getNumReady(); 141 | } 142 | 143 | private: 144 | juce::SpinLock producerLock; 145 | std::atomic autoIncrementPosition{0}; 146 | juce::AbstractFifo fifo; 147 | std::vector messages; 148 | }; 149 | 150 | class MidiServer 151 | : public juce::DeletedAtShutdown 152 | , private juce::MidiInputCallback 153 | , private juce::Timer 154 | { 155 | public: 156 | JUCE_DECLARE_SINGLETON(MidiServer, false) 157 | ~MidiServer() override; 158 | 159 | void initialize(); 160 | void shutdown(); 161 | 162 | void registerClient( 163 | void* clientId, 164 | const MidiClientState& state, 165 | int queueSize, 166 | std::shared_ptr& outIncomingQueue, 167 | std::shared_ptr& outOutgoingQueue 168 | ); 169 | void unregisterClient(void* clientId); 170 | void updateClientSubscriptions(void* clientId, const MidiClientState& state); 171 | MidiClientState getClientState(void* clientId) const; 172 | 173 | juce::StringArray getAvailableMidiInputDevices() const; 174 | juce::StringArray getAvailableMidiOutputDevices() const; 175 | 176 | juce::AudioDeviceManager& getAudioDeviceManager() 177 | { 178 | return deviceManager; 179 | } 180 | 181 | private: 182 | MidiServer(); 183 | 184 | void handleIncomingMidiMessage(juce::MidiInput* source, const juce::MidiMessage& message) override; 185 | void timerCallback() override; 186 | void updateMidiDeviceSubscriptions(); 187 | void rebuildClientSnapshot(); 188 | 189 | struct ClientInfo 190 | { 191 | MidiClientState state; 192 | std::shared_ptr incomingMidiQueue; 193 | std::shared_ptr outgoingMidiQueue; 194 | }; 195 | 196 | struct ClientSnapshot 197 | { 198 | std::shared_ptr incomingMidiQueue; 199 | std::shared_ptr outgoingMidiQueue; 200 | MidiClientState state; 201 | }; 202 | 203 | struct DeviceSnapshot 204 | { 205 | std::unordered_map> inputSubscriptions; 206 | std::unordered_map> outputSubscriptions; 207 | }; 208 | 209 | juce::AudioDeviceManager deviceManager; 210 | mutable juce::CriticalSection clientsMutex; 211 | std::unordered_map clients; 212 | AtomicSharedPtr activeSnapshot{std::make_shared()}; 213 | juce::HashMap outputDevices; 214 | bool initialized = false; 215 | 216 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MidiServer) 217 | }; 218 | 219 | } // namespace atk 220 | -------------------------------------------------------------------------------- /lib/atkaudio/src/atkaudio/atkaudio.cpp: -------------------------------------------------------------------------------- 1 | #include "atkaudio.h" 2 | 3 | #include "AudioProcessorGraphMT/RealtimeThreadPool.h" 4 | #include "JuceApp.h" 5 | #include "LookAndFeel.h" 6 | #include "MessagePump.h" 7 | #include "ModuleInfrastructure/AudioServer/AudioServer.h" 8 | #include "ModuleInfrastructure/MidiServer/MidiServer.h" 9 | #include "UpdateCheck.h" 10 | 11 | #include 12 | 13 | #ifdef ENABLE_QT 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #endif 20 | 21 | #include 22 | 23 | UpdateCheck* updateCheck = nullptr; 24 | atk::MessagePump* g_messagePump = nullptr; 25 | 26 | // Store Qt main window handle for per-instance parent creation (lazy-initialized) 27 | static void* g_qtMainWindowHandle = nullptr; 28 | static bool g_qtMainWindowInitialized = false; 29 | 30 | void atk::create() 31 | { 32 | // Set createInstance to make JUCE think this is a standalone app. 33 | // This enables per-monitor DPI awareness on Windows via setDPIAwareness(). 34 | #ifndef JUCE_MAC 35 | juce::JUCEApplicationBase::createInstance = []() -> juce::JUCEApplicationBase* { return nullptr; }; 36 | #endif 37 | juce::initialiseJuce_GUI(); 38 | 39 | juce::MessageManager::getInstance()->setCurrentThreadAsMessageThread(); 40 | 41 | // Initialize LookAndFeel singleton 42 | juce::SharedResourcePointer lookAndFeel; 43 | 44 | // Sync OBS theme colors to JUCE LookAndFeel 45 | getQtMainWindowHandle(); 46 | 47 | // Initialize MIDI server 48 | if (auto* midiServer = atk::MidiServer::getInstance()) 49 | midiServer->initialize(); 50 | 51 | // Initialize Audio server 52 | if (auto* audioServer = atk::AudioServer::getInstance()) 53 | audioServer->initialize(); 54 | 55 | // Initialize RealtimeThreadPool synchronously so it's ready when filters are created 56 | if (auto* threadPool = atk::RealtimeThreadPool::getInstance()) 57 | threadPool->initialize(); 58 | } 59 | 60 | void atk::pump() 61 | { 62 | #if JUCE_LINUX 63 | // On Linux, we need to pump the JUCE message loop 64 | // Use dispatchPendingMessages() instead of runDispatchLoopUntil() to avoid 65 | // conflicts with Qt's event loop - we just want to process pending JUCE messages 66 | // without polling/blocking for new ones 67 | if (auto* mm = juce::MessageManager::getInstanceWithoutCreating()) 68 | { 69 | // Only dispatch if we're on the message thread 70 | if (mm->isThisTheMessageThread()) 71 | { 72 | // Process any pending async callbacks 73 | mm->runDispatchLoopUntil(0); // 0ms = just process pending, don't wait 74 | } 75 | } 76 | #endif 77 | } 78 | 79 | void atk::startMessagePump(QObject* qtParent) 80 | { 81 | #if JUCE_LINUX 82 | // On Linux, we need a Qt timer-based message pump for JUCE 83 | if (g_messagePump) 84 | { 85 | DBG("startMessagePump: MessagePump already started"); 86 | return; 87 | } 88 | 89 | g_messagePump = new atk::MessagePump(qtParent); 90 | #else 91 | // On macOS and Windows, we don't need a message pump - JUCE integrates with native event loops 92 | (void)qtParent; 93 | #endif 94 | } 95 | 96 | void atk::destroy() 97 | { 98 | juce::MessageManager::getInstance()->setCurrentThreadAsMessageThread(); 99 | 100 | g_messagePump = nullptr; 101 | 102 | if (auto* midiServer = atk::MidiServer::getInstance()) 103 | { 104 | midiServer->shutdown(); 105 | atk::MidiServer::deleteInstance(); 106 | } 107 | 108 | if (auto* audioServer = atk::AudioServer::getInstance()) 109 | { 110 | audioServer->shutdown(); 111 | atk::AudioServer::deleteInstance(); 112 | } 113 | 114 | if (auto* threadPool = atk::RealtimeThreadPool::getInstance()) 115 | { 116 | threadPool->shutdown(); 117 | atk::RealtimeThreadPool::deleteInstance(); 118 | } 119 | 120 | juce::shutdownJuce_GUI(); 121 | } 122 | 123 | void atk::update() 124 | { 125 | if (updateCheck == nullptr) 126 | updateCheck = new UpdateCheck(); // deleted at shutdown 127 | } 128 | 129 | void* atk::getQtMainWindowHandle() 130 | { 131 | // Fully lazy initialization: get Qt window, extract handle, and apply colors on first access 132 | if (!g_qtMainWindowInitialized) 133 | { 134 | g_qtMainWindowInitialized = true; 135 | 136 | #ifdef ENABLE_QT 137 | // Get Qt main window from OBS frontend API 138 | QWidget* mainQWidget = (QWidget*)obs_frontend_get_main_window(); 139 | if (!mainQWidget) 140 | { 141 | DBG("getQtMainWindowHandle: obs_frontend_get_main_window() returned null"); 142 | return nullptr; 143 | } 144 | 145 | // Extract native window handle 146 | void* nativeHandle = nullptr; 147 | #ifdef _WIN32 148 | nativeHandle = reinterpret_cast(mainQWidget->winId()); 149 | #elif defined(__APPLE__) 150 | if (auto* window = mainQWidget->windowHandle()) 151 | nativeHandle = reinterpret_cast(window->winId()); 152 | #elif defined(__linux__) 153 | nativeHandle = reinterpret_cast(mainQWidget->winId()); 154 | #endif 155 | 156 | if (nativeHandle) 157 | { 158 | g_qtMainWindowHandle = nativeHandle; 159 | DBG("getQtMainWindowHandle: Extracted native handle on first access"); 160 | DBG(" Native handle: " + juce::String::toHexString((juce::pointer_sized_int)nativeHandle)); 161 | } 162 | else 163 | { 164 | DBG("getQtMainWindowHandle: Failed to extract native handle"); 165 | } 166 | 167 | // Apply OBS theme colors to JUCE 168 | QPalette palette = mainQWidget->palette(); 169 | QColor bgColor = palette.color(QPalette::Window); 170 | QColor fgColor = palette.color(QPalette::WindowText); 171 | 172 | auto bgColour = juce::Colour(bgColor.red(), bgColor.green(), bgColor.blue()); 173 | auto fgColour = juce::Colour(fgColor.red(), fgColor.green(), fgColor.blue()); 174 | atk::LookAndFeel::applyColorsToInstance(bgColour, fgColour); 175 | 176 | DBG("getQtMainWindowHandle: Applied OBS theme colors"); 177 | #endif 178 | } 179 | 180 | // Return the cached Qt main window handle 181 | return g_qtMainWindowHandle; 182 | } 183 | 184 | void atk::setWindowOwnership(juce::Component* component) 185 | { 186 | // With the invisible parent component attached to Qt, JUCE automatically 187 | // handles the window hierarchy. No manual platform-specific code needed! 188 | // All JUCE windows are now children of our parent component. 189 | (void)component; // Unused - kept for API compatibility 190 | } 191 | 192 | void atk::applyColors(uint8_t bgR, uint8_t bgG, uint8_t bgB, uint8_t fgR, uint8_t fgG, uint8_t fgB) 193 | { 194 | auto bgColour = juce::Colour(bgR, bgG, bgB); 195 | auto fgColour = juce::Colour(fgR, fgG, fgB); 196 | atk::LookAndFeel::applyColorsToInstance(bgColour, fgColour); 197 | } 198 | 199 | void atk::logMessage(const juce::String& message) 200 | { 201 | DBG(message); 202 | } 203 | -------------------------------------------------------------------------------- /linux-cross-compile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Parse command-line arguments 5 | TARGET_ARCH="" 6 | DEBIAN_ARCH="" 7 | APT_ARCH_SUFFIX="" 8 | CROSS_COMPILE="false" 9 | CROSS_TRIPLE="" 10 | CMAKE_SYSTEM_PROCESSOR="" 11 | BUILD_CONFIG="RelWithDebInfo" 12 | 13 | while [[ $# -gt 0 ]]; do 14 | case $1 in 15 | --target-arch) TARGET_ARCH="$2"; shift 2 ;; 16 | --debian-arch) DEBIAN_ARCH="$2"; shift 2 ;; 17 | --apt-arch-suffix) APT_ARCH_SUFFIX="$2"; shift 2 ;; 18 | --cross-compile) CROSS_COMPILE="$2"; shift 2 ;; 19 | --cross-triple) CROSS_TRIPLE="$2"; shift 2 ;; 20 | --cmake-system-processor) CMAKE_SYSTEM_PROCESSOR="$2"; shift 2 ;; 21 | --build-config) BUILD_CONFIG="$2"; shift 2 ;; 22 | *) echo "Unknown parameter: $1"; exit 1 ;; 23 | esac 24 | done 25 | 26 | # Validate required parameters 27 | if [ -z "$TARGET_ARCH" ]; then 28 | echo "ERROR: --target-arch is required" 29 | exit 1 30 | fi 31 | 32 | # Map TARGET_ARCH to cross-compile triple prefix for binutils 33 | case "${TARGET_ARCH}" in 34 | arm64) export CROSS_PREFIX="aarch64" ;; 35 | *) export CROSS_PREFIX="${TARGET_ARCH}" ;; 36 | esac 37 | 38 | # Verify we're running on x86_64 (not ARM64 emulation) 39 | echo "Container architecture: $(uname -m)" 40 | [ "$(uname -m)" = "aarch64" ] && echo "ERROR: Running on ARM64 emulation!" && exit 1 41 | 42 | # Configure environment 43 | export DEBIAN_FRONTEND=noninteractive 44 | 45 | # Install base packages 46 | apt-get update 47 | apt-get install -y --no-install-recommends git software-properties-common 48 | git config --global --add safe.directory /workspace 49 | 50 | # Configure multi-arch for cross-compilation 51 | if [ "${CROSS_COMPILE}" = "true" ]; then 52 | echo "=== Setting up cross-compilation for ${TARGET_ARCH} ===" 53 | 54 | # Restrict default sources to amd64 (Ubuntu 24.04+ DEB822 format) 55 | for sources_file in /etc/apt/sources.list.d/*.sources; do 56 | [ -f "$sources_file" ] && sed -i '/^Types: deb$/a Architectures: amd64' "$sources_file" 57 | done 58 | 59 | # Add ports mirror for ARM64 60 | if [[ "${DEBIAN_ARCH}" =~ ^(arm64|armhf|ppc64el|riscv64|s390x)$ ]]; then 61 | cat > /etc/apt/sources.list.d/ports.sources < /etc/apt/preferences.d/no-foreign-python < /etc/apt/preferences.d/no-foreign-bin < /tmp/toolchain.cmake </dev/null || true 179 | fi 180 | 181 | 182 | 183 | --------------------------------------------------------------------------------