diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 2f9b392..f74c4ba 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -17,3 +17,4 @@ endif() add_subdirectory(GainPlugin) add_subdirectory(NoteNamesPlugin) +add_subdirectory(HostSpecificExtensionsPlugin) diff --git a/examples/HostSpecificExtensionsPlugin/CMakeLists.txt b/examples/HostSpecificExtensionsPlugin/CMakeLists.txt new file mode 100644 index 0000000..2c8cd02 --- /dev/null +++ b/examples/HostSpecificExtensionsPlugin/CMakeLists.txt @@ -0,0 +1,61 @@ +if(CLAP_WRAP_PROJUCER_PLUGIN) + return() +endif() + +if(NOT DEFINED REAPER_SDK_PATH) + message(STATUS "REAPER SDK path not supplied, skipping configuration for host-specific extensions plugin.") + return() +endif() + +if (NOT EXISTS "${REAPER_SDK_PATH}/sdk/reaper_plugin.h") + message(WARNING "REAPER SDK: reaper_plugin.h not found! (Looking at: ${REAPER_SDK_PATH}/sdk/reaper_plugin.h)") +endif() + +message(STATUS "Configuring host-specific extensions plugin with REAPER SDK: ${REAPER_SDK_PATH}") + +juce_add_plugin(HostSpecificExtensionsPlugin + COMPANY_NAME "${COMPANY_NAME}" + PLUGIN_MANUFACTURER_CODE "${COMPANY_CODE}" + PLUGIN_CODE Hsep + FORMATS ${JUCE_FORMATS} + PRODUCT_NAME "Host-Specific Extensions Tester" +) + +clap_juce_extensions_plugin( + TARGET HostSpecificExtensionsPlugin + CLAP_ID "org.free-audio.HostSpecificExtensionsPlugin" + CLAP_FEATURES audio-effect utility + CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES 64 +) + +target_sources(HostSpecificExtensionsPlugin PRIVATE + HostSpecificExtensionsPlugin.cpp + PluginEditor.cpp +) + +target_compile_definitions(HostSpecificExtensionsPlugin PUBLIC + JUCE_DISPLAY_SPLASH_SCREEN=1 + JUCE_REPORT_APP_USAGE=0 + JUCE_WEB_BROWSER=0 + JUCE_USE_CURL=0 + JUCE_JACK=1 + JUCE_ALSA=1 + JUCE_MODAL_LOOPS_PERMITTED=1 # required for Linux FileChooser with JUCE 6.0.7 + JUCE_VST3_CAN_REPLACE_VST2=0 +) + +target_include_directories(HostSpecificExtensionsPlugin + PRIVATE + "${REAPER_SDK_PATH}/sdk" +) + +target_link_libraries(HostSpecificExtensionsPlugin + PRIVATE + juce::juce_audio_utils + juce::juce_audio_plugin_client + clap_juce_extensions + PUBLIC + juce::juce_recommended_config_flags + juce::juce_recommended_lto_flags + juce::juce_recommended_warning_flags +) diff --git a/examples/HostSpecificExtensionsPlugin/HostSpecificExtensionsPlugin.cpp b/examples/HostSpecificExtensionsPlugin/HostSpecificExtensionsPlugin.cpp new file mode 100644 index 0000000..0397b1a --- /dev/null +++ b/examples/HostSpecificExtensionsPlugin/HostSpecificExtensionsPlugin.cpp @@ -0,0 +1,71 @@ +#include "HostSpecificExtensionsPlugin.h" +#include "PluginEditor.h" + +HostSpecificExtensionsPlugin::HostSpecificExtensionsPlugin() + : juce::AudioProcessor(BusesProperties() + .withInput("Input", juce::AudioChannelSet::stereo(), true) + .withOutput("Output", juce::AudioChannelSet::stereo(), true)) +{ + // load extensions here! + reaperPluginExtension = + static_cast(getExtension("cockos.reaper_extension")); + jassert(reaperPluginExtension != nullptr || !juce::PluginHostType{}.isReaper()); + + if (reaperPluginExtension != nullptr) + { + // we want to check that we can load/use the extensions in the plugin constructor. + // for REAPER our silly test is to try muting track 0. + using GetMasterTrackFunc = MediaTrack *(*)(ReaProject *); + auto getMasterTrackFunc = + reinterpret_cast(reaperPluginExtension->GetFunc("GetMasterTrack")); + auto *masterTrack = getMasterTrackFunc(nullptr); + + using SetMuteFunc = int (*)(MediaTrack *track, int mute, int igngroupflags); + auto setMuteFunc = + reinterpret_cast(reaperPluginExtension->GetFunc("SetTrackUIMute")); + auto result = (*setMuteFunc)(masterTrack, 1, 0); + jassert(result == 1); + } +} + +bool HostSpecificExtensionsPlugin::isBusesLayoutSupported( + const juce::AudioProcessor::BusesLayout &layouts) const +{ + // only supports mono and stereo + if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::mono() && + layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo()) + return false; + + // input and output layout must be the same + if (layouts.getMainOutputChannelSet() != layouts.getMainInputChannelSet()) + return false; + + return true; +} + +void HostSpecificExtensionsPlugin::prepareToPlay(double, int) {} + +void HostSpecificExtensionsPlugin::processBlock(juce::AudioBuffer &, juce::MidiBuffer &) {} + +juce::AudioProcessorEditor *HostSpecificExtensionsPlugin::createEditor() +{ + return new PluginEditor(*this); +} + +juce::String HostSpecificExtensionsPlugin::getPluginTypeString() const +{ + if (wrapperType == juce::AudioProcessor::wrapperType_Undefined && is_clap) + return "CLAP"; + + return juce::AudioProcessor::getWrapperTypeDescription(wrapperType); +} + +void HostSpecificExtensionsPlugin::getStateInformation(juce::MemoryBlock &) {} + +void HostSpecificExtensionsPlugin::setStateInformation(const void *, int) {} + +// This creates new instances of the plugin +juce::AudioProcessor *JUCE_CALLTYPE createPluginFilter() +{ + return new HostSpecificExtensionsPlugin(); +} diff --git a/examples/HostSpecificExtensionsPlugin/HostSpecificExtensionsPlugin.h b/examples/HostSpecificExtensionsPlugin/HostSpecificExtensionsPlugin.h new file mode 100644 index 0000000..15704ba --- /dev/null +++ b/examples/HostSpecificExtensionsPlugin/HostSpecificExtensionsPlugin.h @@ -0,0 +1,55 @@ +#pragma once + +#include +JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE("-Wunused-parameter", "-Wextra-semi", "-Wnon-virtual-dtor") +#include +JUCE_END_IGNORE_WARNINGS_GCC_LIKE + +JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE("-Wc++98-compat-extra-semi", + "-Wgnu-anonymous-struct", + "-Wzero-as-null-pointer-constant", + "-Wextra-semi", + "-Wunused-parameter") +#include +JUCE_END_IGNORE_WARNINGS_GCC_LIKE + +class ModulatableFloatParameter; +class HostSpecificExtensionsPlugin : public juce::AudioProcessor, + public clap_juce_extensions::clap_juce_audio_processor_capabilities, + protected clap_juce_extensions::clap_properties +{ + public: + HostSpecificExtensionsPlugin(); + + const juce::String getName() const override { return JucePlugin_Name; } + bool acceptsMidi() const override { return false; } + bool producesMidi() const override { return false; } + bool isMidiEffect() const override { return false; } + + double getTailLengthSeconds() const override { return 0.0; } + + int getNumPrograms() override { return 1; } + int getCurrentProgram() override { return 0; } + void setCurrentProgram(int) override {} + const juce::String getProgramName(int) override { return juce::String(); } + void changeProgramName(int, const juce::String &) override {} + + bool isBusesLayoutSupported(const juce::AudioProcessor::BusesLayout &layouts) const override; + void prepareToPlay(double sampleRate, int samplesPerBlock) override; + void releaseResources() override {} + void processBlock(juce::AudioBuffer &, juce::MidiBuffer &) override; + void processBlock(juce::AudioBuffer &, juce::MidiBuffer &) override {} + + bool hasEditor() const override { return true; } + juce::AudioProcessorEditor *createEditor() override; + + void getStateInformation(juce::MemoryBlock &data) override; + void setStateInformation(const void *data, int sizeInBytes) override; + + juce::String getPluginTypeString() const; + + const reaper_plugin_info_t* reaperPluginExtension = nullptr; + + private: + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(HostSpecificExtensionsPlugin) +}; diff --git a/examples/HostSpecificExtensionsPlugin/PluginEditor.cpp b/examples/HostSpecificExtensionsPlugin/PluginEditor.cpp new file mode 100644 index 0000000..d6c97c2 --- /dev/null +++ b/examples/HostSpecificExtensionsPlugin/PluginEditor.cpp @@ -0,0 +1,54 @@ +#include "PluginEditor.h" + +PluginEditor::PluginEditor(HostSpecificExtensionsPlugin &plug) + : juce::AudioProcessorEditor(plug), plugin(plug) +{ + addAndMakeVisible(changeTrackColour); + changeTrackColour.setEnabled(plugin.reaperPluginExtension != nullptr); + changeTrackColour.onClick = [reaperExt = plugin.reaperPluginExtension] { + using GetTrackFunc = MediaTrack *(*)(ReaProject *, int); + auto getTrackFunc = reinterpret_cast(reaperExt->GetFunc("GetTrack")); + auto *track0 = getTrackFunc(nullptr, 0); + if (track0 == nullptr) + return; + + using ColorToNativeFunc = int (*)(int r, int g, int b); + auto colorToNativeFunc = + reinterpret_cast(reaperExt->GetFunc("ColorToNative")); + + using SetTrackColorFunc = void (*)(MediaTrack *track, int color); + auto setTrackColorFunc = + reinterpret_cast(reaperExt->GetFunc("SetTrackColor")); + + auto &rand = juce::Random::getSystemRandom(); + const auto red = rand.nextInt(256); + const auto green = rand.nextInt(256); + const auto blue = rand.nextInt(256); + setTrackColorFunc(track0, colorToNativeFunc(red, green, blue)); + }; + + setSize(300, 300); +} + +void PluginEditor::resized() +{ + changeTrackColour.setBounds(juce::Rectangle{100, 35}.withCentre(getLocalBounds().getCentre())); +} + +void PluginEditor::paint(juce::Graphics &g) +{ + g.fillAll(juce::Colours::grey); + + auto bounds = getLocalBounds(); + + g.setColour(juce::Colours::black); + g.setFont(25.0f); + const auto titleText = "Host-Specific Extensions Plugin " + plugin.getPluginTypeString(); + g.drawFittedText(titleText, bounds.removeFromTop(30), juce::Justification::centred, 1); + + g.setFont(18.0f); + const auto reaperExtText = + "REAPER plugin extension: " + + juce::String(plugin.reaperPluginExtension != nullptr ? "FOUND" : "NOT FOUND"); + g.drawFittedText(reaperExtText, bounds.removeFromTop(25), juce::Justification::centred, 1); +} diff --git a/examples/HostSpecificExtensionsPlugin/PluginEditor.h b/examples/HostSpecificExtensionsPlugin/PluginEditor.h new file mode 100644 index 0000000..1eadd90 --- /dev/null +++ b/examples/HostSpecificExtensionsPlugin/PluginEditor.h @@ -0,0 +1,20 @@ +#pragma once + +#include "HostSpecificExtensionsPlugin.h" + +class PluginEditor : public juce::AudioProcessorEditor +{ + public: + explicit PluginEditor(HostSpecificExtensionsPlugin &plugin); + ~PluginEditor() override = default; + + void resized() override; + void paint(juce::Graphics &g) override; + + private: + HostSpecificExtensionsPlugin &plugin; + + juce::TextButton changeTrackColour { "Change Track Colour" }; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PluginEditor) +}; diff --git a/include/clap-juce-extensions/clap-juce-extensions.h b/include/clap-juce-extensions/clap-juce-extensions.h index 2727964..39b4b01 100644 --- a/include/clap-juce-extensions/clap-juce-extensions.h +++ b/include/clap-juce-extensions/clap-juce-extensions.h @@ -18,6 +18,13 @@ class ClapJuceWrapper; struct JUCEParameterVariant; +namespace ClapAdapter +{ +const clap_plugin *clap_create_plugin(const struct clap_plugin_factory *, const clap_host *, + const char *); +} + + /** Forward declarations for any JUCE classes we might need. */ namespace juce { @@ -271,6 +278,15 @@ struct clap_juce_audio_processor_capabilities return nullptr; } + const void *getExtension(const char *name) + { + if (clapHostStatic != nullptr) + return clapHostStatic->get_extension(clapHostStatic, name); + if (extensionGet) + return extensionGet(name); + return nullptr; + } + private: friend class ::ClapJuceWrapper; std::function parameterChangeHandler = nullptr; @@ -278,6 +294,11 @@ struct clap_juce_audio_processor_capabilities std::function noteNamesChangedSignal = nullptr; std::function remoteControlsChangedSignal = nullptr; std::function suggestRemoteControlsPageSignal = nullptr; + std::function extensionGet = nullptr; + + friend const clap_plugin *ClapAdapter::clap_create_plugin(const struct clap_plugin_factory *, + const clap_host *, const char *); + static const clap_host *clapHostStatic; }; /* diff --git a/src/extensions/clap-juce-extensions.cpp b/src/extensions/clap-juce-extensions.cpp index 4268802..23306d0 100644 --- a/src/extensions/clap-juce-extensions.cpp +++ b/src/extensions/clap-juce-extensions.cpp @@ -12,4 +12,5 @@ uint32_t clap_properties::clap_version_major{0}, clap_properties::clap_version_m clap_properties::clap_properties() : is_clap{building_clap} {} +const clap_host* clap_juce_audio_processor_capabilities::clapHostStatic{nullptr}; } // namespace clap_juce_extensions \ No newline at end of file diff --git a/src/wrapper/clap-juce-wrapper.cpp b/src/wrapper/clap-juce-wrapper.cpp index 0f3d616..65c8102 100644 --- a/src/wrapper/clap-juce-wrapper.cpp +++ b/src/wrapper/clap-juce-wrapper.cpp @@ -434,6 +434,9 @@ class ClapJuceWrapper : public clap::helpers::Plugin< _host.remoteControlsSuggestPage(pageID); }); }; + processorAsClapExtensions->extensionGet = [this](const char *name) { + return _host.host()->get_extension(_host.host(), name); + }; } const bool forceLegacyParamIDs = false; @@ -2202,8 +2205,8 @@ static const clap_plugin_descriptor *clap_get_plugin_descriptor(const struct cla return &ClapJuceWrapper::desc; } -static const clap_plugin *clap_create_plugin(const struct clap_plugin_factory *, - const clap_host *host, const char *plugin_id) +const clap_plugin *clap_create_plugin(const struct clap_plugin_factory *, const clap_host *host, + const char *plugin_id) { juce::ScopedJuceInitialiser_GUI libraryInitialiser; @@ -2218,8 +2221,10 @@ static const clap_plugin *clap_create_plugin(const struct clap_plugin_factory *, clap_juce_extensions::clap_properties::clap_version_major = CLAP_VERSION_MAJOR; clap_juce_extensions::clap_properties::clap_version_minor = CLAP_VERSION_MINOR; clap_juce_extensions::clap_properties::clap_version_revision = CLAP_VERSION_REVISION; + clap_juce_extensions::clap_juce_audio_processor_capabilities::clapHostStatic = host; auto *const pluginInstance = ::createPluginFilter(); clap_juce_extensions::clap_properties::building_clap = false; + clap_juce_extensions::clap_juce_audio_processor_capabilities::clapHostStatic = nullptr; auto *wrapper = new ClapJuceWrapper(host, pluginInstance); return wrapper->clapPlugin(); }