From f36949c1b2c79fb22654b8765316f08f8d3eaa11 Mon Sep 17 00:00:00 2001 From: attila Date: Wed, 28 Jul 2021 09:27:58 +0200 Subject: [PATCH] ARA Host: Add support for scanning and hosting ARA plugins --- CMakeLists.txt | 16 +- docs/CMake API.md | 7 +- extras/AudioPluginHost/CMakeLists.txt | 2 + .../Source/Plugins/ARAPlugin.cpp | 26 + .../Source/Plugins/ARAPlugin.h | 1397 +++++++++++++++++ .../Source/Plugins/PluginGraph.cpp | 71 +- .../Source/Plugins/PluginGraph.h | 30 +- .../Source/UI/GraphEditorPanel.cpp | 21 +- .../Source/UI/GraphEditorPanel.h | 5 +- .../Source/UI/MainHostWindow.cpp | 76 +- .../Source/UI/MainHostWindow.h | 8 +- .../AudioPluginHost/Source/UI/PluginWindow.h | 13 + extras/Build/CMake/JUCEModuleSupport.cmake | 8 + extras/Build/CMake/JUCEUtils.cmake | 16 + .../format/juce_AudioPluginFormat.h | 12 + .../format/juce_AudioPluginFormatManager.cpp | 16 + .../format/juce_AudioPluginFormatManager.h | 16 + .../format_types/juce_ARACommon.cpp | 69 + .../format_types/juce_ARACommon.h | 78 + .../format_types/juce_ARAHosting.cpp | 451 ++++++ .../format_types/juce_ARAHosting.h | 733 +++++++++ .../format_types/juce_AudioUnitPluginFormat.h | 1 + .../juce_AudioUnitPluginFormat.mm | 359 ++++- .../format_types/juce_VST3PluginFormat.cpp | 152 +- .../format_types/juce_VST3PluginFormat.h | 1 + .../juce_audio_processors.cpp | 2 + .../juce_audio_processors.h | 13 + .../juce_audio_processors_ara.cpp | 33 + .../processors/juce_PluginDescription.cpp | 2 + .../processors/juce_PluginDescription.h | 3 + .../utilities/juce_ExtensionsVisitor.h | 10 + 31 files changed, 3518 insertions(+), 129 deletions(-) create mode 100644 extras/AudioPluginHost/Source/Plugins/ARAPlugin.cpp create mode 100644 extras/AudioPluginHost/Source/Plugins/ARAPlugin.h create mode 100644 modules/juce_audio_processors/format_types/juce_ARACommon.cpp create mode 100644 modules/juce_audio_processors/format_types/juce_ARACommon.h create mode 100644 modules/juce_audio_processors/format_types/juce_ARAHosting.cpp create mode 100644 modules/juce_audio_processors/format_types/juce_ARAHosting.h create mode 100644 modules/juce_audio_processors/juce_audio_processors_ara.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index be73802ffa..1f2201f82f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,10 +69,11 @@ juce_disable_default_flags() add_subdirectory(extras/Build) -# If you want to build the JUCE examples with VST2/AAX support, you'll need to make the VST2/AAX -# headers visible to the juce_audio_processors module. You can either set the paths on the command -# line, (e.g. -DJUCE_GLOBAL_AAX_SDK_PATH=/path/to/sdk) if you're just building the JUCE examples, or -# you can call the `juce_set_*_sdk_path` functions in your own CMakeLists after importing JUCE. +# If you want to build the JUCE examples with VST2/AAX/ARA support, you'll need to make the +# VST2/AAX/ARA headers visible to the juce_audio_processors module. You can either set the paths on +# the command line, (e.g. -DJUCE_GLOBAL_AAX_SDK_PATH=/path/to/sdk) if you're just building the JUCE +# examples, or you can call the `juce_set_*_sdk_path` functions in your own CMakeLists after +# importing JUCE. if(JUCE_GLOBAL_AAX_SDK_PATH) juce_set_aax_sdk_path("${JUCE_GLOBAL_AAX_SDK_PATH}") @@ -82,6 +83,13 @@ if(JUCE_GLOBAL_VST2_SDK_PATH) juce_set_vst2_sdk_path("${JUCE_GLOBAL_VST2_SDK_PATH}") endif() +# The ARA_SDK path should point to the "Umbrella installer" ARA_SDK directory. +# The directory can be obtained by recursively cloning https://github.com/Celemony/ARA_SDK and +# checking out the tag releases/2.1.0. +if(JUCE_GLOBAL_ARA_SDK_PATH) + juce_set_ara_sdk_path("${JUCE_GLOBAL_ARA_SDK_PATH}") +endif() + # We don't build anything other than the juceaide by default, because we want to keep configuration # speedy and the number of targets low. If you want to add targets for the extra projects and # example PIPs (there's a lot of them!), specify -DJUCE_BUILD_EXAMPLES=ON and/or diff --git a/docs/CMake API.md b/docs/CMake API.md index c1e90ae122..942d4b914c 100644 --- a/docs/CMake API.md +++ b/docs/CMake API.md @@ -664,10 +664,11 @@ target!). juce_set_aax_sdk_path() juce_set_vst2_sdk_path() juce_set_vst3_sdk_path() + juce_set_ara_sdk_path() -Call these functions from your CMakeLists to set up your local AAX, VST2, and VST3 SDKs. These -functions should be called *before* adding any targets that may depend on the AAX/VST2/VST3 SDKs -(plugin hosts, AAX/VST2/VST3 plugins etc.). +Call these functions from your CMakeLists to set up your local AAX, VST2, VST3 and ARA SDKs. These +functions should be called *before* adding any targets that may depend on the AAX/VST2/VST3/ARA SDKs +(plugin hosts, AAX/VST2/VST3/ARA plugins etc.). #### `juce_add_module` diff --git a/extras/AudioPluginHost/CMakeLists.txt b/extras/AudioPluginHost/CMakeLists.txt index 6e531837b2..ff85e45765 100644 --- a/extras/AudioPluginHost/CMakeLists.txt +++ b/extras/AudioPluginHost/CMakeLists.txt @@ -24,6 +24,7 @@ juce_generate_juce_header(AudioPluginHost) target_sources(AudioPluginHost PRIVATE Source/HostStartup.cpp + Source/Plugins/ARAPlugin.cpp Source/Plugins/IOConfigurationWindow.cpp Source/Plugins/InternalPlugins.cpp Source/Plugins/PluginGraph.cpp @@ -46,6 +47,7 @@ target_compile_definitions(AudioPluginHost PRIVATE JUCE_PLUGINHOST_LV2=1 JUCE_PLUGINHOST_VST3=1 JUCE_PLUGINHOST_VST=0 + JUCE_PLUGINHOST_ARA=0 JUCE_USE_CAMERA=0 JUCE_USE_CDBURNER=0 JUCE_USE_CDREADER=0 diff --git a/extras/AudioPluginHost/Source/Plugins/ARAPlugin.cpp b/extras/AudioPluginHost/Source/Plugins/ARAPlugin.cpp new file mode 100644 index 0000000000..b805db1d53 --- /dev/null +++ b/extras/AudioPluginHost/Source/Plugins/ARAPlugin.cpp @@ -0,0 +1,26 @@ +/* + ============================================================================== + + This file is part of the JUCE 7 technical preview. + Copyright (c) 2022 - Raw Material Software Limited + + You may use this code under the terms of the GPL v3 + (see www.gnu.org/licenses). + + For the technical preview this file cannot be licensed commercially. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include "ARAPlugin.h" + +#if JUCE_PLUGINHOST_ARA && (JUCE_MAC || JUCE_WINDOWS) + +const Identifier ARAPluginInstanceWrapper::ARATestHost::Context::xmlRootTag { "ARATestHostContext" }; +const Identifier ARAPluginInstanceWrapper::ARATestHost::Context::xmlAudioFileAttrib { "AudioFile" }; + +#endif diff --git a/extras/AudioPluginHost/Source/Plugins/ARAPlugin.h b/extras/AudioPluginHost/Source/Plugins/ARAPlugin.h new file mode 100644 index 0000000000..29882ba57a --- /dev/null +++ b/extras/AudioPluginHost/Source/Plugins/ARAPlugin.h @@ -0,0 +1,1397 @@ +/* + ============================================================================== + + This file is part of the JUCE 7 technical preview. + Copyright (c) 2022 - Raw Material Software Limited + + You may use this code under the terms of the GPL v3 + (see www.gnu.org/licenses). + + For the technical preview this file cannot be licensed commercially. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#pragma once + +#include + +#if JUCE_PLUGINHOST_ARA && (JUCE_MAC || JUCE_WINDOWS) + +#include + +#include +#include + +class FileAudioSource +{ + auto getAudioSourceProperties() const + { + auto properties = ARAHostModel::AudioSource::getEmptyProperties(); + properties.name = formatReader->getFile().getFullPathName().toRawUTF8(); + properties.persistentID = formatReader->getFile().getFullPathName().toRawUTF8(); + properties.sampleCount = formatReader->lengthInSamples; + properties.sampleRate = formatReader->sampleRate; + properties.channelCount = (int) formatReader->numChannels; + properties.merits64BitSamples = false; + return properties; + } + +public: + FileAudioSource (ARA::Host::DocumentController& dc, const juce::File& file) + : formatReader ([&file] + { + auto result = rawToUniquePtr (WavAudioFormat().createMemoryMappedReader (file)); + result->mapEntireFile(); + return result; + }()), + audioSource (Converter::toHostRef (this), dc, getAudioSourceProperties()) + { + audioSource.enableAudioSourceSamplesAccess (true); + } + + bool readAudioSamples (float* const* buffers, int64 startSample, int64 numSamples) + { + // TODO: the ARA interface defines numSamples as int64. We should do multiple reads if necessary with the reader. + if (numSamples > std::numeric_limits::max()) + return false; + + return formatReader->read (buffers, (int) formatReader->numChannels, startSample, (int) (numSamples)); + } + + bool readAudioSamples (double* const* buffers, int64 startSample, int64 numSamples) + { + ignoreUnused (buffers, startSample, numSamples); + return false; + } + + MemoryMappedAudioFormatReader& getFormatReader() const { return *formatReader; } + + auto getPluginRef() const { return audioSource.getPluginRef(); } + + auto& getSource() { return audioSource; } + + using Converter = ARAHostModel::ConversionFunctions; + +private: + std::unique_ptr formatReader; + ARAHostModel::AudioSource audioSource; +}; + +//============================================================================== +class MusicalContext +{ + auto getMusicalContextProperties() const + { + auto properties = ARAHostModel::MusicalContext::getEmptyProperties(); + properties.name = "MusicalContext"; + properties.orderIndex = 0; + properties.color = nullptr; + return properties; + } + +public: + MusicalContext (ARA::Host::DocumentController& dc) + : context (Converter::toHostRef (this), dc, getMusicalContextProperties()) + { + } + + auto getPluginRef() const { return context.getPluginRef(); } + +private: + using Converter = ARAHostModel::ConversionFunctions; + + ARAHostModel::MusicalContext context; +}; + +//============================================================================== +class RegionSequence +{ + auto getRegionSequenceProperties() const + { + auto properties = ARAHostModel::RegionSequence::getEmptyProperties(); + properties.name = name.toRawUTF8(); + properties.orderIndex = 0; + properties.musicalContextRef = context.getPluginRef(); + properties.color = nullptr; + return properties; + } + +public: + RegionSequence (ARA::Host::DocumentController& dc, MusicalContext& contextIn, String nameIn) + : context (contextIn), + name (std::move (nameIn)), + sequence (Converter::toHostRef (this), dc, getRegionSequenceProperties()) + { + } + + auto& getMusicalContext() const { return context; } + auto getPluginRef() const { return sequence.getPluginRef(); } + +private: + using Converter = ARAHostModel::ConversionFunctions; + + MusicalContext& context; + String name; + ARAHostModel::RegionSequence sequence; +}; + +class AudioModification +{ + auto getProperties() const + { + auto properties = ARAHostModel::AudioModification::getEmptyProperties(); + properties.persistentID = "x"; + return properties; + } + +public: + AudioModification (ARA::Host::DocumentController& dc, FileAudioSource& source) + : modification (Converter::toHostRef (this), dc, source.getSource(), getProperties()) + { + } + + auto& getModification() { return modification; } + +private: + using Converter = ARAHostModel::ConversionFunctions; + + ARAHostModel::AudioModification modification; +}; + +//============================================================================== +class PlaybackRegion +{ + auto getPlaybackRegionProperties() const + { + auto properties = ARAHostModel::PlaybackRegion::getEmptyProperties(); + properties.transformationFlags = ARA::kARAPlaybackTransformationNoChanges; + properties.startInModificationTime = 0.0; + const auto& formatReader = audioSource.getFormatReader(); + properties.durationInModificationTime = formatReader.lengthInSamples / formatReader.sampleRate; + properties.startInPlaybackTime = 0.0; + properties.durationInPlaybackTime = properties.durationInModificationTime; + properties.musicalContextRef = sequence.getMusicalContext().getPluginRef(); + properties.regionSequenceRef = sequence.getPluginRef(); + + properties.name = nullptr; + properties.color = nullptr; + return properties; + } + +public: + PlaybackRegion (ARA::Host::DocumentController& dc, + RegionSequence& s, + AudioModification& m, + FileAudioSource& source) + : sequence (s), + audioSource (source), + region (Converter::toHostRef (this), dc, m.getModification(), getPlaybackRegionProperties()) + { + jassert (source.getPluginRef() == m.getModification().getAudioSource().getPluginRef()); + } + + auto& getPlaybackRegion() { return region; } + +private: + using Converter = ARAHostModel::ConversionFunctions; + + RegionSequence& sequence; + FileAudioSource& audioSource; + ARAHostModel::PlaybackRegion region; +}; + +//============================================================================== +class AudioAccessController : public ARA::Host::AudioAccessControllerInterface +{ +public: + ARA::ARAAudioReaderHostRef createAudioReaderForSource (ARA::ARAAudioSourceHostRef audioSourceHostRef, + bool use64BitSamples) noexcept override + { + auto audioReader = std::make_unique (audioSourceHostRef, use64BitSamples); + auto audioReaderHostRef = Converter::toHostRef (audioReader.get()); + auto* readerPtr = audioReader.get(); + audioReaders.emplace (readerPtr, std::move (audioReader)); + return audioReaderHostRef; + } + + bool readAudioSamples (ARA::ARAAudioReaderHostRef readerRef, + ARA::ARASamplePosition samplePosition, + ARA::ARASampleCount samplesPerChannel, + void* const* buffers) noexcept override + { + const auto use64BitSamples = Converter::fromHostRef (readerRef)->use64Bit; + auto* audioSource = FileAudioSource::Converter::fromHostRef (Converter::fromHostRef (readerRef)->sourceHostRef); + + if (use64BitSamples) + return audioSource->readAudioSamples ( + reinterpret_cast (buffers), samplePosition, samplesPerChannel); + + return audioSource->readAudioSamples ( + reinterpret_cast (buffers), samplePosition, samplesPerChannel); + } + + void destroyAudioReader (ARA::ARAAudioReaderHostRef readerRef) noexcept override + { + audioReaders.erase (Converter::fromHostRef (readerRef)); + } + +private: + struct AudioReader + { + AudioReader (ARA::ARAAudioSourceHostRef source, bool use64BitSamples) + : sourceHostRef (source), use64Bit (use64BitSamples) + { + } + + ARA::ARAAudioSourceHostRef sourceHostRef; + bool use64Bit; + }; + + using Converter = ARAHostModel::ConversionFunctions; + + std::map> audioReaders; +}; + +class ArchivingController : public ARA::Host::ArchivingControllerInterface +{ +public: + using ReaderConverter = ARAHostModel::ConversionFunctions; + using WriterConverter = ARAHostModel::ConversionFunctions; + + ARA::ARASize getArchiveSize (ARA::ARAArchiveReaderHostRef archiveReaderHostRef) noexcept override + { + return (ARA::ARASize) ReaderConverter::fromHostRef (archiveReaderHostRef)->getSize(); + } + + bool readBytesFromArchive (ARA::ARAArchiveReaderHostRef archiveReaderHostRef, + ARA::ARASize position, + ARA::ARASize length, + ARA::ARAByte* buffer) noexcept override + { + auto* archiveReader = ReaderConverter::fromHostRef (archiveReaderHostRef); + + if ((position + length) <= archiveReader->getSize()) + { + std::memcpy (buffer, addBytesToPointer (archiveReader->getData(), position), length); + return true; + } + + return false; + } + + bool writeBytesToArchive (ARA::ARAArchiveWriterHostRef archiveWriterHostRef, + ARA::ARASize position, + ARA::ARASize length, + const ARA::ARAByte* buffer) noexcept override + { + auto* archiveWriter = WriterConverter::fromHostRef (archiveWriterHostRef); + + if (archiveWriter->setPosition ((int64) position) && archiveWriter->write (buffer, length)) + return true; + + return false; + } + + void notifyDocumentArchivingProgress (float value) noexcept override { ignoreUnused (value); } + + void notifyDocumentUnarchivingProgress (float value) noexcept override { ignoreUnused (value); } + + ARA::ARAPersistentID getDocumentArchiveID (ARA::ARAArchiveReaderHostRef archiveReaderHostRef) noexcept override + { + ignoreUnused (archiveReaderHostRef); + + return nullptr; + } +}; + +class ContentAccessController : public ARA::Host::ContentAccessControllerInterface +{ +public: + using Converter = ARAHostModel::ConversionFunctions; + + bool isMusicalContextContentAvailable (ARA::ARAMusicalContextHostRef musicalContextHostRef, + ARA::ARAContentType type) noexcept override + { + ignoreUnused (musicalContextHostRef); + + return (type == ARA::kARAContentTypeTempoEntries || type == ARA::kARAContentTypeBarSignatures); + } + + ARA::ARAContentGrade getMusicalContextContentGrade (ARA::ARAMusicalContextHostRef musicalContextHostRef, + ARA::ARAContentType type) noexcept override + { + ignoreUnused (musicalContextHostRef, type); + + return ARA::kARAContentGradeInitial; + } + + ARA::ARAContentReaderHostRef + createMusicalContextContentReader (ARA::ARAMusicalContextHostRef musicalContextHostRef, + ARA::ARAContentType type, + const ARA::ARAContentTimeRange* range) noexcept override + { + ignoreUnused (musicalContextHostRef, range); + + return Converter::toHostRef (type); + } + + bool isAudioSourceContentAvailable (ARA::ARAAudioSourceHostRef audioSourceHostRef, + ARA::ARAContentType type) noexcept override + { + ignoreUnused (audioSourceHostRef, type); + + return false; + } + + ARA::ARAContentGrade getAudioSourceContentGrade (ARA::ARAAudioSourceHostRef audioSourceHostRef, + ARA::ARAContentType type) noexcept override + { + ignoreUnused (audioSourceHostRef, type); + + return 0; + } + + ARA::ARAContentReaderHostRef + createAudioSourceContentReader (ARA::ARAAudioSourceHostRef audioSourceHostRef, + ARA::ARAContentType type, + const ARA::ARAContentTimeRange* range) noexcept override + { + ignoreUnused (audioSourceHostRef, type, range); + + return nullptr; + } + + ARA::ARAInt32 getContentReaderEventCount (ARA::ARAContentReaderHostRef contentReaderHostRef) noexcept override + { + const auto contentType = Converter::fromHostRef (contentReaderHostRef); + + if (contentType == ARA::kARAContentTypeTempoEntries || contentType == ARA::kARAContentTypeBarSignatures) + return 2; + + return 0; + } + + const void* getContentReaderDataForEvent (ARA::ARAContentReaderHostRef contentReaderHostRef, + ARA::ARAInt32 eventIndex) noexcept override + { + if (Converter::fromHostRef (contentReaderHostRef) == ARA::kARAContentTypeTempoEntries) + { + if (eventIndex == 0) + { + tempoEntry.timePosition = 0.0; + tempoEntry.quarterPosition = 0.0; + } + else if (eventIndex == 1) + { + tempoEntry.timePosition = 2.0; + tempoEntry.quarterPosition = 4.0; + } + + return &tempoEntry; + } + else if (Converter::fromHostRef (contentReaderHostRef) == ARA::kARAContentTypeBarSignatures) + { + if (eventIndex == 0) + { + barSignature.position = 0.0; + barSignature.numerator = 4; + barSignature.denominator = 4; + } + + if (eventIndex == 1) + { + barSignature.position = 1.0; + barSignature.numerator = 4; + barSignature.denominator = 4; + } + + return &barSignature; + } + + jassertfalse; + return nullptr; + } + + void destroyContentReader (ARA::ARAContentReaderHostRef contentReaderHostRef) noexcept override + { + ignoreUnused (contentReaderHostRef); + } + + ARA::ARAContentTempoEntry tempoEntry; + ARA::ARAContentBarSignature barSignature; +}; + +class ModelUpdateController : public ARA::Host::ModelUpdateControllerInterface +{ +public: + void notifyAudioSourceAnalysisProgress (ARA::ARAAudioSourceHostRef audioSourceHostRef, + ARA::ARAAnalysisProgressState state, + float value) noexcept override + { + ignoreUnused (audioSourceHostRef, state, value); + } + + void notifyAudioSourceContentChanged (ARA::ARAAudioSourceHostRef audioSourceHostRef, + const ARA::ARAContentTimeRange* range, + ARA::ContentUpdateScopes scopeFlags) noexcept override + { + ignoreUnused (audioSourceHostRef, range, scopeFlags); + } + + void notifyAudioModificationContentChanged (ARA::ARAAudioModificationHostRef audioModificationHostRef, + const ARA::ARAContentTimeRange* range, + ARA::ContentUpdateScopes scopeFlags) noexcept override + { + ignoreUnused (audioModificationHostRef, range, scopeFlags); + } + + void notifyPlaybackRegionContentChanged (ARA::ARAPlaybackRegionHostRef playbackRegionHostRef, + const ARA::ARAContentTimeRange* range, + ARA::ContentUpdateScopes scopeFlags) noexcept override + { + ignoreUnused (playbackRegionHostRef, range, scopeFlags); + } +}; + +class PlaybackController : public ARA::Host::PlaybackControllerInterface +{ +public: + void requestStartPlayback() noexcept override {} + void requestStopPlayback() noexcept override {} + + void requestSetPlaybackPosition (ARA::ARATimePosition timePosition) noexcept override + { + ignoreUnused (timePosition); + } + + void requestSetCycleRange (ARA::ARATimePosition startTime, ARA::ARATimeDuration duration) noexcept override + { + ignoreUnused (startTime, duration); + } + + void requestEnableCycle (bool enable) noexcept override { ignoreUnused (enable); } +}; + +struct SimplePlayHead : public juce::AudioPlayHead +{ + bool getCurrentPosition (CurrentPositionInfo& result) override + { + result.timeInSamples = timeInSamples.load(); + result.isPlaying = isPlaying.load(); + return true; + } + + std::atomic timeInSamples { 0 }; + std::atomic isPlaying { false }; +}; + +struct HostPlaybackController +{ + virtual ~HostPlaybackController() = default; + + virtual void setPlaying (bool isPlaying) = 0; + virtual void goToStart() = 0; + virtual File getAudioSource() const = 0; + virtual void setAudioSource (File audioSourceFile) = 0; + virtual void clearAudioSource() = 0; +}; + +class AudioSourceComponent : public Component, + public FileDragAndDropTarget, + public ChangeListener +{ +public: + explicit AudioSourceComponent (HostPlaybackController& controller, juce::ChangeBroadcaster& bc) + : hostPlaybackController (controller), + broadcaster (bc), + waveformComponent (*this) + { + audioSourceLabel.setText ("You can drag and drop .wav files here", NotificationType::dontSendNotification); + + addAndMakeVisible (audioSourceLabel); + addAndMakeVisible (waveformComponent); + + playButton.setButtonText ("Play / Pause"); + playButton.onClick = [this] + { + isPlaying = ! isPlaying; + hostPlaybackController.setPlaying (isPlaying); + }; + + goToStartButton.setButtonText ("Go to start"); + goToStartButton.onClick = [this] { hostPlaybackController.goToStart(); }; + + addAndMakeVisible (goToStartButton); + addAndMakeVisible (playButton); + + broadcaster.addChangeListener (this); + + update(); + } + + ~AudioSourceComponent() override + { + broadcaster.removeChangeListener (this); + } + + void changeListenerCallback (ChangeBroadcaster*) override + { + update(); + } + + void resized() override + { + auto localBounds = getLocalBounds(); + auto buttonsArea = localBounds.removeFromBottom (40).reduced (5); + auto waveformArea = localBounds.removeFromBottom (150).reduced (5); + + juce::FlexBox fb; + fb.justifyContent = juce::FlexBox::JustifyContent::center; + fb.alignContent = juce::FlexBox::AlignContent::center; + + fb.items = { juce::FlexItem (goToStartButton).withMinWidth (100.0f).withMinHeight ((float) buttonsArea.getHeight()), + juce::FlexItem (playButton).withMinWidth (100.0f).withMinHeight ((float) buttonsArea.getHeight()) }; + + fb.performLayout (buttonsArea); + + waveformComponent.setBounds (waveformArea); + + audioSourceLabel.setBounds (localBounds); + } + + bool isInterestedInFileDrag (const StringArray& files) override + { + if (files.size() != 1) + return false; + + if (files.getReference (0).endsWithIgnoreCase (".wav")) + return true; + + return false; + } + + void update() + { + const auto currentAudioSource = hostPlaybackController.getAudioSource(); + + if (currentAudioSource.existsAsFile()) + { + waveformComponent.setSource (currentAudioSource); + audioSourceLabel.setText (currentAudioSource.getFullPathName(), + NotificationType::dontSendNotification); + } + else + { + waveformComponent.clearSource(); + audioSourceLabel.setText ("You can drag and drop .wav files here", NotificationType::dontSendNotification); + } + } + + void filesDropped (const StringArray& files, int, int) override + { + hostPlaybackController.setAudioSource (files.getReference (0)); + update(); + } + +private: + class WaveformComponent : public Component, + public ChangeListener + { + public: + WaveformComponent (AudioSourceComponent& p) + : parent (p), + thumbCache (7), + audioThumb (128, formatManager, thumbCache) + { + setWantsKeyboardFocus (true); + formatManager.registerBasicFormats(); + audioThumb.addChangeListener (this); + } + + ~WaveformComponent() override + { + audioThumb.removeChangeListener (this); + } + + void mouseDown (const MouseEvent&) override + { + isSelected = true; + repaint(); + } + + void changeListenerCallback (ChangeBroadcaster*) override + { + repaint(); + } + + void paint (juce::Graphics& g) override + { + if (! isEmpty) + { + auto rect = getLocalBounds(); + + const auto waveformColour = Colours::cadetblue; + + if (rect.getWidth() > 2) + { + g.setColour (isSelected ? juce::Colours::yellow : juce::Colours::black); + g.drawRect (rect); + rect.reduce (1, 1); + g.setColour (waveformColour.darker (1.0f)); + g.fillRect (rect); + } + + g.setColour (Colours::cadetblue); + audioThumb.drawChannels (g, rect, 0.0, audioThumb.getTotalLength(), 1.0f); + } + } + + void setSource (const File& source) + { + isEmpty = false; + audioThumb.setSource (new FileInputSource (source)); + } + + void clearSource() + { + isEmpty = true; + isSelected = false; + audioThumb.clear(); + } + + bool keyPressed (const KeyPress& key) override + { + if (isSelected && key == KeyPress::deleteKey) + { + parent.hostPlaybackController.clearAudioSource(); + return true; + } + + return false; + } + + private: + AudioSourceComponent& parent; + + bool isEmpty = true; + bool isSelected = false; + AudioFormatManager formatManager; + AudioThumbnailCache thumbCache; + AudioThumbnail audioThumb; + }; + + HostPlaybackController& hostPlaybackController; + juce::ChangeBroadcaster& broadcaster; + Label audioSourceLabel; + WaveformComponent waveformComponent; + bool isPlaying { false }; + TextButton playButton, goToStartButton; +}; + +class ARAPluginInstanceWrapper : public AudioPluginInstance +{ +public: + class ARATestHost : public HostPlaybackController, + public juce::ChangeBroadcaster + { + public: + class Editor : public AudioProcessorEditor + { + public: + explicit Editor (ARATestHost& araTestHost) + : AudioProcessorEditor (araTestHost.getAudioPluginInstance()), + audioSourceComponent (araTestHost, araTestHost) + { + audioSourceComponent.update(); + addAndMakeVisible (audioSourceComponent); + setSize (512, 220); + } + + ~Editor() override { getAudioProcessor()->editorBeingDeleted (this); } + + void resized() override { audioSourceComponent.setBounds (getLocalBounds()); } + + private: + AudioSourceComponent audioSourceComponent; + }; + + explicit ARATestHost (ARAPluginInstanceWrapper& instanceIn) + : instance (instanceIn) + { + if (instance.inner->getPluginDescription().hasARAExtension) + { + instance.inner->setPlayHead (&playHead); + + createARAFactoryAsync (*instance.inner, [this] (ARAFactoryWrapper araFactory) + { + init (std::move (araFactory)); + }); + } + } + + void init (ARAFactoryWrapper araFactory) + { + if (araFactory.get() != nullptr) + { + documentController = ARAHostDocumentController::create (std::move (araFactory), + "AudioPluginHostDocument", + std::make_unique(), + std::make_unique(), + std::make_unique(), + std::make_unique(), + std::make_unique()); + + if (documentController != nullptr) + { + const auto allRoles = ARA::kARAPlaybackRendererRole | ARA::kARAEditorRendererRole | ARA::kARAEditorViewRole; + const auto plugInExtensionInstance = documentController->bindDocumentToPluginInstance (*instance.inner, + allRoles, + allRoles); + playbackRenderer = plugInExtensionInstance.getPlaybackRendererInterface(); + editorRenderer = plugInExtensionInstance.getEditorRendererInterface(); + synchronizeStateWithDocumentController(); + } + else + jassertfalse; + } + else + jassertfalse; + } + + void getStateInformation (juce::MemoryBlock& b) + { + std::lock_guard configurationLock (instance.innerMutex); + + if (context != nullptr) + context->getStateInformation (b); + } + + void setStateInformation (const void* d, int s) + { + { + std::lock_guard lock { contextUpdateSourceMutex }; + contextUpdateSource = ContextUpdateSource { d, s }; + } + + synchronise(); + } + + ~ARATestHost() override { instance.inner->releaseResources(); } + + void afterProcessBlock (int numSamples) + { + const auto isPlayingNow = isPlaying.load(); + playHead.isPlaying.store (isPlayingNow); + + if (isPlayingNow) + { + const auto currentAudioSourceLength = audioSourceLength.load(); + const auto currentPlayHeadPosition = playHead.timeInSamples.load(); + + // Rudimentary attempt to not seek beyond our sample data, assuming a fairly stable numSamples + // value. We should gain control over calling the AudioProcessorGraph's processBlock() calls so + // that we can do sample precise looping. + if (currentAudioSourceLength - currentPlayHeadPosition < numSamples) + playHead.timeInSamples.store (0); + else + playHead.timeInSamples.fetch_add (numSamples); + } + + if (goToStartSignal.exchange (false)) + playHead.timeInSamples.store (0); + } + + File getAudioSource() const override + { + std::lock_guard lock { instance.innerMutex }; + + if (context != nullptr) + return context->audioFile; + + return {}; + } + + void setAudioSource (File audioSourceFile) override + { + if (audioSourceFile.existsAsFile()) + { + { + std::lock_guard lock { contextUpdateSourceMutex }; + contextUpdateSource = ContextUpdateSource (std::move (audioSourceFile)); + } + + synchronise(); + } + } + + void clearAudioSource() override + { + { + std::lock_guard lock { contextUpdateSourceMutex }; + contextUpdateSource = ContextUpdateSource (ContextUpdateSource::Type::reset); + } + + synchronise(); + } + + void setPlaying (bool isPlayingIn) override { isPlaying.store (isPlayingIn); } + + void goToStart() override { goToStartSignal.store (true); } + + Editor* createEditor() { return new Editor (*this); } + + AudioPluginInstance& getAudioPluginInstance() { return instance; } + + private: + /** Use this to put the plugin in an unprepared state for the duration of adding and removing PlaybackRegions + to and from Renderers. + */ + class ScopedPluginDeactivator + { + public: + explicit ScopedPluginDeactivator (ARAPluginInstanceWrapper& inst) : instance (inst) + { + if (instance.prepareToPlayParams.isValid) + instance.inner->releaseResources(); + } + + ~ScopedPluginDeactivator() + { + if (instance.prepareToPlayParams.isValid) + instance.inner->prepareToPlay (instance.prepareToPlayParams.sampleRate, + instance.prepareToPlayParams.samplesPerBlock); + } + + private: + ARAPluginInstanceWrapper& instance; + + JUCE_DECLARE_NON_COPYABLE (ScopedPluginDeactivator) + }; + + class ContextUpdateSource + { + public: + enum class Type + { + empty, + audioSourceFile, + stateInformation, + reset + }; + + ContextUpdateSource() = default; + + explicit ContextUpdateSource (const File& file) + : type (Type::audioSourceFile), + audioSourceFile (file) + { + } + + ContextUpdateSource (const void* d, int s) + : type (Type::stateInformation), + stateInformation (d, (size_t) s) + { + } + + ContextUpdateSource (Type t) : type (t) + { + jassert (t == Type::reset); + } + + Type getType() const { return type; } + + const File& getAudioSourceFile() const + { + jassert (type == Type::audioSourceFile); + + return audioSourceFile; + } + + const MemoryBlock& getStateInformation() const + { + jassert (type == Type::stateInformation); + + return stateInformation; + } + + private: + Type type = Type::empty; + + File audioSourceFile; + MemoryBlock stateInformation; + }; + + void synchronise() + { + const SpinLock::ScopedLockType scope (instance.innerProcessBlockFlag); + std::lock_guard configurationLock (instance.innerMutex); + synchronizeStateWithDocumentController(); + } + + void synchronizeStateWithDocumentController() + { + bool resetContext = false; + + auto newContext = [&]() -> std::unique_ptr + { + std::lock_guard lock { contextUpdateSourceMutex }; + + switch (contextUpdateSource.getType()) + { + case ContextUpdateSource::Type::empty: + return {}; + + case ContextUpdateSource::Type::audioSourceFile: + if (! (contextUpdateSource.getAudioSourceFile().existsAsFile())) + return {}; + + { + const ARAEditGuard editGuard (documentController->getDocumentController()); + return std::make_unique (documentController->getDocumentController(), + contextUpdateSource.getAudioSourceFile()); + } + + case ContextUpdateSource::Type::stateInformation: + jassert (contextUpdateSource.getStateInformation().getSize() <= std::numeric_limits::max()); + + return Context::createFromStateInformation (documentController->getDocumentController(), + contextUpdateSource.getStateInformation().getData(), + (int) contextUpdateSource.getStateInformation().getSize()); + + case ContextUpdateSource::Type::reset: + resetContext = true; + return {}; + } + + jassertfalse; + return {}; + }(); + + if (newContext != nullptr) + { + { + ScopedPluginDeactivator deactivator (instance); + + context = std::move (newContext); + audioSourceLength.store (context->fileAudioSource.getFormatReader().lengthInSamples); + + auto& region = context->playbackRegion.getPlaybackRegion(); + playbackRenderer.add (region); + editorRenderer.add (region); + } + + sendChangeMessage(); + } + + if (resetContext) + { + { + ScopedPluginDeactivator deactivator (instance); + + context.reset(); + audioSourceLength.store (0); + } + + sendChangeMessage(); + } + } + + struct Context + { + Context (ARA::Host::DocumentController& dc, const File& audioFileIn) + : audioFile (audioFileIn), + musicalContext (dc), + regionSequence (dc, musicalContext, "track 1"), + fileAudioSource (dc, audioFile), + audioModification (dc, fileAudioSource), + playbackRegion (dc, regionSequence, audioModification, fileAudioSource) + { + } + + static std::unique_ptr createFromStateInformation (ARA::Host::DocumentController& dc, const void* d, int s) + { + if (auto xml = getXmlFromBinary (d, s)) + { + if (xml->hasTagName (xmlRootTag)) + { + File file { xml->getStringAttribute (xmlAudioFileAttrib) }; + + if (file.existsAsFile()) + return std::make_unique (dc, std::move (file)); + } + } + + return {}; + } + + void getStateInformation (juce::MemoryBlock& b) + { + XmlElement root { xmlRootTag }; + root.setAttribute (xmlAudioFileAttrib, audioFile.getFullPathName()); + copyXmlToBinary (root, b); + } + + const static Identifier xmlRootTag; + const static Identifier xmlAudioFileAttrib; + + File audioFile; + + MusicalContext musicalContext; + RegionSequence regionSequence; + FileAudioSource fileAudioSource; + AudioModification audioModification; + PlaybackRegion playbackRegion; + }; + + SimplePlayHead playHead; + ARAPluginInstanceWrapper& instance; + + std::unique_ptr documentController; + ARAHostModel::PlaybackRendererInterface playbackRenderer; + ARAHostModel::EditorRendererInterface editorRenderer; + + std::unique_ptr context; + + mutable std::mutex contextUpdateSourceMutex; + ContextUpdateSource contextUpdateSource; + + std::atomic isPlaying { false }; + std::atomic goToStartSignal { false }; + std::atomic audioSourceLength { 0 }; + }; + + explicit ARAPluginInstanceWrapper (std::unique_ptr innerIn) + : inner (std::move (innerIn)), araHost (*this) + { + jassert (inner != nullptr); + + for (auto isInput : { true, false }) + matchBuses (isInput); + + setBusesLayout (inner->getBusesLayout()); + } + + //============================================================================== + AudioProcessorEditor* createARAHostEditor() { return araHost.createEditor(); } + + //============================================================================== + const String getName() const override + { + std::lock_guard lock (innerMutex); + return inner->getName(); + } + + StringArray getAlternateDisplayNames() const override + { + std::lock_guard lock (innerMutex); + return inner->getAlternateDisplayNames(); + } + + double getTailLengthSeconds() const override + { + std::lock_guard lock (innerMutex); + return inner->getTailLengthSeconds(); + } + + bool acceptsMidi() const override + { + std::lock_guard lock (innerMutex); + return inner->acceptsMidi(); + } + + bool producesMidi() const override + { + std::lock_guard lock (innerMutex); + return inner->producesMidi(); + } + + AudioProcessorEditor* createEditor() override + { + std::lock_guard lock (innerMutex); + return inner->createEditorIfNeeded(); + } + + bool hasEditor() const override + { + std::lock_guard lock (innerMutex); + return inner->hasEditor(); + } + + int getNumPrograms() override + { + std::lock_guard lock (innerMutex); + return inner->getNumPrograms(); + } + + int getCurrentProgram() override + { + std::lock_guard lock (innerMutex); + return inner->getCurrentProgram(); + } + + void setCurrentProgram (int i) override + { + std::lock_guard lock (innerMutex); + inner->setCurrentProgram (i); + } + + const String getProgramName (int i) override + { + std::lock_guard lock (innerMutex); + return inner->getProgramName (i); + } + + void changeProgramName (int i, const String& n) override + { + std::lock_guard lock (innerMutex); + inner->changeProgramName (i, n); + } + + void getStateInformation (juce::MemoryBlock& b) override + { + XmlElement state ("ARAPluginInstanceWrapperState"); + + { + MemoryBlock m; + araHost.getStateInformation (m); + state.createNewChildElement ("host")->addTextElement (m.toBase64Encoding()); + } + + { + std::lock_guard lock (innerMutex); + + MemoryBlock m; + inner->getStateInformation (m); + state.createNewChildElement ("plugin")->addTextElement (m.toBase64Encoding()); + } + + copyXmlToBinary (state, b); + } + + void setStateInformation (const void* d, int s) override + { + if (auto xml = getXmlFromBinary (d, s)) + { + if (xml->hasTagName ("ARAPluginInstanceWrapperState")) + { + if (auto* hostState = xml->getChildByName ("host")) + { + MemoryBlock m; + m.fromBase64Encoding (hostState->getAllSubText()); + jassert (m.getSize() <= std::numeric_limits::max()); + araHost.setStateInformation (m.getData(), (int) m.getSize()); + } + + if (auto* pluginState = xml->getChildByName ("plugin")) + { + std::lock_guard lock (innerMutex); + + MemoryBlock m; + m.fromBase64Encoding (pluginState->getAllSubText()); + jassert (m.getSize() <= std::numeric_limits::max()); + inner->setStateInformation (m.getData(), (int) m.getSize()); + } + } + } + } + + void getCurrentProgramStateInformation (juce::MemoryBlock& b) override + { + std::lock_guard lock (innerMutex); + inner->getCurrentProgramStateInformation (b); + } + + void setCurrentProgramStateInformation (const void* d, int s) override + { + std::lock_guard lock (innerMutex); + inner->setCurrentProgramStateInformation (d, s); + } + + void prepareToPlay (double sr, int bs) override + { + std::lock_guard lock (innerMutex); + inner->setRateAndBufferSizeDetails (sr, bs); + inner->prepareToPlay (sr, bs); + prepareToPlayParams = { sr, bs }; + } + + void releaseResources() override { inner->releaseResources(); } + + void memoryWarningReceived() override { inner->memoryWarningReceived(); } + + void processBlock (AudioBuffer& a, MidiBuffer& m) override + { + const SpinLock::ScopedTryLockType scope (innerProcessBlockFlag); + + if (! scope.isLocked()) + return; + + inner->processBlock (a, m); + araHost.afterProcessBlock (a.getNumSamples()); + } + + void processBlock (AudioBuffer& a, MidiBuffer& m) override + { + const SpinLock::ScopedTryLockType scope (innerProcessBlockFlag); + + if (! scope.isLocked()) + return; + + inner->processBlock (a, m); + araHost.afterProcessBlock (a.getNumSamples()); + } + + void processBlockBypassed (AudioBuffer& a, MidiBuffer& m) override + { + const SpinLock::ScopedTryLockType scope (innerProcessBlockFlag); + + if (! scope.isLocked()) + return; + + inner->processBlockBypassed (a, m); + araHost.afterProcessBlock (a.getNumSamples()); + } + + void processBlockBypassed (AudioBuffer& a, MidiBuffer& m) override + { + const SpinLock::ScopedTryLockType scope (innerProcessBlockFlag); + + if (! scope.isLocked()) + return; + + inner->processBlockBypassed (a, m); + araHost.afterProcessBlock (a.getNumSamples()); + } + + bool supportsDoublePrecisionProcessing() const override + { + std::lock_guard lock (innerMutex); + return inner->supportsDoublePrecisionProcessing(); + } + + bool supportsMPE() const override + { + std::lock_guard lock (innerMutex); + return inner->supportsMPE(); + } + + bool isMidiEffect() const override + { + std::lock_guard lock (innerMutex); + return inner->isMidiEffect(); + } + + void reset() override + { + std::lock_guard lock (innerMutex); + inner->reset(); + } + + void setNonRealtime (bool b) noexcept override + { + std::lock_guard lock (innerMutex); + inner->setNonRealtime (b); + } + + void refreshParameterList() override + { + std::lock_guard lock (innerMutex); + inner->refreshParameterList(); + } + + void numChannelsChanged() override + { + std::lock_guard lock (innerMutex); + inner->numChannelsChanged(); + } + + void numBusesChanged() override + { + std::lock_guard lock (innerMutex); + inner->numBusesChanged(); + } + + void processorLayoutsChanged() override + { + std::lock_guard lock (innerMutex); + inner->processorLayoutsChanged(); + } + + void setPlayHead (AudioPlayHead* p) override { ignoreUnused (p); } + + void updateTrackProperties (const TrackProperties& p) override + { + std::lock_guard lock (innerMutex); + inner->updateTrackProperties (p); + } + + bool isBusesLayoutSupported (const BusesLayout& layout) const override + { + std::lock_guard lock (innerMutex); + return inner->checkBusesLayoutSupported (layout); + } + + bool canAddBus (bool) const override + { + std::lock_guard lock (innerMutex); + return true; + } + bool canRemoveBus (bool) const override + { + std::lock_guard lock (innerMutex); + return true; + } + + //============================================================================== + void fillInPluginDescription (PluginDescription& description) const override + { + return inner->fillInPluginDescription (description); + } + +private: + void matchBuses (bool isInput) + { + const auto inBuses = inner->getBusCount (isInput); + + while (getBusCount (isInput) < inBuses) + addBus (isInput); + + while (inBuses < getBusCount (isInput)) + removeBus (isInput); + } + + // Used for mutual exclusion between the audio and other threads + SpinLock innerProcessBlockFlag; + + // Used for mutual exclusion on non-audio threads + mutable std::mutex innerMutex; + + std::unique_ptr inner; + + ARATestHost araHost; + + struct PrepareToPlayParams + { + PrepareToPlayParams() : isValid (false) {} + + PrepareToPlayParams (double sampleRateIn, int samplesPerBlockIn) + : isValid (true), sampleRate (sampleRateIn), samplesPerBlock (samplesPerBlockIn) + { + } + + bool isValid; + double sampleRate; + int samplesPerBlock; + }; + + PrepareToPlayParams prepareToPlayParams; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ARAPluginInstanceWrapper) +}; +#endif diff --git a/extras/AudioPluginHost/Source/Plugins/PluginGraph.cpp b/extras/AudioPluginHost/Source/Plugins/PluginGraph.cpp index 734d65a507..bfde9429d2 100644 --- a/extras/AudioPluginHost/Source/Plugins/PluginGraph.cpp +++ b/extras/AudioPluginHost/Source/Plugins/PluginGraph.cpp @@ -73,21 +73,23 @@ AudioProcessorGraph::Node::Ptr PluginGraph::getNodeForName (const String& name) return nullptr; } -void PluginGraph::addPlugin (const PluginDescription& desc, Point pos) +void PluginGraph::addPlugin (const PluginDescriptionAndPreference& desc, Point pos) { - std::shared_ptr dpiDisabler = makeDPIAwarenessDisablerForPlugin (desc); + std::shared_ptr dpiDisabler = makeDPIAwarenessDisablerForPlugin (desc.pluginDescription); - formatManager.createPluginInstanceAsync (desc, + formatManager.createPluginInstanceAsync (desc.pluginDescription, graph.getSampleRate(), graph.getBlockSize(), - [this, pos, dpiDisabler] (std::unique_ptr instance, const String& error) + [this, pos, dpiDisabler, useARA = desc.useARA] (std::unique_ptr instance, const String& error) { - addPluginCallback (std::move (instance), error, pos); + addPluginCallback (std::move (instance), error, pos, useARA); }); } void PluginGraph::addPluginCallback (std::unique_ptr instance, - const String& error, Point pos) + const String& error, + Point pos, + PluginDescriptionAndPreference::UseARA useARA) { if (instance == nullptr) { @@ -97,12 +99,21 @@ void PluginGraph::addPluginCallback (std::unique_ptr instan } else { + #if JUCE_PLUGINHOST_ARA && (JUCE_MAC || JUCE_WINDOWS) + if (useARA == PluginDescriptionAndPreference::UseARA::yes + && instance->getPluginDescription().hasARAExtension) + { + instance = std::make_unique (std::move (instance)); + } + #endif + instance->enableAllBuses(); if (auto node = graph.addNode (std::move (instance))) { node->properties.set ("x", pos.x); node->properties.set ("y", pos.y); + node->properties.set ("useARA", useARA == PluginDescriptionAndPreference::UseARA::yes); changed(); } } @@ -193,10 +204,10 @@ void PluginGraph::newDocument() jassert (internalFormat.getAllTypes().size() > 3); - addPlugin (internalFormat.getAllTypes()[0], { 0.5, 0.1 }); - addPlugin (internalFormat.getAllTypes()[1], { 0.25, 0.1 }); - addPlugin (internalFormat.getAllTypes()[2], { 0.5, 0.9 }); - addPlugin (internalFormat.getAllTypes()[3], { 0.25, 0.9 }); + addPlugin (PluginDescriptionAndPreference { internalFormat.getAllTypes()[0] }, { 0.5, 0.1 }); + addPlugin (PluginDescriptionAndPreference { internalFormat.getAllTypes()[1] }, { 0.25, 0.1 }); + addPlugin (PluginDescriptionAndPreference { internalFormat.getAllTypes()[2] }, { 0.5, 0.9 }); + addPlugin (PluginDescriptionAndPreference { internalFormat.getAllTypes()[3] }, { 0.25, 0.9 }); MessageManager::callAsync ([this] { @@ -325,6 +336,7 @@ static XmlElement* createNodeXml (AudioProcessorGraph::Node* const node) noexcep e->setAttribute ("uid", (int) node->nodeID.uid); e->setAttribute ("x", node->properties ["x"].toString()); e->setAttribute ("y", node->properties ["y"].toString()); + e->setAttribute ("useARA", node->properties ["useARA"].toString()); for (int i = 0; i < (int) PluginWindow::Type::numTypes; ++i) { @@ -365,26 +377,42 @@ static XmlElement* createNodeXml (AudioProcessorGraph::Node* const node) noexcep void PluginGraph::createNodeFromXml (const XmlElement& xml) { - PluginDescription pd; + PluginDescriptionAndPreference pd; + const auto nodeUsesARA = xml.getBoolAttribute ("useARA"); for (auto* e : xml.getChildIterator()) { - if (pd.loadFromXml (*e)) + if (pd.pluginDescription.loadFromXml (*e)) + { + pd.useARA = nodeUsesARA ? PluginDescriptionAndPreference::UseARA::yes + : PluginDescriptionAndPreference::UseARA::no; break; + } } auto createInstanceWithFallback = [&]() -> std::unique_ptr { - auto createInstance = [this] (const PluginDescription& description) + auto createInstance = [this] (const PluginDescriptionAndPreference& description) -> std::unique_ptr { String errorMessage; - auto localDpiDisabler = makeDPIAwarenessDisablerForPlugin (description); + auto localDpiDisabler = makeDPIAwarenessDisablerForPlugin (description.pluginDescription); - return formatManager.createPluginInstance (description, - graph.getSampleRate(), - graph.getBlockSize(), - errorMessage); + auto instance = formatManager.createPluginInstance (description.pluginDescription, + graph.getSampleRate(), + graph.getBlockSize(), + errorMessage); + + #if JUCE_PLUGINHOST_ARA && (JUCE_MAC || JUCE_WINDOWS) + if (instance + && description.useARA == PluginDescriptionAndPreference::UseARA::yes + && description.pluginDescription.hasARAExtension) + { + return std::make_unique (std::move (instance)); + } + #endif + + return instance; }; if (auto instance = createInstance (pd)) @@ -392,19 +420,19 @@ void PluginGraph::createNodeFromXml (const XmlElement& xml) const auto allFormats = formatManager.getFormats(); const auto matchingFormat = std::find_if (allFormats.begin(), allFormats.end(), - [&] (const AudioPluginFormat* f) { return f->getName() == pd.pluginFormatName; }); + [&] (const AudioPluginFormat* f) { return f->getName() == pd.pluginDescription.pluginFormatName; }); if (matchingFormat == allFormats.end()) return nullptr; const auto plugins = knownPlugins.getTypesForFormat (**matchingFormat); const auto matchingPlugin = std::find_if (plugins.begin(), plugins.end(), - [&] (const PluginDescription& desc) { return pd.uniqueId == desc.uniqueId; }); + [&] (const PluginDescription& desc) { return pd.pluginDescription.uniqueId == desc.uniqueId; }); if (matchingPlugin == plugins.end()) return nullptr; - return createInstance (*matchingPlugin); + return createInstance (PluginDescriptionAndPreference { *matchingPlugin }); }; if (auto instance = createInstanceWithFallback()) @@ -431,6 +459,7 @@ void PluginGraph::createNodeFromXml (const XmlElement& xml) node->properties.set ("x", xml.getDoubleAttribute ("x")); node->properties.set ("y", xml.getDoubleAttribute ("y")); + node->properties.set ("useARA", xml.getBoolAttribute ("useARA")); for (int i = 0; i < (int) PluginWindow::Type::numTypes; ++i) { diff --git a/extras/AudioPluginHost/Source/Plugins/PluginGraph.h b/extras/AudioPluginHost/Source/Plugins/PluginGraph.h index f75c03d196..ef52cb7f74 100644 --- a/extras/AudioPluginHost/Source/Plugins/PluginGraph.h +++ b/extras/AudioPluginHost/Source/Plugins/PluginGraph.h @@ -20,6 +20,29 @@ #include "../UI/PluginWindow.h" +//============================================================================== +/** A type that encapsulates a PluginDescription and some preferences regarding + how plugins of that description should be instantiated. +*/ +struct PluginDescriptionAndPreference +{ + enum class UseARA { no, yes }; + + PluginDescriptionAndPreference() = default; + + explicit PluginDescriptionAndPreference (PluginDescription pd) + : pluginDescription (std::move (pd)), + useARA (pluginDescription.hasARAExtension ? PluginDescriptionAndPreference::UseARA::yes + : PluginDescriptionAndPreference::UseARA::no) + {} + + PluginDescriptionAndPreference (PluginDescription pd, UseARA ara) + : pluginDescription (std::move (pd)), useARA (ara) + {} + + PluginDescription pluginDescription; + UseARA useARA = UseARA::no; +}; //============================================================================== /** @@ -37,7 +60,7 @@ public: //============================================================================== using NodeID = AudioProcessorGraph::NodeID; - void addPlugin (const PluginDescription&, Point); + void addPlugin (const PluginDescriptionAndPreference&, Point); AudioProcessorGraph::Node::Ptr getNodeForName (const String& name) const; @@ -85,7 +108,10 @@ private: NodeID getNextUID() noexcept; void createNodeFromXml (const XmlElement&); - void addPluginCallback (std::unique_ptr, const String& error, Point); + void addPluginCallback (std::unique_ptr, + const String& error, + Point, + PluginDescriptionAndPreference::UseARA useARA); void changeListenerCallback (ChangeBroadcaster*) override; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PluginGraph) diff --git a/extras/AudioPluginHost/Source/UI/GraphEditorPanel.cpp b/extras/AudioPluginHost/Source/UI/GraphEditorPanel.cpp index 54a42129e3..eb7cefcbf4 100644 --- a/extras/AudioPluginHost/Source/UI/GraphEditorPanel.cpp +++ b/extras/AudioPluginHost/Source/UI/GraphEditorPanel.cpp @@ -391,6 +391,14 @@ struct GraphEditorPanel::PluginComponent : public Component, return {}; } + bool isNodeUsingARA() const + { + if (auto node = graph.graph.getNodeForId (pluginID)) + return node->properties["useARA"]; + + return false; + } + void showPopupMenu() { menu.reset (new PopupMenu); @@ -412,6 +420,12 @@ struct GraphEditorPanel::PluginComponent : public Component, menu->addItem ("Show all parameters", [this] { showWindow (PluginWindow::Type::generic); }); menu->addItem ("Show debug log", [this] { showWindow (PluginWindow::Type::debug); }); + #if JUCE_PLUGINHOST_ARA && (JUCE_MAC || JUCE_WINDOWS) + if (auto* instance = dynamic_cast (getProcessor())) + if (instance->getPluginDescription().hasARAExtension && isNodeUsingARA()) + menu->addItem ("Show ARA host controls", [this] { showWindow (PluginWindow::Type::araHost); }); + #endif + if (autoScaleOptionAvailable) addPluginAutoScaleOptionsSubMenu (dynamic_cast (getProcessor()), *menu); @@ -777,7 +791,7 @@ void GraphEditorPanel::mouseDrag (const MouseEvent& e) stopTimer(); } -void GraphEditorPanel::createNewPlugin (const PluginDescription& desc, Point position) +void GraphEditorPanel::createNewPlugin (const PluginDescriptionAndPreference& desc, Point position) { graph.addPlugin (desc, position.toDouble() / Point ((double) getWidth(), (double) getHeight())); } @@ -1273,7 +1287,7 @@ void GraphDocumentComponent::resized() checkAvailableWidth(); } -void GraphDocumentComponent::createNewPlugin (const PluginDescription& desc, Point pos) +void GraphDocumentComponent::createNewPlugin (const PluginDescriptionAndPreference& desc, Point pos) { graphPanel->createNewPlugin (desc, pos); } @@ -1320,7 +1334,8 @@ void GraphDocumentComponent::itemDropped (const SourceDetails& details) // must be a valid index! jassert (isPositiveAndBelow (pluginTypeIndex, pluginList.getNumTypes())); - createNewPlugin (pluginList.getTypes()[pluginTypeIndex], details.localPosition); + createNewPlugin (PluginDescriptionAndPreference { pluginList.getTypes()[pluginTypeIndex] }, + details.localPosition); } void GraphDocumentComponent::showSidePanel (bool showSettingsPanel) diff --git a/extras/AudioPluginHost/Source/UI/GraphEditorPanel.h b/extras/AudioPluginHost/Source/UI/GraphEditorPanel.h index a2a9de16e2..413a6224b2 100644 --- a/extras/AudioPluginHost/Source/UI/GraphEditorPanel.h +++ b/extras/AudioPluginHost/Source/UI/GraphEditorPanel.h @@ -31,10 +31,11 @@ class GraphEditorPanel : public Component, private Timer { public: + //============================================================================== GraphEditorPanel (PluginGraph& graph); ~GraphEditorPanel() override; - void createNewPlugin (const PluginDescription&, Point position); + void createNewPlugin (const PluginDescriptionAndPreference&, Point position); void paint (Graphics&) override; void resized() override; @@ -103,7 +104,7 @@ public: ~GraphDocumentComponent() override; //============================================================================== - void createNewPlugin (const PluginDescription&, Point position); + void createNewPlugin (const PluginDescriptionAndPreference&, Point position); void setDoublePrecision (bool doublePrecision); bool closeAnyOpenPluginWindows(); diff --git a/extras/AudioPluginHost/Source/UI/MainHostWindow.cpp b/extras/AudioPluginHost/Source/UI/MainHostWindow.cpp index 926753296c..2ca2c5eed3 100644 --- a/extras/AudioPluginHost/Source/UI/MainHostWindow.cpp +++ b/extras/AudioPluginHost/Source/UI/MainHostWindow.cpp @@ -576,7 +576,7 @@ void MainHostWindow::menuItemSelected (int menuItemID, int /*topLevelMenuIndex*/ } else { - if (KnownPluginList::getIndexChosenByMenu (pluginDescriptions, menuItemID) >= 0) + if (getIndexChosenByMenu (menuItemID) >= 0) createPlugin (getChosenType (menuItemID), { proportionOfWidth (0.3f + Random::getSystemRandom().nextFloat() * 0.6f), proportionOfHeight (0.3f + Random::getSystemRandom().nextFloat() * 0.6f) }); } @@ -588,12 +588,64 @@ void MainHostWindow::menuBarActivated (bool isActivated) graphHolder->unfocusKeyboardComponent(); } -void MainHostWindow::createPlugin (const PluginDescription& desc, Point pos) +void MainHostWindow::createPlugin (const PluginDescriptionAndPreference& desc, Point pos) { if (graphHolder != nullptr) graphHolder->createNewPlugin (desc, pos); } +static bool containsDuplicateNames (const Array& plugins, const String& name) +{ + int matches = 0; + + for (auto& p : plugins) + if (p.name == name && ++matches > 1) + return true; + + return false; +} + +static constexpr int menuIDBase = 0x324503f4; + +static void addToMenu (const KnownPluginList::PluginTree& tree, + PopupMenu& m, + const Array& allPlugins, + Array& addedPlugins) +{ + for (auto* sub : tree.subFolders) + { + PopupMenu subMenu; + addToMenu (*sub, subMenu, allPlugins, addedPlugins); + + m.addSubMenu (sub->folder, subMenu, true, nullptr, false, 0); + } + + auto addPlugin = [&] (const auto& descriptionAndPreference, const auto& pluginName) + { + addedPlugins.add (descriptionAndPreference); + const auto menuID = addedPlugins.size() - 1 + menuIDBase; + m.addItem (menuID, pluginName, true, false); + }; + + for (auto& plugin : tree.plugins) + { + auto name = plugin.name; + + if (containsDuplicateNames (tree.plugins, name)) + name << " (" << plugin.pluginFormatName << ')'; + + addPlugin (PluginDescriptionAndPreference { plugin, PluginDescriptionAndPreference::UseARA::no }, name); + + #if JUCE_PLUGINHOST_ARA && (JUCE_MAC || JUCE_WINDOWS) + if (plugin.hasARAExtension) + { + name << " (ARA)"; + addPlugin (PluginDescriptionAndPreference { plugin }, name); + } + #endif + } +} + void MainHostWindow::addPluginsToMenu (PopupMenu& m) { if (graphHolder != nullptr) @@ -606,7 +658,7 @@ void MainHostWindow::addPluginsToMenu (PopupMenu& m) m.addSeparator(); - pluginDescriptions = knownPluginList.getTypes(); + auto pluginDescriptions = knownPluginList.getTypes(); // This avoids showing the internal types again later on in the list pluginDescriptions.removeIf ([] (PluginDescription& desc) @@ -614,15 +666,23 @@ void MainHostWindow::addPluginsToMenu (PopupMenu& m) return desc.pluginFormatName == InternalPluginFormat::getIdentifier(); }); - KnownPluginList::addToMenu (m, pluginDescriptions, pluginSortMethod); + auto tree = KnownPluginList::createTree (pluginDescriptions, pluginSortMethod); + pluginDescriptionsAndPreference = {}; + addToMenu (*tree, m, pluginDescriptions, pluginDescriptionsAndPreference); +} + +int MainHostWindow::getIndexChosenByMenu (int menuID) const +{ + const auto i = menuID - menuIDBase; + return isPositiveAndBelow (i, pluginDescriptionsAndPreference.size()) ? i : -1; } -PluginDescription MainHostWindow::getChosenType (const int menuID) const +PluginDescriptionAndPreference MainHostWindow::getChosenType (const int menuID) const { if (menuID >= 1 && menuID < (int) (1 + internalTypes.size())) - return internalTypes[(size_t) (menuID - 1)]; + return PluginDescriptionAndPreference { internalTypes[(size_t) (menuID - 1)] }; - return pluginDescriptions[KnownPluginList::getIndexChosenByMenu (pluginDescriptions, menuID)]; + return pluginDescriptionsAndPreference[getIndexChosenByMenu (menuID)]; } //============================================================================== @@ -905,7 +965,7 @@ void MainHostWindow::filesDropped (const StringArray& files, int x, int y) for (int i = 0; i < jmin (5, typesFound.size()); ++i) if (auto* desc = typesFound.getUnchecked(i)) - createPlugin (*desc, pos); + createPlugin (PluginDescriptionAndPreference { *desc }, pos); } } } diff --git a/extras/AudioPluginHost/Source/UI/MainHostWindow.h b/extras/AudioPluginHost/Source/UI/MainHostWindow.h index c463e4f400..eb177a5cf1 100644 --- a/extras/AudioPluginHost/Source/UI/MainHostWindow.h +++ b/extras/AudioPluginHost/Source/UI/MainHostWindow.h @@ -100,10 +100,10 @@ public: void tryToQuitApplication(); - void createPlugin (const PluginDescription&, Point pos); + void createPlugin (const PluginDescriptionAndPreference&, Point pos); void addPluginsToMenu (PopupMenu&); - PluginDescription getChosenType (int menuID) const; + PluginDescriptionAndPreference getChosenType (int menuID) const; std::unique_ptr graphHolder; @@ -117,6 +117,8 @@ private: void showAudioSettings(); + int getIndexChosenByMenu (int menuID) const; + //============================================================================== AudioDeviceManager deviceManager; AudioPluginFormatManager formatManager; @@ -124,7 +126,7 @@ private: std::vector internalTypes; KnownPluginList knownPluginList; KnownPluginList::SortMethod pluginSortMethod; - Array pluginDescriptions; + Array pluginDescriptionsAndPreference; class PluginListWindow; std::unique_ptr pluginListWindow; diff --git a/extras/AudioPluginHost/Source/UI/PluginWindow.h b/extras/AudioPluginHost/Source/UI/PluginWindow.h index 12ee688a6e..88246b4697 100644 --- a/extras/AudioPluginHost/Source/UI/PluginWindow.h +++ b/extras/AudioPluginHost/Source/UI/PluginWindow.h @@ -19,6 +19,7 @@ #pragma once #include "../Plugins/IOConfigurationWindow.h" +#include "../Plugins/ARAPlugin.h" inline String getFormatSuffix (const AudioProcessor* plugin) { @@ -148,6 +149,7 @@ public: programs, audioIO, debug, + araHost, numTypes }; @@ -234,6 +236,16 @@ private: type = PluginWindow::Type::generic; } + if (type == PluginWindow::Type::araHost) + { + #if JUCE_PLUGINHOST_ARA && (JUCE_MAC || JUCE_WINDOWS) + if (auto* araPluginInstanceWrapper = dynamic_cast (&processor)) + if (auto* ui = araPluginInstanceWrapper->createARAHostEditor()) + return ui; + #endif + return {}; + } + if (type == PluginWindow::Type::generic) return new GenericAudioProcessorEditor (processor); if (type == PluginWindow::Type::programs) return new ProgramAudioProcessorEditor (processor); if (type == PluginWindow::Type::audioIO) return new IOConfigurationWindow (processor); @@ -252,6 +264,7 @@ private: case Type::programs: return "Programs"; case Type::audioIO: return "IO"; case Type::debug: return "Debug"; + case Type::araHost: return "ARAHost"; case Type::numTypes: default: return {}; } diff --git a/extras/Build/CMake/JUCEModuleSupport.cmake b/extras/Build/CMake/JUCEModuleSupport.cmake index 8857757d25..07d0e01859 100644 --- a/extras/Build/CMake/JUCEModuleSupport.cmake +++ b/extras/Build/CMake/JUCEModuleSupport.cmake @@ -502,9 +502,17 @@ function(juce_add_module module_path) "${lv2_base_path}/lilv/src") target_link_libraries(juce_audio_processors INTERFACE juce_lilv_headers) + add_library(juce_ara_headers INTERFACE) + + target_include_directories(juce_ara_headers INTERFACE + "$<$:$>") + + target_link_libraries(juce_audio_processors INTERFACE juce_ara_headers) + if(JUCE_ARG_ALIAS_NAMESPACE) add_library(${JUCE_ARG_ALIAS_NAMESPACE}::juce_vst3_headers ALIAS juce_vst3_headers) add_library(${JUCE_ARG_ALIAS_NAMESPACE}::juce_lilv_headers ALIAS juce_lilv_headers) + add_library(${JUCE_ARG_ALIAS_NAMESPACE}::juce_ara_headers ALIAS juce_ara_headers) endif() endif() diff --git a/extras/Build/CMake/JUCEUtils.cmake b/extras/Build/CMake/JUCEUtils.cmake index a2de746b1e..b883a658c5 100644 --- a/extras/Build/CMake/JUCEUtils.cmake +++ b/extras/Build/CMake/JUCEUtils.cmake @@ -1969,6 +1969,22 @@ function(juce_set_vst3_sdk_path path) target_include_directories(juce_vst3_sdk INTERFACE "${path}") endfunction() +function(juce_set_ara_sdk_path path) + if(TARGET juce_ara_sdk) + message(FATAL_ERROR "juce_set_ara_sdk_path should only be called once") + endif() + + _juce_make_absolute(path) + + if(NOT EXISTS "${path}") + message(FATAL_ERROR "Could not find ARA SDK at the specified path: ${path}") + endif() + + add_library(juce_ara_sdk INTERFACE IMPORTED GLOBAL) + + target_include_directories(juce_ara_sdk INTERFACE "${path}") +endfunction() + # ================================================================================================== function(juce_disable_default_flags) diff --git a/modules/juce_audio_processors/format/juce_AudioPluginFormat.h b/modules/juce_audio_processors/format/juce_AudioPluginFormat.h index 87c13937e5..131e7da288 100644 --- a/modules/juce_audio_processors/format/juce_AudioPluginFormat.h +++ b/modules/juce_audio_processors/format/juce_AudioPluginFormat.h @@ -133,6 +133,18 @@ public: /** Returns true if instantiation of this plugin type must be done from a non-message thread. */ virtual bool requiresUnblockedMessageThreadDuringCreation (const PluginDescription&) const = 0; + /** A callback lambda that is passed to getARAFactory() */ + using ARAFactoryCreationCallback = std::function; + + /** Tries to create an ::ARAFactoryWrapper for this description. + + The result of the operation will be wrapped into an ARAFactoryResult, + which will be passed to a callback object supplied by the caller. + + @see AudioPluginFormatManager::createARAFactoryAsync + */ + virtual void createARAFactoryAsync (const PluginDescription&, ARAFactoryCreationCallback callback) { callback ({}); } + protected: //============================================================================== friend class AudioPluginFormatManager; diff --git a/modules/juce_audio_processors/format/juce_AudioPluginFormatManager.cpp b/modules/juce_audio_processors/format/juce_AudioPluginFormatManager.cpp index c10b076091..09ce7e6e17 100644 --- a/modules/juce_audio_processors/format/juce_AudioPluginFormatManager.cpp +++ b/modules/juce_audio_processors/format/juce_AudioPluginFormatManager.cpp @@ -129,6 +129,22 @@ std::unique_ptr AudioPluginFormatManager::createPluginInsta return {}; } +void AudioPluginFormatManager::createARAFactoryAsync (const PluginDescription& description, + AudioPluginFormat::ARAFactoryCreationCallback callback) const +{ + String errorMessage; + + if (auto* format = findFormatForDescription (description, errorMessage)) + { + format->createARAFactoryAsync (description, callback); + } + else + { + errorMessage = NEEDS_TRANS ("Couldn't find format for the provided description"); + callback ({ {}, std::move (errorMessage) }); + } +} + void AudioPluginFormatManager::createPluginInstanceAsync (const PluginDescription& description, double initialSampleRate, int initialBufferSize, AudioPluginFormat::PluginCreationCallback callback) diff --git a/modules/juce_audio_processors/format/juce_AudioPluginFormatManager.h b/modules/juce_audio_processors/format/juce_AudioPluginFormatManager.h index af85379a13..039252451d 100644 --- a/modules/juce_audio_processors/format/juce_AudioPluginFormatManager.h +++ b/modules/juce_audio_processors/format/juce_AudioPluginFormatManager.h @@ -102,6 +102,22 @@ public: double initialSampleRate, int initialBufferSize, AudioPluginFormat::PluginCreationCallback callback); + /** Tries to create an ::ARAFactoryWrapper for this description. + + The result of the operation will be wrapped into an ARAFactoryResult, + which will be passed to a callback object supplied by the caller. + + The operation may fail, in which case the callback will be called with + with a result object where ARAFactoryResult::araFactory.get() will return + a nullptr. + + In case of success the returned ::ARAFactoryWrapper will ensure that + modules required for the correct functioning of the ARAFactory will remain + loaded for the lifetime of the object. + */ + void createARAFactoryAsync (const PluginDescription& description, + AudioPluginFormat::ARAFactoryCreationCallback callback) const; + /** Checks that the file or component for this plugin actually still exists. (This won't try to load the plugin) */ diff --git a/modules/juce_audio_processors/format_types/juce_ARACommon.cpp b/modules/juce_audio_processors/format_types/juce_ARACommon.cpp new file mode 100644 index 0000000000..f4990cd492 --- /dev/null +++ b/modules/juce_audio_processors/format_types/juce_ARACommon.cpp @@ -0,0 +1,69 @@ +/* + ============================================================================== + + This file is part of the JUCE 7 technical preview. + Copyright (c) 2022 - Raw Material Software Limited + + You may use this code under the terms of the GPL v3 + (see www.gnu.org/licenses). + + For the technical preview this file cannot be licensed commercially. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#if (JUCE_PLUGINHOST_ARA && (JUCE_PLUGINHOST_VST3 || JUCE_PLUGINHOST_AU) && (JUCE_MAC || JUCE_WINDOWS)) + +#include + +namespace juce +{ + +static void dummyARAInterfaceAssert (ARA::ARAAssertCategory, const void*, const char*) +{} + +static ARA::ARAInterfaceConfiguration createInterfaceConfig (const ARA::ARAFactory* araFactory) +{ + static auto* assertFunction = &dummyARAInterfaceAssert; + + #if ARA_VALIDATE_API_CALLS + assertFunction = &::ARA::ARAInterfaceAssert; + static std::once_flag flag; + std::call_once (flag, [] { ARA::ARASetExternalAssertReference (&assertFunction); }); + #endif + + return makeARASizedStruct (&ARA::ARAInterfaceConfiguration::assertFunctionAddress, + jmin (araFactory->highestSupportedApiGeneration, (ARA::ARAAPIGeneration) ARA::kARAAPIGeneration_2_X_Draft), + &assertFunction); +} + +static std::shared_ptr getOrCreateARAFactory (const ARA::ARAFactory* ptr, + std::function onDelete) +{ + JUCE_ASSERT_MESSAGE_THREAD + + static std::unordered_map> cache; + + auto& cachePtr = cache[ptr]; + + if (const auto obj = cachePtr.lock()) + return obj; + + const auto interfaceConfig = createInterfaceConfig (ptr); + ptr->initializeARAWithConfiguration (&interfaceConfig); + const auto obj = std::shared_ptr (ptr, [deleter = std::move (onDelete)] (const ARA::ARAFactory* factory) + { + factory->uninitializeARA(); + deleter (factory); + }); + cachePtr = obj; + return obj; +} + +} + +#endif diff --git a/modules/juce_audio_processors/format_types/juce_ARACommon.h b/modules/juce_audio_processors/format_types/juce_ARACommon.h new file mode 100644 index 0000000000..a8c2347669 --- /dev/null +++ b/modules/juce_audio_processors/format_types/juce_ARACommon.h @@ -0,0 +1,78 @@ +/* + ============================================================================== + + This file is part of the JUCE 7 technical preview. + Copyright (c) 2022 - Raw Material Software Limited + + You may use this code under the terms of the GPL v3 + (see www.gnu.org/licenses). + + For the technical preview this file cannot be licensed commercially. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#pragma once + +namespace ARA +{ + struct ARAFactory; +} + +namespace juce +{ + +/** Encapsulates an ARAFactory pointer and makes sure that it remains in a valid state + for the lifetime of the ARAFactoryWrapper object. + + @tags{ARA} +*/ +class ARAFactoryWrapper +{ +public: + ARAFactoryWrapper() = default; + + /** @internal + + Used by the framework to encapsulate ARAFactory pointers loaded from plugins. + */ + explicit ARAFactoryWrapper (std::shared_ptr factoryIn) : factory (std::move (factoryIn)) {} + + /** Returns the contained ARAFactory pointer, which can be a nullptr. + + The validity of the returned pointer is only guaranteed for the lifetime of this wrapper. + */ + const ARA::ARAFactory* get() const noexcept { return factory.get(); } + +private: + std::shared_ptr factory; +}; + +/** Represents the result of AudioPluginFormatManager::createARAFactoryAsync(). + + If the operation fails then #araFactory will contain `nullptr`, and #errorMessage may + contain a reason for the failure. + + The araFactory member ensures that the module necessary for the correct functioning + of the factory will remain loaded. + + @tags{ARA} +*/ +struct ARAFactoryResult +{ + ARAFactoryWrapper araFactory; + String errorMessage; +}; + +template +constexpr Obj makeARASizedStruct (Member Obj::* member, Ts&&... ts) +{ + return { reinterpret_cast (&(static_cast (nullptr)->*member)) + sizeof (Member), + std::forward (ts)... }; +} + +} // namespace juce diff --git a/modules/juce_audio_processors/format_types/juce_ARAHosting.cpp b/modules/juce_audio_processors/format_types/juce_ARAHosting.cpp new file mode 100644 index 0000000000..b259e3bda3 --- /dev/null +++ b/modules/juce_audio_processors/format_types/juce_ARAHosting.cpp @@ -0,0 +1,451 @@ +/* + ============================================================================== + + This file is part of the JUCE 7 technical preview. + Copyright (c) 2022 - Raw Material Software Limited + + You may use this code under the terms of the GPL v3 + (see www.gnu.org/licenses). + + For the technical preview this file cannot be licensed commercially. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#if (JUCE_PLUGINHOST_ARA && (JUCE_PLUGINHOST_VST3 || JUCE_PLUGINHOST_AU) && (JUCE_MAC || JUCE_WINDOWS)) + +#include "juce_ARAHosting.h" +#include + +#include + +namespace juce +{ +struct ARAEditGuardState +{ +public: + /* Returns true if this controller wasn't previously present. */ + bool add (ARA::Host::DocumentController& dc) + { + const std::lock_guard lock (mutex); + return ++counts[&dc] == 1; + } + + /* Returns true if this controller is no longer present. */ + bool remove (ARA::Host::DocumentController& dc) + { + const std::lock_guard lock (mutex); + return --counts[&dc] == 0; + } + +private: + std::map counts; + std::mutex mutex; +}; + +static ARAEditGuardState editGuardState; + +ARAEditGuard::ARAEditGuard (ARA::Host::DocumentController& dcIn) : dc (dcIn) +{ + if (editGuardState.add (dc)) + dc.beginEditing(); +} + +ARAEditGuard::~ARAEditGuard() +{ + if (editGuardState.remove (dc)) + dc.endEditing(); +} + +//============================================================================== +namespace ARAHostModel +{ + +//============================================================================== +AudioSource::AudioSource (ARA::ARAAudioSourceHostRef hostRef, + ARA::Host::DocumentController& dc, + const ARA::ARAAudioSourceProperties& props) + : ManagedARAHandle (dc, [&] + { + const ARAEditGuard guard (dc); + return dc.createAudioSource (hostRef, &props); + }()) +{ +} + +void AudioSource::update (const ARA::ARAAudioSourceProperties& props) +{ + const ARAEditGuard guard (getDocumentController()); + getDocumentController().updateAudioSourceProperties (getPluginRef(), &props); +} + +void AudioSource::enableAudioSourceSamplesAccess (bool x) +{ + const ARAEditGuard guard (getDocumentController()); + getDocumentController().enableAudioSourceSamplesAccess (getPluginRef(), x); +} + +void AudioSource::destroy (ARA::Host::DocumentController& dc, Ptr ptr) +{ + dc.destroyAudioSource (ptr); +} + +//============================================================================== +AudioModification::AudioModification (ARA::ARAAudioModificationHostRef hostRef, + ARA::Host::DocumentController& dc, + AudioSource& s, + const ARA::ARAAudioModificationProperties& props) + : ManagedARAHandle (dc, [&] + { + const ARAEditGuard guard (dc); + return dc.createAudioModification (s.getPluginRef(), hostRef, &props); + }()), + source (s) +{ +} + +void AudioModification::update (const ARA::ARAAudioModificationProperties& props) +{ + const ARAEditGuard guard (getDocumentController()); + getDocumentController().updateAudioModificationProperties (getPluginRef(), &props); +} + +void AudioModification::destroy (ARA::Host::DocumentController& dc, Ptr ptr) +{ + dc.destroyAudioModification (ptr); +} + +//============================================================================== +class PlaybackRegion::Impl : public ManagedARAHandle, + public DeletionListener +{ +public: + Impl (ARA::ARAPlaybackRegionHostRef hostRef, + ARA::Host::DocumentController& dc, + AudioModification& m, + const ARA::ARAPlaybackRegionProperties& props); + + ~Impl() override + { + for (const auto& l : listeners) + l->removeListener (*this); + } + + /* Updates the state of the corresponding %ARA model object. + + Places the DocumentController in editable state. + + You can use getEmptyProperties() to acquire a properties struct where the `structSize` + field has already been correctly set. + */ + void update (const ARA::ARAPlaybackRegionProperties& props); + + auto& getAudioModification() const { return modification; } + + static void destroy (ARA::Host::DocumentController&, Ptr); + + void addListener (DeletionListener& l) { listeners.insert (&l); } + void removeListener (DeletionListener& l) noexcept override { listeners.erase (&l); } + +private: + AudioModification* modification = nullptr; + std::unordered_set listeners; +}; + +PlaybackRegion::Impl::Impl (ARA::ARAPlaybackRegionHostRef hostRef, + ARA::Host::DocumentController& dc, + AudioModification& m, + const ARA::ARAPlaybackRegionProperties& props) + : ManagedARAHandle (dc, [&] + { + const ARAEditGuard guard (dc); + return dc.createPlaybackRegion (m.getPluginRef(), hostRef, &props); + }()), + modification (&m) +{ +} + +PlaybackRegion::~PlaybackRegion() = default; + +void PlaybackRegion::Impl::update (const ARA::ARAPlaybackRegionProperties& props) +{ + const ARAEditGuard guard (getDocumentController()); + getDocumentController().updatePlaybackRegionProperties (getPluginRef(), &props); +} + +void PlaybackRegion::Impl::destroy (ARA::Host::DocumentController& dc, Ptr ptr) +{ + dc.destroyPlaybackRegion (ptr); +} + +PlaybackRegion::PlaybackRegion (ARA::ARAPlaybackRegionHostRef hostRef, + ARA::Host::DocumentController& dc, + AudioModification& m, + const ARA::ARAPlaybackRegionProperties& props) + : impl (std::make_unique (hostRef, dc, m, props)) +{ +} + +void PlaybackRegion::update (const ARA::ARAPlaybackRegionProperties& props) { impl->update (props); } + +void PlaybackRegion::addListener (DeletionListener& x) { impl->addListener (x); } + +auto& PlaybackRegion::getAudioModification() const { return impl->getAudioModification(); } + +ARA::ARAPlaybackRegionRef PlaybackRegion::getPluginRef() const noexcept { return impl->getPluginRef(); } + +DeletionListener& PlaybackRegion::getDeletionListener() const noexcept { return *impl.get(); } + +//============================================================================== +MusicalContext::MusicalContext (ARA::ARAMusicalContextHostRef hostRef, + ARA::Host::DocumentController& dc, + const ARA::ARAMusicalContextProperties& props) + : ManagedARAHandle (dc, [&] + { + const ARAEditGuard guard (dc); + return dc.createMusicalContext (hostRef, &props); + }()) +{ +} + +void MusicalContext::update (const ARA::ARAMusicalContextProperties& props) +{ + const ARAEditGuard guard (getDocumentController()); + return getDocumentController().updateMusicalContextProperties (getPluginRef(), &props); +} + +void MusicalContext::destroy (ARA::Host::DocumentController& dc, Ptr ptr) +{ + dc.destroyMusicalContext (ptr); +} + +//============================================================================== +RegionSequence::RegionSequence (ARA::ARARegionSequenceHostRef hostRef, + ARA::Host::DocumentController& dc, + const ARA::ARARegionSequenceProperties& props) + : ManagedARAHandle (dc, [&] + { + const ARAEditGuard guard (dc); + return dc.createRegionSequence (hostRef, &props); + }()) +{ +} + +void RegionSequence::update (const ARA::ARARegionSequenceProperties& props) +{ + const ARAEditGuard guard (getDocumentController()); + return getDocumentController().updateRegionSequenceProperties (getPluginRef(), &props); +} + +void RegionSequence::destroy (ARA::Host::DocumentController& dc, Ptr ptr) +{ + dc.destroyRegionSequence (ptr); +} + +//============================================================================== +PlaybackRendererInterface PlugInExtensionInstance::getPlaybackRendererInterface() const +{ + if (instance != nullptr) + return PlaybackRendererInterface (instance->playbackRendererRef, instance->playbackRendererInterface); + + return {}; +} + +EditorRendererInterface PlugInExtensionInstance::getEditorRendererInterface() const +{ + if (instance != nullptr) + return EditorRendererInterface (instance->editorRendererRef, instance->editorRendererInterface); + + return {}; +} + +} // namespace ARAHostModel + +//============================================================================== +class ARAHostDocumentController::Impl +{ +public: + Impl (ARAFactoryWrapper araFactoryIn, + std::unique_ptr&& dcHostInstanceIn, + const ARA::ARADocumentControllerInstance* documentControllerInstance) + : araFactory (std::move (araFactoryIn)), + dcHostInstance (std::move (dcHostInstanceIn)), + documentController (documentControllerInstance) + { + } + + ~Impl() + { + documentController.destroyDocumentController(); + } + + static std::unique_ptr + createImpl (ARAFactoryWrapper araFactory, + const String& documentName, + std::unique_ptr&& audioAccessController, + std::unique_ptr&& archivingController, + std::unique_ptr&& contentAccessController, + std::unique_ptr&& modelUpdateController, + std::unique_ptr&& playbackController) + { + std::unique_ptr dcHostInstance = + std::make_unique (audioAccessController.release(), + archivingController.release(), + contentAccessController.release(), + modelUpdateController.release(), + playbackController.release()); + + const auto documentProperties = makeARASizedStruct (&ARA::ARADocumentProperties::name, documentName.toRawUTF8()); + + if (auto* dci = araFactory.get()->createDocumentControllerWithDocument (dcHostInstance.get(), &documentProperties)) + return std::make_unique (std::move (araFactory), std::move (dcHostInstance), dci); + + return {}; + } + + ARAHostModel::PlugInExtensionInstance bindDocumentToPluginInstance (AudioPluginInstance& instance, + ARA::ARAPlugInInstanceRoleFlags knownRoles, + ARA::ARAPlugInInstanceRoleFlags assignedRoles) + { + + const auto makeVisitor = [] (auto vst3Fn, auto auFn) + { + using Vst3Fn = decltype (vst3Fn); + using AuFn = decltype (auFn); + + struct Visitor : ExtensionsVisitor, Vst3Fn, AuFn + { + explicit Visitor (Vst3Fn vst3Fn, AuFn auFn) : Vst3Fn (std::move (vst3Fn)), AuFn (std::move (auFn)) {} + void visitVST3Client (const VST3Client& x) override { Vst3Fn::operator() (x); } + void visitAudioUnitClient (const AudioUnitClient& x) override { AuFn::operator() (x); } + }; + + return Visitor { std::move (vst3Fn), std::move (auFn) }; + }; + + const ARA::ARAPlugInExtensionInstance* pei = nullptr; + auto visitor = makeVisitor ([this, &pei, knownRoles, assignedRoles] (const ExtensionsVisitor::VST3Client& vst3Client) + { + auto* iComponentPtr = vst3Client.getIComponentPtr(); + VSTComSmartPtr araEntryPoint; + + if (araEntryPoint.loadFrom (iComponentPtr)) + pei = araEntryPoint->bindToDocumentControllerWithRoles (documentController.getRef(), knownRoles, assignedRoles); + }, + #if JUCE_PLUGINHOST_AU && JUCE_MAC + [this, &pei, knownRoles, assignedRoles] (const ExtensionsVisitor::AudioUnitClient& auClient) + { + auto audioUnit = auClient.getAudioUnitHandle(); + auto propertySize = (UInt32) sizeof (ARA::ARAAudioUnitPlugInExtensionBinding); + const auto expectedPropertySize = propertySize; + ARA::ARAAudioUnitPlugInExtensionBinding audioUnitBinding { ARA::kARAAudioUnitMagic, + documentController.getRef(), + nullptr, + knownRoles, + assignedRoles }; + + auto status = AudioUnitGetProperty (audioUnit, + ARA::kAudioUnitProperty_ARAPlugInExtensionBindingWithRoles, + kAudioUnitScope_Global, + 0, + &audioUnitBinding, + &propertySize); + + if (status == noErr + && propertySize == expectedPropertySize + && audioUnitBinding.inOutMagicNumber == ARA::kARAAudioUnitMagic + && audioUnitBinding.inDocumentControllerRef == documentController.getRef() + && audioUnitBinding.outPlugInExtension != nullptr) + { + pei = audioUnitBinding.outPlugInExtension; + } + else + jassertfalse; + } + #else + [] (const auto&) {} + #endif + ); + + instance.getExtensions (visitor); + return ARAHostModel::PlugInExtensionInstance { pei }; + } + + auto& getDocumentController() { return documentController; } + +private: + ARAFactoryWrapper araFactory; + std::unique_ptr dcHostInstance; + ARA::Host::DocumentController documentController; +}; + +ARAHostDocumentController::ARAHostDocumentController (std::unique_ptr&& implIn) + : impl { std::move (implIn) } +{} + +std::unique_ptr ARAHostDocumentController::create (ARAFactoryWrapper factory, + const String& documentName, + std::unique_ptr audioAccessController, + std::unique_ptr archivingController, + std::unique_ptr contentAccessController, + std::unique_ptr modelUpdateController, + std::unique_ptr playbackController) +{ + if (auto impl = Impl::createImpl (std::move (factory), + documentName, + std::move (audioAccessController), + std::move (archivingController), + std::move (contentAccessController), + std::move (modelUpdateController), + std::move (playbackController))) + { + return rawToUniquePtr (new ARAHostDocumentController (std::move (impl))); + } + + return {}; +} + +ARAHostDocumentController::~ARAHostDocumentController() = default; + +ARA::Host::DocumentController& ARAHostDocumentController::getDocumentController() const +{ + return impl->getDocumentController(); +} + +ARAHostModel::PlugInExtensionInstance ARAHostDocumentController::bindDocumentToPluginInstance (AudioPluginInstance& instance, + ARA::ARAPlugInInstanceRoleFlags knownRoles, + ARA::ARAPlugInInstanceRoleFlags assignedRoles) +{ + return impl->bindDocumentToPluginInstance (instance, knownRoles, assignedRoles); +} + +void createARAFactoryAsync (AudioPluginInstance& instance, std::function cb) +{ + if (! instance.getPluginDescription().hasARAExtension) + cb (ARAFactoryWrapper{}); + + struct Extensions : public ExtensionsVisitor + { + Extensions (std::function callbackIn) + : callback (std::move (callbackIn)) + {} + + void visitARAClient (const ARAClient& araClient) override + { + araClient.createARAFactoryAsync (std::move (callback)); + } + + std::function callback; + }; + + Extensions extensions { std::move(cb) }; + instance.getExtensions (extensions); +} + +} // namespace juce + +#endif diff --git a/modules/juce_audio_processors/format_types/juce_ARAHosting.h b/modules/juce_audio_processors/format_types/juce_ARAHosting.h new file mode 100644 index 0000000000..906183b25a --- /dev/null +++ b/modules/juce_audio_processors/format_types/juce_ARAHosting.h @@ -0,0 +1,733 @@ +/* + ============================================================================== + + This file is part of the JUCE 7 technical preview. + Copyright (c) 2022 - Raw Material Software Limited + + You may use this code under the terms of the GPL v3 + (see www.gnu.org/licenses). + + For the technical preview this file cannot be licensed commercially. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#pragma once + +#if (JUCE_PLUGINHOST_ARA && (JUCE_PLUGINHOST_VST3 || JUCE_PLUGINHOST_AU) && (JUCE_MAC || JUCE_WINDOWS)) || DOXYGEN + +// Include ARA SDK headers +JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wgnu-zero-variadic-macro-arguments") + +#include +#include + +JUCE_END_IGNORE_WARNINGS_GCC_LIKE + +//============================================================================== +namespace juce +{ +/** Reference counting helper class to ensure that the DocumentController is in editable state. + + When adding, removing or modifying %ARA model objects the enclosing DocumentController must be + in editable state. + + You can achieve this by using the %ARA Library calls + ARA::Host::DocumentController::beginEditing() and ARA::Host::DocumentController::endEditing(). + + However, putting the DocumentController in and out of editable state is a potentially costly + operation, thus it makes sense to group multiple modifications together and change the editable + state only once. + + ARAEditGuard keeps track of all scopes that want to edit a particular DocumentController and + will trigger beginEditing() and endEditing() only for the outermost scope. This allows you to + merge multiple editing operations into one by putting ARAEditGuard in their enclosing scope. + + @tags{ARA} +*/ +class ARAEditGuard +{ +public: + explicit ARAEditGuard (ARA::Host::DocumentController& dcIn); + ~ARAEditGuard(); + +private: + ARA::Host::DocumentController& dc; +}; + +namespace ARAHostModel +{ + +//============================================================================== +/** Allows converting, without warnings, between pointers of two unrelated types. + + This is a bit like ARA_MAP_HOST_REF, but not macro-based. + + To use it, add a line like this to a type that needs to deal in host references: + @code + using Converter = ConversionFunctions; + @endcode + + Now, you can convert back and forth with host references by calling + Converter::toHostRef() and Converter::fromHostRef(). + + @tags{ARA} +*/ +template +struct ConversionFunctions +{ + static_assert (sizeof (A) <= sizeof (B), + "It is only possible to convert between types of the same size"); + + static B toHostRef (A value) + { + return readUnaligned (&value); + } + + static A fromHostRef (B value) + { + return readUnaligned (&value); + } +}; + +//============================================================================== +template +class ManagedARAHandle +{ +public: + using Ptr = PtrIn; + + ManagedARAHandle (ARA::Host::DocumentController& dc, Ptr ptr) noexcept + : handle (ptr, Deleter { dc }) {} + + auto& getDocumentController() const { return handle.get_deleter().documentController; } + + Ptr getPluginRef() const { return handle.get(); } + +private: + struct Deleter + { + void operator() (Ptr ptr) const noexcept + { + const ARAEditGuard guard (documentController); + Base::destroy (documentController, ptr); + } + + ARA::Host::DocumentController& documentController; + }; + + std::unique_ptr, Deleter> handle; +}; + +//============================================================================== +/** Helper class for the host side implementation of the %ARA %AudioSource model object. + + Its intended use is to add a member variable of this type to your host side %AudioSource + implementation. Then it provides a RAII approach to managing the lifetime of the corresponding + objects created inside the DocumentController. When the host side object is instantiated an ARA + model object is also created in the DocumentController. When the host side object is deleted it + will be removed from the DocumentController as well. + + The class will automatically put the DocumentController into editable state for operations that + mandate this e.g. creation, deletion or updating. + + You can encapsulate multiple such operations into a scope with an ARAEditGuard in order to invoke + the editable state of the DocumentController only once. + + @tags{ARA} +*/ +class AudioSource : public ManagedARAHandle +{ +public: + /** Returns an %ARA versioned struct with the `structSize` correctly set for the currently + used SDK version. + + You should leave `structSize` unchanged, and fill out the rest of the fields appropriately + for the host implementation of the %ARA model object. + */ + static constexpr auto getEmptyProperties() { return makeARASizedStruct (&ARA::ARAAudioSourceProperties::merits64BitSamples); } + + /** Creates an AudioSource object. During construction it registers an %ARA %AudioSource model + object with the DocumentController that refers to the provided hostRef. When this object + is deleted the corresponding DocumentController model object will also be deregistered. + + You can acquire a correctly versioned `ARA::ARAAudioSourceProperties` struct by calling + getEmptyProperties(). + + Places the DocumentController in editable state. + + @see ARAEditGuard + */ + AudioSource (ARA::ARAAudioSourceHostRef hostRef, + ARA::Host::DocumentController& dc, + const ARA::ARAAudioSourceProperties& props); + + /** Destructor. Temporarily places the DocumentController in an editable state. */ + ~AudioSource() = default; + + /** Updates the state of the corresponding %ARA model object. + + Places the DocumentController in editable state. + + You can use getEmptyProperties() to acquire a properties struct where the `structSize` + field has already been correctly set. + */ + void update (const ARA::ARAAudioSourceProperties& props); + + /** Changes the plugin's access to the %AudioSource samples through the DocumentController. + + Places the DocumentController in editable state. + */ + void enableAudioSourceSamplesAccess (bool); + + /** Called by ManagedARAHandle to deregister the model object during the destruction of + AudioSource. + + You shouldn't call this function manually. + */ + static void destroy (ARA::Host::DocumentController&, Ptr); +}; + +/** Helper class for the host side implementation of the %ARA %AudioModification model object. + + Its intended use is to add a member variable of this type to your host side %AudioModification + implementation. Then it provides a RAII approach to managing the lifetime of the corresponding + objects created inside the DocumentController. When the host side object is instantiated an ARA + model object is also created in the DocumentController. When the host side object is deleted it + will be removed from the DocumentController as well. + + The class will automatically put the DocumentController into editable state for operations that + mandate this e.g. creation, deletion or updating. + + You can encapsulate multiple such operations into a scope with an ARAEditGuard in order to invoke + the editable state of the DocumentController only once. + + @tags{ARA} +*/ +class AudioModification : public ManagedARAHandle +{ +public: + /** Returns an %ARA versioned struct with the `structSize` correctly set for the currently + used SDK version. + + You should leave `structSize` unchanged, and fill out the rest of the fields appropriately + for the host implementation of the %ARA model object. + */ + static constexpr auto getEmptyProperties() + { + return makeARASizedStruct (&ARA::ARAAudioModificationProperties::persistentID); + } + + /** Creates an AudioModification object. During construction it registers an %ARA %AudioModification model + object with the DocumentController that refers to the provided hostRef. When this object + is deleted the corresponding DocumentController model object will also be deregistered. + + You can acquire a correctly versioned `ARA::ARAAudioModificationProperties` struct by calling + getEmptyProperties(). + + Places the DocumentController in editable state. + + @see ARAEditGuard + */ + AudioModification (ARA::ARAAudioModificationHostRef hostRef, + ARA::Host::DocumentController& dc, + AudioSource& s, + const ARA::ARAAudioModificationProperties& props); + + /** Updates the state of the corresponding %ARA model object. + + Places the DocumentController in editable state. + + You can use getEmptyProperties() to acquire a properties struct where the `structSize` + field has already been correctly set. + */ + void update (const ARA::ARAAudioModificationProperties& props); + + /** Returns the AudioSource containing this AudioModification. */ + auto& getAudioSource() const { return source; } + + /** Called by ManagedARAHandle to deregister the model object during the destruction of + AudioModification. + + You shouldn't call this function manually. + */ + static void destroy (ARA::Host::DocumentController&, Ptr); + +private: + AudioSource& source; +}; + +struct DeletionListener +{ + virtual ~DeletionListener() = default; + virtual void removeListener (DeletionListener& other) noexcept = 0; +}; + +struct PlaybackRegion +{ +public: + /** Returns an %ARA versioned struct with the `structSize` correctly set for the currently + used SDK version. + + You should leave `structSize` unchanged, and fill out the rest of the fields appropriately + for the host implementation of the %ARA model object. + */ + static constexpr auto getEmptyProperties() + { + return makeARASizedStruct (&ARA::ARAPlaybackRegionProperties::color); + } + + PlaybackRegion (ARA::ARAPlaybackRegionHostRef hostRef, + ARA::Host::DocumentController& dc, + AudioModification& m, + const ARA::ARAPlaybackRegionProperties& props); + + ~PlaybackRegion(); + + /** Updates the state of the corresponding %ARA model object. + + Places the DocumentController in editable state. + + You can use getEmptyProperties() to acquire a properties struct where the `structSize` + field has already been correctly set. + */ + void update (const ARA::ARAPlaybackRegionProperties& props); + + /** Adds a DeletionListener object that will be notified when the PlaybackRegion object + is deleted. + + Used by the PlaybackRegionRegistry. + + @see PlaybackRendererInterface, EditorRendererInterface + */ + void addListener (DeletionListener& x); + + /** Returns the AudioModification containing this PlaybackRegion. */ + auto& getAudioModification() const; + + /** Returns the plugin side reference to the PlaybackRegion */ + ARA::ARAPlaybackRegionRef getPluginRef() const noexcept; + DeletionListener& getDeletionListener() const noexcept; + +private: + class Impl; + + std::unique_ptr impl; +}; + +/** Helper class for the host side implementation of the %ARA %MusicalContext model object. + + Its intended use is to add a member variable of this type to your host side %MusicalContext + implementation. Then it provides a RAII approach to managing the lifetime of the corresponding + objects created inside the DocumentController. When the host side object is instantiated an ARA + model object is also created in the DocumentController. When the host side object is deleted it + will be removed from the DocumentController as well. + + The class will automatically put the DocumentController into editable state for operations that + mandate this e.g. creation, deletion or updating. + + You can encapsulate multiple such operations into a scope with an ARAEditGuard in order to invoke + the editable state of the DocumentController only once. + + @tags{ARA} +*/ +class MusicalContext : public ManagedARAHandle +{ +public: + /** Returns an %ARA versioned struct with the `structSize` correctly set for the currently + used SDK version. + + You should leave `structSize` unchanged, and fill out the rest of the fields appropriately + for the host implementation of the %ARA model object. + */ + static constexpr auto getEmptyProperties() + { + return makeARASizedStruct (&ARA::ARAMusicalContextProperties::color); + } + + /** Creates a MusicalContext object. During construction it registers an %ARA %MusicalContext model + object with the DocumentController that refers to the provided hostRef. When this object + is deleted the corresponding DocumentController model object will also be deregistered. + + You can acquire a correctly versioned `ARA::ARAMusicalContextProperties` struct by calling + getEmptyProperties(). + + Places the DocumentController in editable state. + + @see ARAEditGuard + */ + MusicalContext (ARA::ARAMusicalContextHostRef hostRef, + ARA::Host::DocumentController& dc, + const ARA::ARAMusicalContextProperties& props); + + /** Updates the state of the corresponding %ARA model object. + + Places the DocumentController in editable state. + + You can use getEmptyProperties() to acquire a properties struct where the `structSize` + field has already been correctly set. + */ + void update (const ARA::ARAMusicalContextProperties& props); + + /** Called by ManagedARAHandle to deregister the model object during the destruction of + AudioModification. + + You shouldn't call this function manually. + */ + static void destroy (ARA::Host::DocumentController&, Ptr); +}; + +/** Helper class for the host side implementation of the %ARA %RegionSequence model object. + + Its intended use is to add a member variable of this type to your host side %RegionSequence + implementation. Then it provides a RAII approach to managing the lifetime of the corresponding + objects created inside the DocumentController. When the host side object is instantiated an ARA + model object is also created in the DocumentController. When the host side object is deleted it + will be removed from the DocumentController as well. + + The class will automatically put the DocumentController into editable state for operations that + mandate this e.g. creation, deletion or updating. + + You can encapsulate multiple such operations into a scope with an ARAEditGuard in order to invoke + the editable state of the DocumentController only once. + + @tags{ARA} +*/ +class RegionSequence : public ManagedARAHandle +{ +public: + /** Returns an %ARA versioned struct with the `structSize` correctly set for the currently + used SDK version. + + You should leave `structSize` unchanged, and fill out the rest of the fields appropriately + for the host implementation of the %ARA model object. + */ + static constexpr auto getEmptyProperties() + { + return makeARASizedStruct (&ARA::ARARegionSequenceProperties::color); + } + + /** Creates a RegionSequence object. During construction it registers an %ARA %RegionSequence model + object with the DocumentController that refers to the provided hostRef. When this object + is deleted the corresponding DocumentController model object will also be deregistered. + + You can acquire a correctly versioned `ARA::ARARegionSequenceProperties` struct by calling + getEmptyProperties(). + + Places the DocumentController in editable state. + + @see ARAEditGuard + */ + RegionSequence (ARA::ARARegionSequenceHostRef hostRef, + ARA::Host::DocumentController& dc, + const ARA::ARARegionSequenceProperties& props); + + /** Updates the state of the corresponding %ARA model object. + + Places the DocumentController in editable state. + + You can use getEmptyProperties() to acquire a properties struct where the `structSize` + field has already been correctly set. + */ + void update (const ARA::ARARegionSequenceProperties& props); + + /** Called by ManagedARAHandle to deregister the model object during the destruction of + AudioModification. + + You shouldn't call this function manually. + */ + static void destroy (ARA::Host::DocumentController&, Ptr); +}; + +//============================================================================== +/** Base class used by the ::PlaybackRendererInterface and ::EditorRendererInterface + plugin extension interfaces. + + Hosts will want to create one or typically more %ARA plugin extension instances per plugin for + the purpose of playback and editor rendering. The PlaybackRegions created by the host then have + to be assigned to these instances through the appropriate interfaces. + + Whether a PlaybackRegion or an assigned RendererInterface is deleted first depends on the host + implementation and exact use case. + + By using these helper classes you can ensure that the %ARA DocumentController remains in a + valid state in both situations. In order to use them acquire an object from + PlugInExtensionInstance::getPlaybackRendererInterface() or + PlugInExtensionInstance::getEditorRendererInterface(). + + Then call add() to register a PlaybackRegion with that particular PlugInExtensionInstance's + interface. + + Now when you delete that PlaybackRegion it will be deregistered from that extension instance. + If however you want to delete the plugin extension instance before the PlaybackRegion, you can + delete the PlaybackRegionRegistry instance before deleting the plugin extension instance, which + takes care of deregistering all PlaybackRegions. + + When adding or removing PlaybackRegions the plugin instance must be in an unprepared state i.e. + before AudioProcessor::prepareToPlay() or after AudioProcessor::releaseResources(). + + @code + auto playbackRenderer = std::make_unique (plugInExtensionInstance.getPlaybackRendererInterface()); + auto playbackRegion = std::make_unique (documentController, regionSequence, audioModification, audioSource); + + // Either of the following three code variations are valid + // (1) =================================================== + playbackRenderer.add (playbackRegion); + playbackRenderer.remove (playbackRegion); + + // (2) =================================================== + playbackRenderer.add (playbackRegion); + playbackRegion.reset(); + + // (3) =================================================== + playbackRenderer.add (playbackRegion); + playbackRenderer.reset(); + @endcode + + @see PluginExtensionInstance + + @tags{ARA} +*/ +template +class PlaybackRegionRegistry +{ +public: + PlaybackRegionRegistry() = default; + + PlaybackRegionRegistry (RendererRef rendererRefIn, const Interface* interfaceIn) + : registry (std::make_unique (rendererRefIn, interfaceIn)) + { + } + + /** Adds a PlaybackRegion to the corresponding ::PlaybackRendererInterface or ::EditorRendererInterface. + + The plugin instance must be in an unprepared state i.e. before AudioProcessor::prepareToPlay() or + after AudioProcessor::releaseResources(). + */ + void add (PlaybackRegion& region) { registry->add (region); } + + /** Removes a PlaybackRegion from the corresponding ::PlaybackRendererInterface or ::EditorRendererInterface. + + The plugin instance must be in an unprepared state i.e. before AudioProcessor::prepareToPlay() or + after AudioProcessor::releaseResources(). + */ + void remove (PlaybackRegion& region) { registry->remove (region); } + + /** Returns true if the underlying %ARA plugin extension instance fulfills the corresponding role. */ + bool isValid() { return registry->isValid(); } + +private: + class Registry : private DeletionListener + { + public: + Registry (RendererRef rendererRefIn, const Interface* interfaceIn) + : rendererRef (rendererRefIn), rendererInterface (interfaceIn) + { + } + + Registry (const Registry&) = delete; + Registry (Registry&&) noexcept = delete; + + Registry& operator= (const Registry&) = delete; + Registry& operator= (Registry&&) noexcept = delete; + + ~Registry() override + { + for (const auto& region : regions) + doRemoveListener (*region.first); + } + + bool isValid() { return rendererRef != nullptr && rendererInterface != nullptr; } + + void add (PlaybackRegion& region) + { + if (isValid()) + rendererInterface->addPlaybackRegion (rendererRef, region.getPluginRef()); + + regions.emplace (®ion.getDeletionListener(), region.getPluginRef()); + region.addListener (*this); + } + + void remove (PlaybackRegion& region) + { + doRemoveListener (region.getDeletionListener()); + } + + private: + void doRemoveListener (DeletionListener& listener) noexcept + { + listener.removeListener (*this); + removeListener (listener); + } + + void removeListener (DeletionListener& listener) noexcept override + { + const auto it = regions.find (&listener); + + if (it == regions.end()) + { + jassertfalse; + return; + } + + if (isValid()) + rendererInterface->removePlaybackRegion (rendererRef, it->second); + + regions.erase (it); + } + + RendererRef rendererRef = nullptr; + const Interface* rendererInterface = nullptr; + std::map regions; + }; + + std::unique_ptr registry; +}; + +//============================================================================== +/** Helper class for managing the lifetimes of %ARA plugin extension instances and PlaybackRegions. + + You can read more about its usage at PlaybackRegionRegistry. + + @see PlaybackRegion, PlaybackRegionRegistry + + @tags{ARA} +*/ +using PlaybackRendererInterface = PlaybackRegionRegistry; + +//============================================================================== +/** Helper class for managing the lifetimes of %ARA plugin extension instances and PlaybackRegions. + + You can read more about its usage at PlaybackRegionRegistry. + + @see PlaybackRegion, PlaybackRegionRegistry + + @tags{ARA} +*/ +using EditorRendererInterface = PlaybackRegionRegistry; + +//============================================================================== +/** Wrapper class for `ARA::ARAPlugInExtensionInstance*`. + + Returned by ARAHostDocumentController::bindDocumentToPluginInstance(). The corresponding + ARAHostDocumentController must remain valid as long as the plugin extension is in use. +*/ +class PlugInExtensionInstance final +{ +public: + /** Creates an empty PlugInExtensionInstance object. + + Calling isValid() on such an object will return false. + */ + PlugInExtensionInstance() = default; + + /** Creates a PlugInExtensionInstance object that wraps a `const ARA::ARAPlugInExtensionInstance*`. + + The intended way to obtain a PlugInExtensionInstance object is to call + ARAHostDocumentController::bindDocumentToPluginInstance(), which is using this constructor. + */ + explicit PlugInExtensionInstance (const ARA::ARAPlugInExtensionInstance* instanceIn) + : instance (instanceIn) + { + } + + /** Returns the PlaybackRendererInterface for the extension instance. + + Depending on what roles were passed into + ARAHostDocumentController::bindDocumentToPluginInstance() one particular instance may not + fulfill a given role. You can use PlaybackRendererInterface::isValid() to see if this + interface was provided by the instance. + */ + PlaybackRendererInterface getPlaybackRendererInterface() const; + + /** Returns the EditorRendererInterface for the extension instance. + + Depending on what roles were passed into + ARAHostDocumentController::bindDocumentToPluginInstance() one particular instance may not + fulfill a given role. You can use EditorRendererInterface::isValid() to see if this + interface was provided by the instance. + */ + EditorRendererInterface getEditorRendererInterface() const; + + /** Returns false if the PlugInExtensionInstance was default constructed and represents + no binding to an ARAHostDocumentController. + */ + bool isValid() const noexcept { return instance != nullptr; } + +private: + const ARA::ARAPlugInExtensionInstance* instance = nullptr; +}; + +} // namespace ARAHostModel + +//============================================================================== +/** Wrapper class for `ARA::Host::DocumentController`. + + In order to create an ARAHostDocumentController from an ARAFactoryWrapper you must + provide at least two mandatory host side interfaces. You can create these implementations + by inheriting from the base classes in the `ARA::Host` namespace. + + @tags{ARA} +*/ +class ARAHostDocumentController final +{ +public: + /** Factory function. + + You must check if the returned pointer is valid. + */ + static std::unique_ptr + create (ARAFactoryWrapper factory, + const String& documentName, + std::unique_ptr audioAccessController, + std::unique_ptr archivingController, + std::unique_ptr contentAccessController = nullptr, + std::unique_ptr modelUpdateController = nullptr, + std::unique_ptr playbackController = nullptr); + + ~ARAHostDocumentController(); + + /** Returns the underlying ARA::Host::DocumentController reference. */ + ARA::Host::DocumentController& getDocumentController() const; + + /** Binds the ARAHostDocumentController and its enclosed document to a plugin instance. + + The resulting ARAHostModel::PlugInExtensionInstance is responsible for fulfilling the + ARA specific roles of the plugin. + + A single DocumentController can be bound to multiple plugin instances, which is a typical + practice among hosts. + */ + ARAHostModel::PlugInExtensionInstance bindDocumentToPluginInstance (AudioPluginInstance& instance, + ARA::ARAPlugInInstanceRoleFlags knownRoles, + ARA::ARAPlugInInstanceRoleFlags assignedRoles); + +private: + class Impl; + std::unique_ptr impl; + + explicit ARAHostDocumentController (std::unique_ptr&& implIn); +}; + +/** Calls the provided callback with an ARAFactoryWrapper object obtained from the provided + AudioPluginInstance. + + If the provided AudioPluginInstance has no ARA extensions, the callback will be called with an + ARAFactoryWrapper that wraps a nullptr. + + The object passed to the callback must be checked even if the plugin instance reports having + ARA extensions. +*/ +void createARAFactoryAsync (AudioPluginInstance& instance, std::function cb); + +} // namespace juce + +//============================================================================== +#undef ARA_REF +#undef ARA_HOST_REF + +#endif diff --git a/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.h b/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.h index 54e0856a03..17c6207776 100644 --- a/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.h +++ b/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.h @@ -47,6 +47,7 @@ public: StringArray searchPathsForPlugins (const FileSearchPath&, bool recursive, bool) override; bool doesPluginStillExist (const PluginDescription&) override; FileSearchPath getDefaultLocationsToSearch() override; + void createARAFactoryAsync (const PluginDescription&, ARAFactoryCreationCallback callback) override; private: //============================================================================== diff --git a/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.mm b/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.mm index 7724272c0a..3acd2a9255 100644 --- a/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.mm +++ b/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.mm @@ -24,6 +24,11 @@ JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") #include #include #include + +#if JUCE_PLUGINHOST_ARA + #include +#endif + #endif #include @@ -422,6 +427,197 @@ namespace AudioUnitFormatHelpers } } +static bool hasARAExtension (AudioUnit audioUnit) +{ + #if JUCE_PLUGINHOST_ARA + UInt32 propertySize = sizeof (ARA::ARAAudioUnitFactory); + Boolean isWriteable = FALSE; + + OSStatus status = AudioUnitGetPropertyInfo (audioUnit, + ARA::kAudioUnitProperty_ARAFactory, + kAudioUnitScope_Global, + 0, + &propertySize, + &isWriteable); + + if ((status == noErr) && (propertySize == sizeof (ARA::ARAAudioUnitFactory)) && ! isWriteable) + return true; + #else + ignoreUnused (audioUnit); + #endif + + return false; +} + +struct AudioUnitDeleter +{ + void operator() (AudioUnit au) const { AudioComponentInstanceDispose (au); } +}; + +using AudioUnitUniquePtr = std::unique_ptr, AudioUnitDeleter>; +using AudioUnitSharedPtr = std::shared_ptr>; +using AudioUnitWeakPtr = std::weak_ptr>; + +static std::shared_ptr getARAFactory (AudioUnitSharedPtr audioUnit) +{ + #if JUCE_PLUGINHOST_ARA + jassert (audioUnit != nullptr); + + UInt32 propertySize = sizeof (ARA::ARAAudioUnitFactory); + ARA::ARAAudioUnitFactory audioUnitFactory { ARA::kARAAudioUnitMagic, nullptr }; + + if (hasARAExtension (audioUnit.get())) + { + OSStatus status = AudioUnitGetProperty (audioUnit.get(), + ARA::kAudioUnitProperty_ARAFactory, + kAudioUnitScope_Global, + 0, + &audioUnitFactory, + &propertySize); + + if ((status == noErr) + && (propertySize == sizeof (ARA::ARAAudioUnitFactory)) + && (audioUnitFactory.inOutMagicNumber == ARA::kARAAudioUnitMagic)) + { + jassert (audioUnitFactory.outFactory != nullptr); + return getOrCreateARAFactory (audioUnitFactory.outFactory, + [owningAuPtr = std::move (audioUnit)] (const ARA::ARAFactory*) {}); + } + } + #else + ignoreUnused (audioUnit); + #endif + + return {}; +} + +struct VersionedAudioComponent +{ + AudioComponent audioComponent = nullptr; + bool isAUv3 = false; + + bool operator< (const VersionedAudioComponent& other) const { return audioComponent < other.audioComponent; } +}; + +using AudioUnitCreationCallback = std::function; + +static void createAudioUnit (VersionedAudioComponent versionedComponent, AudioUnitCreationCallback callback) +{ + struct AUAsyncInitializationCallback + { + typedef void (^AUCompletionCallbackBlock)(AudioComponentInstance, OSStatus); + + explicit AUAsyncInitializationCallback (AudioUnitCreationCallback inOriginalCallback) + : originalCallback (std::move (inOriginalCallback)) + { + block = CreateObjCBlock (this, &AUAsyncInitializationCallback::completion); + } + + AUCompletionCallbackBlock getBlock() noexcept { return block; } + + void completion (AudioComponentInstance audioUnit, OSStatus err) + { + originalCallback (audioUnit, err); + + delete this; + } + + double sampleRate; + int framesPerBuffer; + AudioUnitCreationCallback originalCallback; + + ObjCBlock block; + }; + + auto callbackBlock = new AUAsyncInitializationCallback (std::move (callback)); + + if (versionedComponent.isAUv3) + { + if (@available (macOS 10.11, *)) + { + AudioComponentInstantiate (versionedComponent.audioComponent, kAudioComponentInstantiation_LoadOutOfProcess, + callbackBlock->getBlock()); + + return; + } + } + + AudioComponentInstance audioUnit; + auto err = AudioComponentInstanceNew (versionedComponent.audioComponent, &audioUnit); + callbackBlock->completion (err != noErr ? nullptr : audioUnit, err); +} + +struct AudioComponentResult +{ + explicit AudioComponentResult (String error) : errorMessage (std::move (error)) {} + + explicit AudioComponentResult (VersionedAudioComponent auComponent) : component (std::move (auComponent)) {} + + bool isValid() const { return component.audioComponent != nullptr; } + + VersionedAudioComponent component; + String errorMessage; +}; + +static AudioComponentResult getAudioComponent (AudioUnitPluginFormat& format, const PluginDescription& desc) +{ + using namespace AudioUnitFormatHelpers; + + AudioUnitPluginFormat audioUnitPluginFormat; + + if (! format.fileMightContainThisPluginType (desc.fileOrIdentifier)) + return AudioComponentResult { NEEDS_TRANS ("Plug-in description is not an AudioUnit plug-in") }; + + String pluginName, version, manufacturer; + AudioComponentDescription componentDesc; + AudioComponent auComponent; + String errMessage = NEEDS_TRANS ("Cannot find AudioUnit from description"); + + if (! getComponentDescFromIdentifier (desc.fileOrIdentifier, componentDesc, pluginName, version, manufacturer) + && ! getComponentDescFromFile (desc.fileOrIdentifier, componentDesc, pluginName, version, manufacturer)) + { + return AudioComponentResult { errMessage }; + } + + if ((auComponent = AudioComponentFindNext (nullptr, &componentDesc)) == nullptr) + { + return AudioComponentResult { errMessage }; + } + + if (AudioComponentGetDescription (auComponent, &componentDesc) != noErr) + { + return AudioComponentResult { errMessage }; + } + + const bool isAUv3 = AudioUnitFormatHelpers::isPluginAUv3 (componentDesc); + + return AudioComponentResult { { auComponent, isAUv3 } }; +} + +static void getOrCreateARAAudioUnit (VersionedAudioComponent auComponent, std::function callback) +{ + static std::map audioUnitARACache; + + if (auto audioUnit = audioUnitARACache[auComponent].lock()) + { + callback (std::move (audioUnit)); + return; + } + + createAudioUnit (auComponent, [auComponent, cb = std::move (callback)] (AudioUnit audioUnit, OSStatus err) + { + cb ([auComponent, audioUnit, err]() -> AudioUnitSharedPtr + { + if (err != noErr) + return nullptr; + + AudioUnitSharedPtr auPtr { AudioUnitUniquePtr { audioUnit } }; + audioUnitARACache[auComponent] = auPtr; + return auPtr; + }()); + }); +} + //============================================================================== class AudioUnitPluginWindowCocoa; @@ -974,6 +1170,23 @@ public: desc.numInputChannels = getTotalNumInputChannels(); desc.numOutputChannels = getTotalNumOutputChannels(); desc.isInstrument = (componentDesc.componentType == kAudioUnitType_MusicDevice); + + #if JUCE_PLUGINHOST_ARA + desc.hasARAExtension = [&] + { + UInt32 propertySize = sizeof (ARA::ARAAudioUnitFactory); + Boolean isWriteable = FALSE; + + OSStatus status = AudioUnitGetPropertyInfo (audioUnit, + ARA::kAudioUnitProperty_ARAFactory, + kAudioUnitScope_Global, + 0, + &propertySize, + &isWriteable); + + return (status == noErr) && (propertySize == sizeof (ARA::ARAAudioUnitFactory)) && ! isWriteable; + }(); + #endif } void getExtensions (ExtensionsVisitor& visitor) const override @@ -988,6 +1201,33 @@ public: }; visitor.visitAudioUnitClient (Extensions { this }); + + #ifdef JUCE_PLUGINHOST_ARA + struct ARAExtensions : public ExtensionsVisitor::ARAClient + { + explicit ARAExtensions (const AudioUnitPluginInstance* instanceIn) : instance (instanceIn) {} + + void createARAFactoryAsync (std::function cb) const override + { + getOrCreateARAAudioUnit ({ instance->auComponent, instance->isAUv3 }, + [origCb = std::move (cb)] (auto dylibKeepAliveAudioUnit) + { + origCb ([&]() -> ARAFactoryWrapper + { + if (dylibKeepAliveAudioUnit != nullptr) + return ARAFactoryWrapper { ::juce::getARAFactory (std::move (dylibKeepAliveAudioUnit)) }; + + return ARAFactoryWrapper { nullptr }; + }()); + }); + } + + const AudioUnitPluginInstance* instance = nullptr; + }; + + if (hasARAExtension (audioUnit)) + visitor.visitARAClient (ARAExtensions (this)); + #endif } void* getPlatformSpecificData() override { return audioUnit; } @@ -2639,95 +2879,54 @@ void AudioUnitPluginFormat::createPluginInstance (const PluginDescription& desc, double rate, int blockSize, PluginCreationCallback callback) { - using namespace AudioUnitFormatHelpers; + auto auComponentResult = getAudioComponent (*this, desc); - if (fileMightContainThisPluginType (desc.fileOrIdentifier)) + if (! auComponentResult.isValid()) { - String pluginName, version, manufacturer; - AudioComponentDescription componentDesc; - AudioComponent auComponent; - String errMessage = NEEDS_TRANS ("Cannot find AudioUnit from description"); - - if ((! getComponentDescFromIdentifier (desc.fileOrIdentifier, componentDesc, pluginName, version, manufacturer)) - && (! getComponentDescFromFile (desc.fileOrIdentifier, componentDesc, pluginName, version, manufacturer))) - { - callback (nullptr, errMessage); - return; - } - - if ((auComponent = AudioComponentFindNext (nullptr, &componentDesc)) == nullptr) - { - callback (nullptr, errMessage); - return; - } - - if (AudioComponentGetDescription (auComponent, &componentDesc) != noErr) - { - callback (nullptr, errMessage); - return; - } - - struct AUAsyncInitializationCallback - { - typedef void (^AUCompletionCallbackBlock)(AudioComponentInstance, OSStatus); - - AUAsyncInitializationCallback (double inSampleRate, int inFramesPerBuffer, - PluginCreationCallback inOriginalCallback) - : sampleRate (inSampleRate), framesPerBuffer (inFramesPerBuffer), - originalCallback (std::move (inOriginalCallback)) - { - block = CreateObjCBlock (this, &AUAsyncInitializationCallback::completion); - } - - AUCompletionCallbackBlock getBlock() noexcept { return block; } - - void completion (AudioComponentInstance audioUnit, OSStatus err) - { - if (err == noErr) - { - std::unique_ptr instance (new AudioUnitPluginInstance (audioUnit)); + callback (nullptr, std::move (auComponentResult.errorMessage)); + return; + } - if (instance->initialise (sampleRate, framesPerBuffer)) - originalCallback (std::move (instance), {}); - else - originalCallback (nullptr, NEEDS_TRANS ("Unable to initialise the AudioUnit plug-in")); - } - else - { - auto errMsg = TRANS ("An OS error occurred during initialisation of the plug-in (XXX)"); - originalCallback (nullptr, errMsg.replace ("XXX", String (err))); - } + createAudioUnit (auComponentResult.component, + [rate, blockSize, origCallback = std::move (callback)] (AudioUnit audioUnit, OSStatus err) + { + if (err == noErr) + { + auto instance = std::make_unique (audioUnit); - delete this; - } + if (instance->initialise (rate, blockSize)) + origCallback (std::move (instance), {}); + else + origCallback (nullptr, NEEDS_TRANS ("Unable to initialise the AudioUnit plug-in")); + } + else + { + auto errMsg = TRANS ("An OS error occurred during initialisation of the plug-in (XXX)"); + origCallback (nullptr, errMsg.replace ("XXX", String (err))); + } + }); +} - double sampleRate; - int framesPerBuffer; - PluginCreationCallback originalCallback; - ObjCBlock block; - }; +void AudioUnitPluginFormat::createARAFactoryAsync (const PluginDescription& desc, ARAFactoryCreationCallback callback) +{ + auto auComponentResult = getAudioComponent (*this, desc); - auto callbackBlock = new AUAsyncInitializationCallback (rate, blockSize, std::move (callback)); + if (! auComponentResult.isValid()) + { + callback ({ {}, "Failed to create AudioComponent for " + desc.descriptiveName }); + return; + } - if (AudioUnitFormatHelpers::isPluginAUv3 (componentDesc)) - { - if (@available (macOS 10.11, *)) + getOrCreateARAAudioUnit (auComponentResult.component, [cb = std::move (callback)] (auto dylibKeepAliveAudioUnit) + { + cb ([&]() -> ARAFactoryResult { - AudioComponentInstantiate (auComponent, kAudioComponentInstantiation_LoadOutOfProcess, - callbackBlock->getBlock()); - - return; - } - } + if (dylibKeepAliveAudioUnit != nullptr) + return { ARAFactoryWrapper { ::juce::getARAFactory (std::move (dylibKeepAliveAudioUnit)) }, "" }; - AudioComponentInstance audioUnit; - auto err = AudioComponentInstanceNew(auComponent, &audioUnit); - callbackBlock->completion (err != noErr ? nullptr : audioUnit, err); - } - else - { - callback (nullptr, NEEDS_TRANS ("Plug-in description is not an AudioUnit plug-in")); - } + return { {}, "Failed to create ARAFactory from the provided AudioUnit" }; + }()); + }); } bool AudioUnitPluginFormat::requiresUnblockedMessageThreadDuringCreation (const PluginDescription& desc) const diff --git a/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.cpp b/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.cpp index 15c512d072..ca4ff48a47 100644 --- a/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.cpp +++ b/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.cpp @@ -20,6 +20,18 @@ #include "juce_VST3Headers.h" #include "juce_VST3Common.h" +#include "juce_ARACommon.h" + +#if JUCE_PLUGINHOST_ARA && (JUCE_MAC || JUCE_WINDOWS) +#include + +namespace ARA +{ +DEF_CLASS_IID (IMainFactory) +DEF_CLASS_IID (IPlugInEntryPoint) +DEF_CLASS_IID (IPlugInEntryPoint2) +} +#endif namespace juce { @@ -804,6 +816,20 @@ struct DescriptionFactory auto numClasses = factory->countClasses(); + // Every ARA::IMainFactory must have a matching Steinberg::IComponent. + // The match is determined by the two classes having the same name. + std::unordered_set araMainFactoryClassNames; + + #if JUCE_PLUGINHOST_ARA && (JUCE_MAC || JUCE_WINDOWS) + for (Steinberg::int32 i = 0; i < numClasses; ++i) + { + PClassInfo info; + factory->getClassInfo (i, &info); + if (std::strcmp (info.category, kARAMainFactoryClass) == 0) + araMainFactoryClassNames.insert (info.name); + } + #endif + for (Steinberg::int32 i = 0; i < numClasses; ++i) { PClassInfo info; @@ -867,6 +893,9 @@ struct DescriptionFactory } } + if (araMainFactoryClassNames.find (name) != araMainFactoryClassNames.end()) + desc.hasARAExtension = true; + if (desc.uniqueId != 0) result = performOnDescription (desc); @@ -1330,6 +1359,72 @@ private: JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (VST3ModuleHandle) }; +template +static int compareWithString (Type (&charArray)[N], const String& str) +{ + return std::strncmp (str.toRawUTF8(), + charArray, + std::min (str.getNumBytesAsUTF8(), (size_t) numElementsInArray (charArray))); +} + +template +static void forEachARAFactory (IPluginFactory* pluginFactory, Callback&& cb) +{ + #if JUCE_PLUGINHOST_ARA && (JUCE_MAC || JUCE_WINDOWS) + const auto numClasses = pluginFactory->countClasses(); + for (Steinberg::int32 i = 0; i < numClasses; ++i) + { + PClassInfo info; + pluginFactory->getClassInfo (i, &info); + + if (std::strcmp (info.category, kARAMainFactoryClass) == 0) + { + const bool keepGoing = cb (info); + if (! keepGoing) + break; + } + } + #else + ignoreUnused (pluginFactory, cb); + #endif +} + +static std::shared_ptr getARAFactory (Steinberg::IPluginFactory* pluginFactory, const String& pluginName) +{ + std::shared_ptr factory; + + #if JUCE_PLUGINHOST_ARA && (JUCE_MAC || JUCE_WINDOWS) + forEachARAFactory (pluginFactory, + [&pluginFactory, &pluginName, &factory] (const auto& pcClassInfo) + { + if (compareWithString (pcClassInfo.name, pluginName) == 0) + { + ARA::IMainFactory* source; + if (pluginFactory->createInstance (pcClassInfo.cid, ARA::IMainFactory::iid, (void**) &source) + == Steinberg::kResultOk) + { + factory = getOrCreateARAFactory (source->getFactory(), + [source] (const ARA::ARAFactory*) { source->release(); }); + return false; + } + jassert (source == nullptr); + } + + return true; + }); + #else + ignoreUnused (pluginFactory, pluginName); + #endif + + return factory; +} + +static std::shared_ptr getARAFactory (VST3ModuleHandle& module) +{ + auto* pluginFactory = module.getPluginFactory(); + return getARAFactory (pluginFactory, module.getName()); +} + //============================================================================== struct VST3PluginWindow : public AudioProcessorEditor, private ComponentMovementWatcher, @@ -1677,6 +1772,27 @@ private: JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4996) // warning about overriding deprecated methods +//============================================================================== +static bool hasARAExtension (IPluginFactory* pluginFactory, const String& pluginClassName) +{ + bool result = false; + + forEachARAFactory (pluginFactory, + [&pluginClassName, &result] (const auto& pcClassInfo) + { + if (compareWithString (pcClassInfo.name, pluginClassName) == 0) + { + result = true; + + return false; + } + + return true; + }); + + return result; +} + //============================================================================== struct VST3ComponentHolder { @@ -1802,6 +1918,8 @@ struct VST3ComponentHolder totalNumInputChannels, totalNumOutputChannels); + description.hasARAExtension = hasARAExtension (factory, description.name); + return; } @@ -2280,7 +2398,8 @@ public: void getExtensions (ExtensionsVisitor& visitor) const override { - struct Extensions : public ExtensionsVisitor::VST3Client + struct Extensions : public ExtensionsVisitor::VST3Client, + public ExtensionsVisitor::ARAClient { explicit Extensions (const VST3PluginInstance* instanceIn) : instance (instanceIn) {} @@ -2293,10 +2412,21 @@ public: return instance->setStateFromPresetFile (rawData); } + void createARAFactoryAsync (std::function cb) const noexcept override + { + cb (ARAFactoryWrapper { ::juce::getARAFactory (*(instance->holder->module)) }); + } + const VST3PluginInstance* instance = nullptr; }; - visitor.visitVST3Client (Extensions { this }); + Extensions extensions { this }; + visitor.visitVST3Client (extensions); + + if (::juce::getARAFactory (*(holder->module))) + { + visitor.visitARAClient (extensions); + } } void* getPlatformSpecificData() override { return holder->component; } @@ -3107,7 +3237,7 @@ private: if ((paramInfo.flags & Vst::ParameterInfo::kIsBypass) != 0) bypassParam = param; - std::function findOrCreateGroup; + std::function findOrCreateGroup; findOrCreateGroup = [&groupMap, &infoMap, &findOrCreateGroup] (Vst::UnitID groupID) { auto existingGroup = groupMap.find (groupID); @@ -3669,6 +3799,22 @@ void VST3PluginFormat::findAllTypesForFile (OwnedArray& resul } } +void VST3PluginFormat::createARAFactoryAsync (const PluginDescription& description, ARAFactoryCreationCallback callback) +{ + if (! description.hasARAExtension) + { + jassertfalse; + callback ({ {}, "The provided plugin does not support ARA features" }); + } + + File file (description.fileOrIdentifier); + VSTComSmartPtr pluginFactory ( + DLLHandleCache::getInstance()->findOrCreateHandle (file.getFullPathName()).getPluginFactory()); + const auto* pluginName = description.name.toRawUTF8(); + + callback ({ ARAFactoryWrapper { ::juce::getARAFactory (pluginFactory, pluginName) }, {} }); +} + void VST3PluginFormat::createPluginInstance (const PluginDescription& description, double, int, PluginCreationCallback callback) { diff --git a/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.h b/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.h index 0a294ce8df..b766a1a31d 100644 --- a/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.h +++ b/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.h @@ -61,6 +61,7 @@ public: StringArray searchPathsForPlugins (const FileSearchPath&, bool recursive, bool) override; bool doesPluginStillExist (const PluginDescription&) override; FileSearchPath getDefaultLocationsToSearch() override; + void createARAFactoryAsync (const PluginDescription&, ARAFactoryCreationCallback callback) override; private: //============================================================================== diff --git a/modules/juce_audio_processors/juce_audio_processors.cpp b/modules/juce_audio_processors/juce_audio_processors.cpp index 98abe6c2b7..ce2036783e 100644 --- a/modules/juce_audio_processors/juce_audio_processors.cpp +++ b/modules/juce_audio_processors/juce_audio_processors.cpp @@ -199,10 +199,12 @@ private: #include "processors/juce_AudioProcessorGraph.cpp" #include "processors/juce_GenericAudioProcessorEditor.cpp" #include "processors/juce_PluginDescription.cpp" +#include "format_types/juce_ARACommon.cpp" #include "format_types/juce_LADSPAPluginFormat.cpp" #include "format_types/juce_VSTPluginFormat.cpp" #include "format_types/juce_VST3PluginFormat.cpp" #include "format_types/juce_AudioUnitPluginFormat.mm" +#include "format_types/juce_ARAHosting.cpp" #include "scanning/juce_KnownPluginList.cpp" #include "scanning/juce_PluginDirectoryScanner.cpp" #include "scanning/juce_PluginListComponent.cpp" diff --git a/modules/juce_audio_processors/juce_audio_processors.h b/modules/juce_audio_processors/juce_audio_processors.h index 101a659e94..99c107ae8d 100644 --- a/modules/juce_audio_processors/juce_audio_processors.h +++ b/modules/juce_audio_processors/juce_audio_processors.h @@ -94,6 +94,17 @@ #define JUCE_PLUGINHOST_LV2 0 #endif +/** Config: JUCE_PLUGINHOST_ARA + Enables the ARA plugin extension hosting classes. You will need to download the ARA SDK and specify the + path to it either in the Projucer, using juce_set_ara_sdk_path() in your CMake project file. + + The directory can be obtained by recursively cloning https://github.com/Celemony/ARA_SDK and checking out + the tag releases/2.1.0. +*/ +#ifndef JUCE_PLUGINHOST_ARA + #define JUCE_PLUGINHOST_ARA 0 +#endif + /** Config: JUCE_CUSTOM_VST3_SDK If enabled, the embedded VST3 SDK in JUCE will not be added to the project and instead you should add the path to your custom VST3 SDK to the project's header search paths. Most users shouldn't @@ -115,6 +126,7 @@ #include "utilities/juce_VSTCallbackHandler.h" #include "utilities/juce_VST3ClientExtensions.h" #include "utilities/juce_NativeScaleFactorNotifier.h" +#include "format_types/juce_ARACommon.h" #include "utilities/juce_ExtensionsVisitor.h" #include "processors/juce_AudioProcessorParameter.h" #include "processors/juce_HostedAudioProcessorParameter.h" @@ -136,6 +148,7 @@ #include "format_types/juce_VST3PluginFormat.h" #include "format_types/juce_VSTMidiEventList.h" #include "format_types/juce_VSTPluginFormat.h" +#include "format_types/juce_ARAHosting.h" #include "scanning/juce_PluginDirectoryScanner.h" #include "scanning/juce_PluginListComponent.h" #include "utilities/juce_AudioProcessorParameterWithID.h" diff --git a/modules/juce_audio_processors/juce_audio_processors_ara.cpp b/modules/juce_audio_processors/juce_audio_processors_ara.cpp new file mode 100644 index 0000000000..8f16f9beeb --- /dev/null +++ b/modules/juce_audio_processors/juce_audio_processors_ara.cpp @@ -0,0 +1,33 @@ +/* + ============================================================================== + + This file is part of the JUCE 7 technical preview. + Copyright (c) 2022 - Raw Material Software Limited + + You may use this code under the terms of the GPL v3 + (see www.gnu.org/licenses). + + For the technical preview this file cannot be licensed commercially. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include +#include + +/* Having WIN32_LEAN_AND_MEAN defined at the point of including ARADebug.c will produce warnings. + + To prevent such problems it's easiest to have it in its own translation unit. +*/ + +#if (JUCE_PLUGINHOST_ARA && (JUCE_PLUGINHOST_VST3 || JUCE_PLUGINHOST_AU) && (JUCE_MAC || JUCE_WINDOWS)) + +JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wgnu-zero-variadic-macro-arguments", "-Wmissing-prototypes") + #include +JUCE_END_IGNORE_WARNINGS_GCC_LIKE + +#endif diff --git a/modules/juce_audio_processors/processors/juce_PluginDescription.cpp b/modules/juce_audio_processors/processors/juce_PluginDescription.cpp index b63391d465..3d55eebb83 100644 --- a/modules/juce_audio_processors/processors/juce_PluginDescription.cpp +++ b/modules/juce_audio_processors/processors/juce_PluginDescription.cpp @@ -72,6 +72,7 @@ std::unique_ptr PluginDescription::createXml() const e->setAttribute ("numInputs", numInputChannels); e->setAttribute ("numOutputs", numOutputChannels); e->setAttribute ("isShell", hasSharedContainer); + e->setAttribute ("hasARAExtension", hasARAExtension); e->setAttribute ("uid", String::toHexString (deprecatedUid)); @@ -95,6 +96,7 @@ bool PluginDescription::loadFromXml (const XmlElement& xml) numInputChannels = xml.getIntAttribute ("numInputs"); numOutputChannels = xml.getIntAttribute ("numOutputs"); hasSharedContainer = xml.getBoolAttribute ("isShell", false); + hasARAExtension = xml.getBoolAttribute ("hasARAExtension", false); deprecatedUid = xml.getStringAttribute ("uid").getHexValue32(); uniqueId = xml.getStringAttribute ("uniqueId", "0").getHexValue32(); diff --git a/modules/juce_audio_processors/processors/juce_PluginDescription.h b/modules/juce_audio_processors/processors/juce_PluginDescription.h index 1afa932f8f..32e38de39b 100644 --- a/modules/juce_audio_processors/processors/juce_PluginDescription.h +++ b/modules/juce_audio_processors/processors/juce_PluginDescription.h @@ -122,6 +122,9 @@ public: /** True if the plug-in is part of a multi-type container, e.g. a VST Shell. */ bool hasSharedContainer = false; + /** True if the plug-in is ARA enabled and can supply a valid ARAFactoryWrapper. */ + bool hasARAExtension = false; + /** Returns true if the two descriptions refer to the same plug-in. This isn't quite as simple as them just having the same file (because of diff --git a/modules/juce_audio_processors/utilities/juce_ExtensionsVisitor.h b/modules/juce_audio_processors/utilities/juce_ExtensionsVisitor.h index 260669c8d2..40ea9c44f3 100644 --- a/modules/juce_audio_processors/utilities/juce_ExtensionsVisitor.h +++ b/modules/juce_audio_processors/utilities/juce_ExtensionsVisitor.h @@ -106,6 +106,13 @@ struct ExtensionsVisitor virtual AEffect* getAEffectPtr() const noexcept = 0; }; + /** Can be used to retrieve information about a plugin that provides ARA extensions. */ + struct ARAClient + { + virtual ~ARAClient() = default; + virtual void createARAFactoryAsync (std::function) const = 0; + }; + virtual ~ExtensionsVisitor() = default; /** Will be called if there is no platform specific information available. */ @@ -119,6 +126,9 @@ struct ExtensionsVisitor /** Called with AU-specific information. */ virtual void visitAudioUnitClient (const AudioUnitClient&) {} + + /** Called with ARA-specific information. */ + virtual void visitARAClient (const ARAClient&) {} }; } // namespace juce