|
- /*
- ==============================================================================
-
- This file is part of the JUCE examples.
- Copyright (c) 2022 - Raw Material Software Limited
-
- The code included in this file is provided under the terms of the ISC license
- http://www.isc.org/downloads/software-support-policy/isc-license. Permission
- To use, copy, modify, and/or distribute this software for any purpose with or
- without fee is hereby granted provided that the above copyright notice and
- this permission notice appear in all copies.
-
- THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
- WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
- PURPOSE, ARE DISCLAIMED.
-
- ==============================================================================
- */
-
- /*******************************************************************************
- The block below describes the properties of this PIP. A PIP is a short snippet
- of code that can be read by the Projucer and used to generate a JUCE project.
-
- BEGIN_JUCE_PIP_METADATA
-
- name: ARAPluginDemo
- version: 1.0.0
- vendor: JUCE
- website: http://juce.com
- description: Audio plugin using the ARA API.
-
- dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats,
- juce_audio_plugin_client, juce_audio_processors,
- juce_audio_utils, juce_core, juce_data_structures,
- juce_events, juce_graphics, juce_gui_basics, juce_gui_extra
- exporters: xcode_mac, vs2022
-
- moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
-
- type: AudioProcessor
- mainClass: ARADemoPluginAudioProcessor
- documentControllerClass: ARADemoPluginDocumentControllerSpecialisation
-
- useLocalCopy: 1
-
- END_JUCE_PIP_METADATA
-
- *******************************************************************************/
-
- #pragma once
-
-
- //==============================================================================
- struct PreviewState
- {
- std::atomic<double> previewTime { 0.0 };
- std::atomic<ARAPlaybackRegion*> previewedRegion { nullptr };
- };
-
- class SharedTimeSliceThread : public TimeSliceThread
- {
- public:
- SharedTimeSliceThread()
- : TimeSliceThread (String (JucePlugin_Name) + " ARA Sample Reading Thread")
- {
- startThread (7); // Above default priority so playback is fluent, but below realtime
- }
- };
-
- class AsyncConfigurationCallback : private AsyncUpdater
- {
- public:
- explicit AsyncConfigurationCallback (std::function<void()> callbackIn)
- : callback (std::move (callbackIn)) {}
-
- ~AsyncConfigurationCallback() override { cancelPendingUpdate(); }
-
- template <typename RequiresLock>
- auto withLock (RequiresLock&& fn)
- {
- const SpinLock::ScopedTryLockType scope (processingFlag);
- return fn (scope.isLocked());
- }
-
- void startConfigure() { triggerAsyncUpdate(); }
-
- private:
- void handleAsyncUpdate() override
- {
- const SpinLock::ScopedLockType scope (processingFlag);
- callback();
- }
-
- std::function<void()> callback;
- SpinLock processingFlag;
- };
-
- class Looper
- {
- public:
- Looper() : inputBuffer (nullptr), pos (loopRange.getStart())
- {
- }
-
- Looper (const AudioBuffer<float>* buffer, Range<int64> range)
- : inputBuffer (buffer), loopRange (range), pos (range.getStart())
- {
- }
-
- void writeInto (AudioBuffer<float>& buffer)
- {
- if (loopRange.getLength() == 0)
- buffer.clear();
-
- const auto numChannelsToCopy = std::min (inputBuffer->getNumChannels(), buffer.getNumChannels());
-
- for (auto samplesCopied = 0; samplesCopied < buffer.getNumSamples();)
- {
- const auto numSamplesToCopy =
- std::min (buffer.getNumSamples() - samplesCopied, (int) (loopRange.getEnd() - pos));
-
- for (int i = 0; i < numChannelsToCopy; ++i)
- {
- buffer.copyFrom (i, samplesCopied, *inputBuffer, i, (int) pos, numSamplesToCopy);
- }
-
- samplesCopied += numSamplesToCopy;
- pos += numSamplesToCopy;
-
- jassert (pos <= loopRange.getEnd());
-
- if (pos == loopRange.getEnd())
- pos = loopRange.getStart();
- }
- }
-
- private:
- const AudioBuffer<float>* inputBuffer;
- Range<int64> loopRange;
- int64 pos;
- };
-
- class OptionalRange
- {
- public:
- using Type = Range<int64>;
-
- OptionalRange() : valid (false) {}
- explicit OptionalRange (Type valueIn) : valid (true), value (std::move (valueIn)) {}
-
- explicit operator bool() const noexcept { return valid; }
-
- const auto& operator*() const
- {
- jassert (valid);
- return value;
- }
-
- private:
- bool valid;
- Type value;
- };
-
- //==============================================================================
- // Returns the modified sample range in the output buffer.
- inline OptionalRange readPlaybackRangeIntoBuffer (Range<double> playbackRange,
- const ARAPlaybackRegion* playbackRegion,
- AudioBuffer<float>& buffer,
- const std::function<AudioFormatReader* (ARA::PlugIn::AudioSource*)>& getReader)
- {
- const auto rangeInAudioModificationTime = playbackRange.movedToStartAt (playbackRange.getStart()
- - playbackRegion->getStartInAudioModificationTime());
-
- const auto audioSource = playbackRegion->getAudioModification()->getAudioSource();
- const auto audioModificationSampleRate = audioSource->getSampleRate();
-
- const Range<int64_t> sampleRangeInAudioModification {
- ARA::roundSamplePosition (rangeInAudioModificationTime.getStart() * audioModificationSampleRate),
- ARA::roundSamplePosition (rangeInAudioModificationTime.getEnd() * audioModificationSampleRate) - 1
- };
-
- const auto inputOffset = jlimit ((int64_t) 0, audioSource->getSampleCount(), sampleRangeInAudioModification.getStart());
-
- const auto outputOffset = -std::min (sampleRangeInAudioModification.getStart(), (int64_t) 0);
-
- /* TODO: Handle different AudioSource and playback sample rates.
-
- The conversion should be done inside a specialized AudioFormatReader so that we could use
- playbackSampleRate everywhere in this function and we could still read `readLength` number of samples
- from the source.
-
- The current implementation will be incorrect when sampling rates differ.
- */
- const auto readLength = [&]
- {
- const auto sourceReadLength =
- std::min (sampleRangeInAudioModification.getEnd(), audioSource->getSampleCount()) - inputOffset;
-
- const auto outputReadLength =
- std::min (outputOffset + sourceReadLength, (int64_t) buffer.getNumSamples()) - outputOffset;
-
- return std::min (sourceReadLength, outputReadLength);
- }();
-
- if (readLength == 0)
- return OptionalRange { {} };
-
- auto* reader = getReader (audioSource);
-
- if (reader != nullptr && reader->read (&buffer, (int) outputOffset, (int) readLength, inputOffset, true, true))
- return OptionalRange { { outputOffset, readLength } };
-
- return {};
- }
-
- class PossiblyBufferedReader
- {
- public:
- PossiblyBufferedReader() = default;
-
- explicit PossiblyBufferedReader (std::unique_ptr<BufferingAudioReader> readerIn)
- : setTimeoutFn ([ptr = readerIn.get()] (int ms) { ptr->setReadTimeout (ms); }),
- reader (std::move (readerIn))
- {}
-
- explicit PossiblyBufferedReader (std::unique_ptr<AudioFormatReader> readerIn)
- : setTimeoutFn(),
- reader (std::move (readerIn))
- {}
-
- void setReadTimeout (int ms)
- {
- NullCheckedInvocation::invoke (setTimeoutFn, ms);
- }
-
- AudioFormatReader* get() const { return reader.get(); }
-
- private:
- std::function<void (int)> setTimeoutFn;
- std::unique_ptr<AudioFormatReader> reader;
- };
-
- //==============================================================================
- class PlaybackRenderer : public ARAPlaybackRenderer
- {
- public:
- using ARAPlaybackRenderer::ARAPlaybackRenderer;
-
- void prepareToPlay (double sampleRateIn,
- int maximumSamplesPerBlockIn,
- int numChannelsIn,
- AudioProcessor::ProcessingPrecision,
- AlwaysNonRealtime alwaysNonRealtime) override
- {
- numChannels = numChannelsIn;
- sampleRate = sampleRateIn;
- maximumSamplesPerBlock = maximumSamplesPerBlockIn;
- tempBuffer.reset (new AudioBuffer<float> (numChannels, maximumSamplesPerBlock));
-
- useBufferedAudioSourceReader = alwaysNonRealtime == AlwaysNonRealtime::no;
-
- for (const auto playbackRegion : getPlaybackRegions())
- {
- auto audioSource = playbackRegion->getAudioModification()->getAudioSource();
-
- if (audioSourceReaders.find (audioSource) == audioSourceReaders.end())
- {
- auto reader = std::make_unique<ARAAudioSourceReader> (audioSource);
-
- if (! useBufferedAudioSourceReader)
- {
- audioSourceReaders.emplace (audioSource,
- PossiblyBufferedReader { std::move (reader) });
- }
- else
- {
- const auto readAheadSize = jmax (4 * maximumSamplesPerBlock,
- roundToInt (2.0 * sampleRate));
- audioSourceReaders.emplace (audioSource,
- PossiblyBufferedReader { std::make_unique<BufferingAudioReader> (reader.release(),
- *sharedTimesliceThread,
- readAheadSize) });
- }
- }
- }
- }
-
- void releaseResources() override
- {
- audioSourceReaders.clear();
- tempBuffer.reset();
- }
-
- bool processBlock (AudioBuffer<float>& buffer,
- AudioProcessor::Realtime realtime,
- const AudioPlayHead::PositionInfo& positionInfo) noexcept override
- {
- const auto numSamples = buffer.getNumSamples();
- jassert (numSamples <= maximumSamplesPerBlock);
- jassert (numChannels == buffer.getNumChannels());
- jassert (realtime == AudioProcessor::Realtime::no || useBufferedAudioSourceReader);
- const auto timeInSamples = positionInfo.getTimeInSamples().orFallback (0);
- const auto isPlaying = positionInfo.getIsPlaying();
-
- bool success = true;
- bool didRenderAnyRegion = false;
-
- if (isPlaying)
- {
- const auto blockRange = Range<int64>::withStartAndLength (timeInSamples, numSamples);
-
- for (const auto& playbackRegion : getPlaybackRegions())
- {
- // Evaluate region borders in song time, calculate sample range to render in song time.
- // Note that this example does not use head- or tailtime, so the includeHeadAndTail
- // parameter is set to false here - this might need to be adjusted in actual plug-ins.
- const auto playbackSampleRange = playbackRegion->getSampleRange (sampleRate, ARAPlaybackRegion::IncludeHeadAndTail::no);
- auto renderRange = blockRange.getIntersectionWith (playbackSampleRange);
-
- if (renderRange.isEmpty())
- continue;
-
- // Evaluate region borders in modification/source time and calculate offset between
- // song and source samples, then clip song samples accordingly
- // (if an actual plug-in supports time stretching, this must be taken into account here).
- Range<int64> modificationSampleRange { playbackRegion->getStartInAudioModificationSamples(),
- playbackRegion->getEndInAudioModificationSamples() };
- const auto modificationSampleOffset = modificationSampleRange.getStart() - playbackSampleRange.getStart();
-
- renderRange = renderRange.getIntersectionWith (modificationSampleRange.movedToStartAt (playbackSampleRange.getStart()));
-
- if (renderRange.isEmpty())
- continue;
-
- // Get the audio source for the region and find the reader for that source.
- // This simplified example code only produces audio if sample rate and channel count match -
- // a robust plug-in would need to do conversion, see ARA SDK documentation.
- const auto audioSource = playbackRegion->getAudioModification()->getAudioSource();
- const auto readerIt = audioSourceReaders.find (audioSource);
-
- if (std::make_tuple (audioSource->getChannelCount(), audioSource->getSampleRate()) != std::make_tuple (numChannels, sampleRate)
- || (readerIt == audioSourceReaders.end()))
- {
- success = false;
- continue;
- }
-
- auto& reader = readerIt->second;
- reader.setReadTimeout (realtime == AudioProcessor::Realtime::no ? 100 : 0);
-
- // Calculate buffer offsets.
- const int numSamplesToRead = (int) renderRange.getLength();
- const int startInBuffer = (int) (renderRange.getStart() - blockRange.getStart());
- auto startInSource = renderRange.getStart() + modificationSampleOffset;
-
- // Read samples:
- // first region can write directly into output, later regions need to use local buffer.
- auto& readBuffer = (didRenderAnyRegion) ? *tempBuffer : buffer;
-
- if (! reader.get()->read (&readBuffer, startInBuffer, numSamplesToRead, startInSource, true, true))
- {
- success = false;
- continue;
- }
-
- if (didRenderAnyRegion)
- {
- // Mix local buffer into the output buffer.
- for (int c = 0; c < numChannels; ++c)
- buffer.addFrom (c, startInBuffer, *tempBuffer, c, startInBuffer, numSamplesToRead);
- }
- else
- {
- // Clear any excess at start or end of the region.
- if (startInBuffer != 0)
- buffer.clear (0, startInBuffer);
-
- const int endInBuffer = startInBuffer + numSamplesToRead;
- const int remainingSamples = numSamples - endInBuffer;
-
- if (remainingSamples != 0)
- buffer.clear (endInBuffer, remainingSamples);
-
- didRenderAnyRegion = true;
- }
- }
- }
-
- // If no playback or no region did intersect, clear buffer now.
- if (! didRenderAnyRegion)
- buffer.clear();
-
- return success;
- }
-
- private:
- //==============================================================================
- // We're subclassing here only to provide a proper default c'tor for our shared resource
-
- SharedResourcePointer<SharedTimeSliceThread> sharedTimesliceThread;
- std::map<ARA::PlugIn::AudioSource*, PossiblyBufferedReader> audioSourceReaders;
- bool useBufferedAudioSourceReader = true;
- int numChannels = 2;
- double sampleRate = 48000.0;
- int maximumSamplesPerBlock = 128;
- std::unique_ptr<AudioBuffer<float>> tempBuffer;
- };
-
- class EditorRenderer : public ARAEditorRenderer,
- private ARARegionSequence::Listener
- {
- public:
- EditorRenderer (ARA::PlugIn::DocumentController* documentController, const PreviewState* previewStateIn)
- : ARAEditorRenderer (documentController), previewState (previewStateIn), previewBuffer()
- {
- jassert (previewState != nullptr);
- }
-
- ~EditorRenderer() override
- {
- for (const auto& rs : regionSequences)
- rs->removeListener (this);
- }
-
- void didAddPlaybackRegionToRegionSequence (ARARegionSequence*, ARAPlaybackRegion*) override
- {
- asyncConfigCallback.startConfigure();
- }
-
- void didAddRegionSequence (ARA::PlugIn::RegionSequence* rs) noexcept override
- {
- auto* sequence = dynamic_cast<ARARegionSequence*> (rs);
- sequence->addListener (this);
- regionSequences.insert (sequence);
- asyncConfigCallback.startConfigure();
- }
-
- void didAddPlaybackRegion (ARA::PlugIn::PlaybackRegion*) noexcept override
- {
- asyncConfigCallback.startConfigure();
- }
-
- /* An ARA host could be using either the `addPlaybackRegion()` or `addRegionSequence()` interface
- so we need to check the other side of both.
-
- The callback must have a signature of `bool (ARAPlaybackRegion*)`
- */
- template <typename Callback>
- void forEachPlaybackRegion (Callback&& cb)
- {
- for (const auto& playbackRegion : getPlaybackRegions())
- if (! cb (playbackRegion))
- return;
-
- for (const auto& regionSequence : getRegionSequences())
- for (const auto& playbackRegion : regionSequence->getPlaybackRegions())
- if (! cb (playbackRegion))
- return;
- }
-
- void prepareToPlay (double sampleRateIn,
- int maximumExpectedSamplesPerBlock,
- int numChannels,
- AudioProcessor::ProcessingPrecision,
- AlwaysNonRealtime alwaysNonRealtime) override
- {
- sampleRate = sampleRateIn;
- previewBuffer = std::make_unique<AudioBuffer<float>> (numChannels, (int) (2 * sampleRateIn));
-
- ignoreUnused (maximumExpectedSamplesPerBlock, alwaysNonRealtime);
- }
-
- void releaseResources() override
- {
- audioSourceReaders.clear();
- }
-
- void reset() override
- {
- previewBuffer->clear();
- }
-
- bool processBlock (AudioBuffer<float>& buffer,
- AudioProcessor::Realtime realtime,
- const AudioPlayHead::PositionInfo& positionInfo) noexcept override
- {
- ignoreUnused (realtime);
-
- return asyncConfigCallback.withLock ([&] (bool locked)
- {
- if (! locked)
- return true;
-
- if (positionInfo.getIsPlaying())
- return true;
-
- if (const auto previewedRegion = previewState->previewedRegion.load())
- {
- const auto regionIsAssignedToEditor = [&]()
- {
- bool regionIsAssigned = false;
-
- forEachPlaybackRegion ([&previewedRegion, ®ionIsAssigned] (const auto& region)
- {
- if (region == previewedRegion)
- {
- regionIsAssigned = true;
- return false;
- }
-
- return true;
- });
-
- return regionIsAssigned;
- }();
-
- if (regionIsAssignedToEditor)
- {
- const auto previewTime = previewState->previewTime.load();
-
- if (lastPreviewTime != previewTime || lastPlaybackRegion != previewedRegion)
- {
- Range<double> previewRangeInPlaybackTime { previewTime - 0.25, previewTime + 0.25 };
- previewBuffer->clear();
- const auto rangeInOutput = readPlaybackRangeIntoBuffer (previewRangeInPlaybackTime,
- previewedRegion,
- *previewBuffer,
- [this] (auto* source) -> auto*
- {
- const auto iter = audioSourceReaders.find (source);
- return iter != audioSourceReaders.end() ? iter->second.get() : nullptr;
- });
-
- if (rangeInOutput)
- {
- lastPreviewTime = previewTime;
- lastPlaybackRegion = previewedRegion;
- previewLooper = Looper (previewBuffer.get(), *rangeInOutput);
- }
- }
- else
- {
- previewLooper.writeInto (buffer);
- }
- }
- }
-
- return true;
- });
- }
-
- private:
- void configure()
- {
- forEachPlaybackRegion ([this, maximumExpectedSamplesPerBlock = 1000] (const auto& playbackRegion)
- {
- const auto audioSource = playbackRegion->getAudioModification()->getAudioSource();
-
- if (audioSourceReaders.find (audioSource) == audioSourceReaders.end())
- {
- audioSourceReaders[audioSource] = std::make_unique<BufferingAudioReader> (
- new ARAAudioSourceReader (playbackRegion->getAudioModification()->getAudioSource()),
- *timeSliceThread,
- std::max (4 * maximumExpectedSamplesPerBlock, (int) sampleRate));
- }
-
- return true;
- });
- }
-
- const PreviewState* previewState = nullptr;
- AsyncConfigurationCallback asyncConfigCallback { [this] { configure(); } };
- double lastPreviewTime = 0.0;
- ARAPlaybackRegion* lastPlaybackRegion = nullptr;
- std::unique_ptr<AudioBuffer<float>> previewBuffer;
- Looper previewLooper;
-
- double sampleRate = 48000.0;
- SharedResourcePointer<SharedTimeSliceThread> timeSliceThread;
- std::map<ARA::PlugIn::AudioSource*, std::unique_ptr<BufferingAudioReader>> audioSourceReaders;
-
- std::set<ARARegionSequence*> regionSequences;
- };
-
- //==============================================================================
- class ARADemoPluginDocumentControllerSpecialisation : public ARADocumentControllerSpecialisation
- {
- public:
- using ARADocumentControllerSpecialisation::ARADocumentControllerSpecialisation;
-
- PreviewState previewState;
-
- protected:
- ARAPlaybackRenderer* doCreatePlaybackRenderer() noexcept override
- {
- return new PlaybackRenderer (getDocumentController());
- }
-
- EditorRenderer* doCreateEditorRenderer() noexcept override
- {
- return new EditorRenderer (getDocumentController(), &previewState);
- }
-
- bool doRestoreObjectsFromStream (ARAInputStream& input,
- const ARARestoreObjectsFilter* filter) noexcept override
- {
- ignoreUnused (input, filter);
- return false;
- }
-
- bool doStoreObjectsToStream (ARAOutputStream& output, const ARAStoreObjectsFilter* filter) noexcept override
- {
- ignoreUnused (output, filter);
- return false;
- }
- };
-
- struct PlayHeadState
- {
- void update (AudioPlayHead* aph)
- {
- const auto info = aph->getPosition();
-
- if (info.hasValue() && info->getIsPlaying())
- {
- isPlaying.store (true);
- timeInSeconds.store (info->getTimeInSeconds().orFallback (0));
- }
- else
- {
- isPlaying.store (false);
- }
- }
-
- std::atomic<bool> isPlaying { false };
- std::atomic<double> timeInSeconds { 0.0 };
- };
-
- //==============================================================================
- class ARADemoPluginAudioProcessorImpl : public AudioProcessor,
- public AudioProcessorARAExtension
- {
- public:
- //==============================================================================
- ARADemoPluginAudioProcessorImpl()
- : AudioProcessor (getBusesProperties())
- {}
-
- ~ARADemoPluginAudioProcessorImpl() override = default;
-
- //==============================================================================
- void prepareToPlay (double sampleRate, int samplesPerBlock) override
- {
- playHeadState.isPlaying.store (false);
- prepareToPlayForARA (sampleRate, samplesPerBlock, getMainBusNumOutputChannels(), getProcessingPrecision());
- }
-
- void releaseResources() override
- {
- playHeadState.isPlaying.store (false);
- releaseResourcesForARA();
- }
-
- bool isBusesLayoutSupported (const BusesLayout& layouts) const override
- {
- if (layouts.getMainOutputChannelSet() != AudioChannelSet::mono()
- && layouts.getMainOutputChannelSet() != AudioChannelSet::stereo())
- return false;
-
- return true;
- }
-
- void processBlock (AudioBuffer<float>& buffer, MidiBuffer& midiMessages) override
- {
- ignoreUnused (midiMessages);
-
- ScopedNoDenormals noDenormals;
-
- auto* audioPlayHead = getPlayHead();
- playHeadState.update (audioPlayHead);
-
- if (! processBlockForARA (buffer, isRealtime(), audioPlayHead))
- processBlockBypassed (buffer, midiMessages);
- }
-
- //==============================================================================
- const String getName() const override { return "ARAPluginDemo"; }
- bool acceptsMidi() const override { return true; }
- bool producesMidi() const override { return true; }
- double getTailLengthSeconds() const override { return 0.0; }
-
- //==============================================================================
- int getNumPrograms() override { return 0; }
- int getCurrentProgram() override { return 0; }
- void setCurrentProgram (int) override {}
- const String getProgramName (int) override { return "None"; }
- void changeProgramName (int, const String&) override {}
-
- //==============================================================================
- void getStateInformation (MemoryBlock&) override {}
- void setStateInformation (const void*, int) override {}
-
- PlayHeadState playHeadState;
-
- private:
- //==============================================================================
- static BusesProperties getBusesProperties()
- {
- return BusesProperties().withInput ("Input", AudioChannelSet::stereo(), true)
- .withOutput ("Output", AudioChannelSet::stereo(), true);
- }
-
- JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ARADemoPluginAudioProcessorImpl)
- };
-
- //==============================================================================
- struct WaveformCache : private ARAAudioSource::Listener
- {
- WaveformCache() : thumbnailCache (20)
- {
- }
-
- ~WaveformCache() override
- {
- for (const auto& entry : thumbnails)
- {
- entry.first->removeListener (this);
- }
- }
-
- //==============================================================================
- void willDestroyAudioSource (ARAAudioSource* audioSource) override
- {
- removeAudioSource (audioSource);
- }
-
- AudioThumbnail& getOrCreateThumbnail (ARAAudioSource* audioSource)
- {
- const auto iter = thumbnails.find (audioSource);
-
- if (iter != std::end (thumbnails))
- return *iter->second;
-
- auto thumb = std::make_unique<AudioThumbnail> (128, dummyManager, thumbnailCache);
- auto& result = *thumb;
-
- ++hash;
- thumb->setReader (new ARAAudioSourceReader (audioSource), hash);
-
- audioSource->addListener (this);
- thumbnails.emplace (audioSource, std::move (thumb));
- return result;
- }
-
- private:
- void removeAudioSource (ARAAudioSource* audioSource)
- {
- audioSource->removeListener (this);
- thumbnails.erase (audioSource);
- }
-
- int64 hash = 0;
- AudioFormatManager dummyManager;
- AudioThumbnailCache thumbnailCache;
- std::map<ARAAudioSource*, std::unique_ptr<AudioThumbnail>> thumbnails;
- };
-
- class PlaybackRegionView : public Component,
- public ChangeListener
- {
- public:
- PlaybackRegionView (ARAPlaybackRegion& region, WaveformCache& cache)
- : playbackRegion (region), waveformCache (cache)
- {
- auto* audioSource = playbackRegion.getAudioModification()->getAudioSource();
-
- waveformCache.getOrCreateThumbnail (audioSource).addChangeListener (this);
- }
-
- ~PlaybackRegionView() override
- {
- waveformCache.getOrCreateThumbnail (playbackRegion.getAudioModification()->getAudioSource())
- .removeChangeListener (this);
- }
-
- void mouseDown (const MouseEvent& m) override
- {
- const auto relativeTime = (double) m.getMouseDownX() / getLocalBounds().getWidth();
- const auto previewTime = playbackRegion.getStartInPlaybackTime()
- + relativeTime * playbackRegion.getDurationInPlaybackTime();
- auto& previewState = ARADocumentControllerSpecialisation::getSpecialisedDocumentController<ARADemoPluginDocumentControllerSpecialisation> (playbackRegion.getDocumentController())->previewState;
- previewState.previewTime.store (previewTime);
- previewState.previewedRegion.store (&playbackRegion);
- }
-
- void mouseUp (const MouseEvent&) override
- {
- auto& previewState = ARADocumentControllerSpecialisation::getSpecialisedDocumentController<ARADemoPluginDocumentControllerSpecialisation> (playbackRegion.getDocumentController())->previewState;
- previewState.previewTime.store (0.0);
- previewState.previewedRegion.store (nullptr);
- }
-
- void changeListenerCallback (ChangeBroadcaster*) override
- {
- repaint();
- }
-
- void paint (Graphics& g) override
- {
- g.fillAll (Colours::white.darker());
- g.setColour (Colours::darkgrey.darker());
- auto& thumbnail = waveformCache.getOrCreateThumbnail (playbackRegion.getAudioModification()->getAudioSource());
- thumbnail.drawChannels (g,
- getLocalBounds(),
- playbackRegion.getStartInAudioModificationTime(),
- playbackRegion.getEndInAudioModificationTime(),
- 1.0f);
- g.setColour (Colours::black);
- g.drawRect (getLocalBounds());
- }
-
- void resized() override
- {
- repaint();
- }
-
- private:
- ARAPlaybackRegion& playbackRegion;
- WaveformCache& waveformCache;
- };
-
- class RegionSequenceView : public Component,
- public ARARegionSequence::Listener,
- public ChangeBroadcaster,
- private ARAPlaybackRegion::Listener
- {
- public:
- RegionSequenceView (ARARegionSequence& rs, WaveformCache& cache, double pixelPerSec)
- : regionSequence (rs), waveformCache (cache), zoomLevelPixelPerSecond (pixelPerSec)
- {
- regionSequence.addListener (this);
-
- for (auto* playbackRegion : regionSequence.getPlaybackRegions())
- createAndAddPlaybackRegionView (playbackRegion);
-
- updatePlaybackDuration();
- }
-
- ~RegionSequenceView() override
- {
- regionSequence.removeListener (this);
-
- for (const auto& it : playbackRegionViews)
- it.first->removeListener (this);
- }
-
- //==============================================================================
- // ARA Document change callback overrides
- void willRemovePlaybackRegionFromRegionSequence (ARARegionSequence*,
- ARAPlaybackRegion* playbackRegion) override
- {
- playbackRegion->removeListener (this);
- removeChildComponent (playbackRegionViews[playbackRegion].get());
- playbackRegionViews.erase (playbackRegion);
- updatePlaybackDuration();
- }
-
- void didAddPlaybackRegionToRegionSequence (ARARegionSequence*, ARAPlaybackRegion* playbackRegion) override
- {
- createAndAddPlaybackRegionView (playbackRegion);
- updatePlaybackDuration();
- }
-
- void willDestroyPlaybackRegion (ARAPlaybackRegion* playbackRegion) override
- {
- playbackRegion->removeListener (this);
- removeChildComponent (playbackRegionViews[playbackRegion].get());
- playbackRegionViews.erase (playbackRegion);
- updatePlaybackDuration();
- }
-
- void willUpdatePlaybackRegionProperties (ARAPlaybackRegion*, ARAPlaybackRegion::PropertiesPtr) override
- {
- }
-
- void didUpdatePlaybackRegionProperties (ARAPlaybackRegion*) override
- {
- updatePlaybackDuration();
- }
-
- void resized() override
- {
- for (auto& pbr : playbackRegionViews)
- {
- const auto playbackRegion = pbr.first;
- pbr.second->setBounds (
- getLocalBounds()
- .withTrimmedLeft (roundToInt (playbackRegion->getStartInPlaybackTime() * zoomLevelPixelPerSecond))
- .withWidth (roundToInt (playbackRegion->getDurationInPlaybackTime() * zoomLevelPixelPerSecond)));
- }
- }
-
- auto getPlaybackDuration() const noexcept
- {
- return playbackDuration;
- }
-
- void setZoomLevel (double pixelPerSecond)
- {
- zoomLevelPixelPerSecond = pixelPerSecond;
- resized();
- }
-
- private:
- void createAndAddPlaybackRegionView (ARAPlaybackRegion* playbackRegion)
- {
- playbackRegionViews[playbackRegion] = std::make_unique<PlaybackRegionView> (*playbackRegion, waveformCache);
- playbackRegion->addListener (this);
- addAndMakeVisible (*playbackRegionViews[playbackRegion]);
- }
-
- void updatePlaybackDuration()
- {
- const auto iter = std::max_element (
- playbackRegionViews.begin(),
- playbackRegionViews.end(),
- [] (const auto& a, const auto& b) { return a.first->getEndInPlaybackTime() < b.first->getEndInPlaybackTime(); });
-
- playbackDuration = iter != playbackRegionViews.end() ? iter->first->getEndInPlaybackTime()
- : 0.0;
-
- sendChangeMessage();
- }
-
- ARARegionSequence& regionSequence;
- WaveformCache& waveformCache;
- std::unordered_map<ARAPlaybackRegion*, std::unique_ptr<PlaybackRegionView>> playbackRegionViews;
- double playbackDuration = 0.0;
- double zoomLevelPixelPerSecond;
- };
-
- class ZoomControls : public Component
- {
- public:
- ZoomControls()
- {
- addAndMakeVisible (zoomInButton);
- addAndMakeVisible (zoomOutButton);
- }
-
- void setZoomInCallback (std::function<void()> cb) { zoomInButton.onClick = std::move (cb); }
- void setZoomOutCallback (std::function<void()> cb) { zoomOutButton.onClick = std::move (cb); }
-
- void resized() override
- {
- FlexBox fb;
- fb.justifyContent = FlexBox::JustifyContent::flexEnd;
-
- for (auto* button : { &zoomInButton, &zoomOutButton })
- fb.items.add (FlexItem (*button).withMinHeight (30.0f).withMinWidth (30.0f).withMargin ({ 5, 5, 5, 0 }));
-
- fb.performLayout (getLocalBounds());
- }
-
- private:
- TextButton zoomInButton { "+" }, zoomOutButton { "-" };
- };
-
- class TrackHeader : public Component
- {
- public:
- explicit TrackHeader (const ARARegionSequence& regionSequenceIn) : regionSequence (regionSequenceIn)
- {
- update();
-
- addAndMakeVisible (trackNameLabel);
- }
-
- void resized() override
- {
- trackNameLabel.setBounds (getLocalBounds().reduced (2));
- }
-
- void paint (Graphics& g) override
- {
- g.setColour (getLookAndFeel().findColour (ResizableWindow::backgroundColourId));
- g.fillRoundedRectangle (getLocalBounds().reduced (2).toType<float>(), 6.0f);
- g.setColour (getLookAndFeel().findColour (ResizableWindow::backgroundColourId).contrasting());
- g.drawRoundedRectangle (getLocalBounds().reduced (2).toType<float>(), 6.0f, 1.0f);
- }
-
- private:
- void update()
- {
- const auto getWithDefaultValue =
- [] (const ARA::PlugIn::OptionalProperty<ARA::ARAUtf8String>& optional, String defaultValue)
- {
- if (const ARA::ARAUtf8String value = optional)
- return String (value);
-
- return defaultValue;
- };
-
- trackNameLabel.setText (getWithDefaultValue (regionSequence.getName(), "No track name"),
- NotificationType::dontSendNotification);
- }
-
- const ARARegionSequence& regionSequence;
- Label trackNameLabel;
- };
-
- constexpr auto trackHeight = 60;
-
- class VerticalLayoutViewportContent : public Component
- {
- public:
- void resized() override
- {
- auto bounds = getLocalBounds();
-
- for (auto* component : getChildren())
- {
- component->setBounds (bounds.removeFromTop (trackHeight));
- component->resized();
- }
- }
-
- void setOverlayComponent (Component* component)
- {
- if (overlayComponent != nullptr && overlayComponent != component)
- removeChildComponent (overlayComponent);
-
- addChildComponent (component);
- overlayComponent = component;
- }
-
- private:
- Component* overlayComponent = nullptr;
- };
-
- class VerticalLayoutViewport : public Viewport
- {
- public:
- VerticalLayoutViewport()
- {
- setViewedComponent (&content, false);
- }
-
- void paint (Graphics& g) override
- {
- g.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId).brighter());
- }
-
- std::function<void (Rectangle<int>)> onVisibleAreaChanged;
-
- VerticalLayoutViewportContent content;
-
- private:
- void visibleAreaChanged (const Rectangle<int>& newVisibleArea) override
- {
- NullCheckedInvocation::invoke (onVisibleAreaChanged, newVisibleArea);
- }
- };
-
- class OverlayComponent : public Component,
- private Timer
- {
- public:
- class PlayheadMarkerComponent : public Component
- {
- void paint (Graphics& g) override { g.fillAll (juce::Colours::yellow.darker (0.2f)); }
- };
-
- OverlayComponent (PlayHeadState& playHeadStateIn)
- : playHeadState (&playHeadStateIn)
- {
- addChildComponent (playheadMarker);
- setInterceptsMouseClicks (false, false);
- startTimerHz (30);
- }
-
- ~OverlayComponent() override
- {
- stopTimer();
- }
-
- void resized() override
- {
- doResize();
- }
-
- void setZoomLevel (double pixelPerSecondIn)
- {
- pixelPerSecond = pixelPerSecondIn;
- }
-
- void setHorizontalOffset (int offset)
- {
- horizontalOffset = offset;
- }
-
- private:
- void doResize()
- {
- if (playHeadState->isPlaying.load())
- {
- const auto markerX = playHeadState->timeInSeconds.load() * pixelPerSecond;
- const auto playheadLine = getLocalBounds().withTrimmedLeft ((int) (markerX - markerWidth / 2.0) - horizontalOffset)
- .removeFromLeft ((int) markerWidth);
- playheadMarker.setVisible (true);
- playheadMarker.setBounds (playheadLine);
- }
- else
- {
- playheadMarker.setVisible (false);
- }
- }
-
- void timerCallback() override
- {
- doResize();
- }
-
- static constexpr double markerWidth = 2.0;
-
- PlayHeadState* playHeadState;
- double pixelPerSecond = 1.0;
- int horizontalOffset = 0;
- PlayheadMarkerComponent playheadMarker;
- };
-
- class DocumentView : public Component,
- public ChangeListener,
- private ARADocument::Listener,
- private ARAEditorView::Listener
- {
- public:
- explicit DocumentView (ARADocument& document, PlayHeadState& playHeadState)
- : araDocument (document),
- overlay (playHeadState)
- {
- addAndMakeVisible (tracksBackground);
-
- viewport.onVisibleAreaChanged = [this] (const auto& r)
- {
- viewportHeightOffset = r.getY();
- overlay.setHorizontalOffset (r.getX());
- resized();
- };
-
- addAndMakeVisible (viewport);
-
- overlay.setZoomLevel (zoomLevelPixelPerSecond);
- addAndMakeVisible (overlay);
-
- zoomControls.setZoomInCallback ([this] { zoom (2.0); });
- zoomControls.setZoomOutCallback ([this] { zoom (0.5); });
- addAndMakeVisible (zoomControls);
-
- invalidateRegionSequenceViews();
- araDocument.addListener (this);
- }
-
- ~DocumentView() override
- {
- araDocument.removeListener (this);
- }
-
- //==============================================================================
- // ARADocument::Listener overrides
- void didReorderRegionSequencesInDocument (ARADocument*) override
- {
- invalidateRegionSequenceViews();
- }
-
- void didAddRegionSequenceToDocument (ARADocument*, ARARegionSequence*) override
- {
- invalidateRegionSequenceViews();
- }
-
- void willRemoveRegionSequenceFromDocument (ARADocument*, ARARegionSequence* regionSequence) override
- {
- removeRegionSequenceView (regionSequence);
- }
-
- void didEndEditing (ARADocument*) override
- {
- rebuildRegionSequenceViews();
- update();
- }
-
- //==============================================================================
- void changeListenerCallback (ChangeBroadcaster*) override
- {
- update();
- }
-
- //==============================================================================
- // ARAEditorView::Listener overrides
- void onNewSelection (const ARA::PlugIn::ViewSelection&) override
- {
- }
-
- void onHideRegionSequences (const std::vector<ARARegionSequence*>&) override
- {
- }
-
- //==============================================================================
- void paint (Graphics& g) override
- {
- g.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId).darker());
- }
-
- void resized() override
- {
- auto bounds = getLocalBounds();
- const auto bottomControlsBounds = bounds.removeFromBottom (40);
- const auto headerBounds = bounds.removeFromLeft (headerWidth).reduced (2);
-
- zoomControls.setBounds (bottomControlsBounds);
- layOutVertically (headerBounds, trackHeaders, viewportHeightOffset);
- tracksBackground.setBounds (bounds);
- viewport.setBounds (bounds);
- overlay.setBounds (bounds);
- }
-
- //==============================================================================
- void setZoomLevel (double pixelPerSecond)
- {
- zoomLevelPixelPerSecond = pixelPerSecond;
-
- for (const auto& view : regionSequenceViews)
- view.second->setZoomLevel (zoomLevelPixelPerSecond);
-
- overlay.setZoomLevel (zoomLevelPixelPerSecond);
-
- update();
- }
-
- static constexpr int headerWidth = 120;
-
- private:
- struct RegionSequenceViewKey
- {
- explicit RegionSequenceViewKey (ARARegionSequence* regionSequence)
- : orderIndex (regionSequence->getOrderIndex()), sequence (regionSequence)
- {
- }
-
- bool operator< (const RegionSequenceViewKey& other) const
- {
- return std::tie (orderIndex, sequence) < std::tie (other.orderIndex, other.sequence);
- }
-
- ARA::ARAInt32 orderIndex;
- ARARegionSequence* sequence;
- };
-
- void zoom (double factor)
- {
- zoomLevelPixelPerSecond = jlimit (minimumZoom, minimumZoom * 32, zoomLevelPixelPerSecond * factor);
- setZoomLevel (zoomLevelPixelPerSecond);
- }
-
- template <typename T>
- void layOutVertically (Rectangle<int> bounds, T& components, int verticalOffset = 0)
- {
- bounds = bounds.withY (bounds.getY() - verticalOffset).withHeight (bounds.getHeight() + verticalOffset);
-
- for (auto& component : components)
- {
- component.second->setBounds (bounds.removeFromTop (trackHeight));
- component.second->resized();
- }
- }
-
- void update()
- {
- timelineLength = 0.0;
-
- for (const auto& view : regionSequenceViews)
- timelineLength = std::max (timelineLength, view.second->getPlaybackDuration());
-
- const Rectangle<int> timelineSize (roundToInt (timelineLength * zoomLevelPixelPerSecond),
- (int) regionSequenceViews.size() * trackHeight);
- viewport.content.setSize (timelineSize.getWidth(), timelineSize.getHeight());
- viewport.content.resized();
-
- resized();
- }
-
- void addTrackViews (ARARegionSequence* regionSequence)
- {
- const auto insertIntoMap = [](auto& map, auto key, auto value) -> auto&
- {
- auto it = map.insert ({ std::move (key), std::move (value) });
- return *(it.first->second);
- };
-
- auto& regionSequenceView = insertIntoMap (
- regionSequenceViews,
- RegionSequenceViewKey { regionSequence },
- std::make_unique<RegionSequenceView> (*regionSequence, waveformCache, zoomLevelPixelPerSecond));
-
- regionSequenceView.addChangeListener (this);
- viewport.content.addAndMakeVisible (regionSequenceView);
-
- auto& trackHeader = insertIntoMap (trackHeaders,
- RegionSequenceViewKey { regionSequence },
- std::make_unique<TrackHeader> (*regionSequence));
-
- addAndMakeVisible (trackHeader);
- }
-
- void removeRegionSequenceView (ARARegionSequence* regionSequence)
- {
- const auto& view = regionSequenceViews.find (RegionSequenceViewKey { regionSequence });
-
- if (view != regionSequenceViews.cend())
- {
- removeChildComponent (view->second.get());
- regionSequenceViews.erase (view);
- }
-
- invalidateRegionSequenceViews();
- }
-
- void invalidateRegionSequenceViews()
- {
- regionSequenceViewsAreValid = false;
- rebuildRegionSequenceViews();
- }
-
- void rebuildRegionSequenceViews()
- {
- if (! regionSequenceViewsAreValid && ! araDocument.getDocumentController()->isHostEditingDocument())
- {
- for (auto& view : regionSequenceViews)
- removeChildComponent (view.second.get());
-
- regionSequenceViews.clear();
-
- for (auto& view : trackHeaders)
- removeChildComponent (view.second.get());
-
- trackHeaders.clear();
-
- for (auto* regionSequence : araDocument.getRegionSequences())
- {
- addTrackViews (regionSequence);
- }
-
- update();
-
- regionSequenceViewsAreValid = true;
- }
- }
-
- class TracksBackgroundComponent : public Component
- {
- void paint (Graphics& g) override
- {
- g.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId).brighter());
- }
- };
-
- static constexpr auto minimumZoom = 10.0;
- static constexpr auto trackHeight = 60;
-
- ARADocument& araDocument;
-
- bool regionSequenceViewsAreValid = false;
- double timelineLength = 0;
- double zoomLevelPixelPerSecond = minimumZoom * 4;
-
- WaveformCache waveformCache;
- TracksBackgroundComponent tracksBackground;
- std::map<RegionSequenceViewKey, std::unique_ptr<TrackHeader>> trackHeaders;
- std::map<RegionSequenceViewKey, std::unique_ptr<RegionSequenceView>> regionSequenceViews;
- VerticalLayoutViewport viewport;
- OverlayComponent overlay;
- ZoomControls zoomControls;
-
- int viewportHeightOffset = 0;
- };
-
-
- class ARADemoPluginProcessorEditor : public AudioProcessorEditor,
- public AudioProcessorEditorARAExtension
- {
- public:
- explicit ARADemoPluginProcessorEditor (ARADemoPluginAudioProcessorImpl& p)
- : AudioProcessorEditor (&p),
- AudioProcessorEditorARAExtension (&p)
- {
- if (auto* editorView = getARAEditorView())
- {
- auto* document = ARADocumentControllerSpecialisation::getSpecialisedDocumentController(editorView->getDocumentController())->getDocument();
- documentView = std::make_unique<DocumentView> (*document, p.playHeadState );
- }
-
- addAndMakeVisible (documentView.get());
-
- // ARA requires that plugin editors are resizable to support tight integration
- // into the host UI
- setResizable (true, false);
- setSize (400, 300);
- }
-
- //==============================================================================
- void paint (Graphics& g) override
- {
- g.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId));
-
- if (! isARAEditorView())
- {
- g.setColour (Colours::white);
- g.setFont (15.0f);
- g.drawFittedText ("ARA host isn't detected. This plugin only supports ARA mode",
- getLocalBounds(),
- Justification::centred,
- 1);
- }
- }
-
- void resized() override
- {
- if (documentView != nullptr)
- documentView->setBounds (getLocalBounds());
- }
-
- private:
- std::unique_ptr<Component> documentView;
-
- JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ARADemoPluginProcessorEditor)
- };
-
- class ARADemoPluginAudioProcessor : public ARADemoPluginAudioProcessorImpl
- {
- public:
- bool hasEditor() const override { return true; }
- AudioProcessorEditor* createEditor() override { return new ARADemoPluginProcessorEditor (*this); }
- };
|