diff --git a/examples/Plugins/SurroundPluginDemo.h b/examples/Plugins/SurroundPluginDemo.h index fef0664dc8..428b232dab 100644 --- a/examples/Plugins/SurroundPluginDemo.h +++ b/examples/Plugins/SurroundPluginDemo.h @@ -50,288 +50,412 @@ //============================================================================== -class ChannelClickListener +class ProcessorWithLevels : public AudioProcessor, + private AsyncUpdater, + private Timer { public: - virtual ~ChannelClickListener() {} - virtual void channelButtonClicked (int channelIndex) = 0; - virtual bool isChannelActive (int channelIndex) = 0; -}; - -class SurroundEditor : public AudioProcessorEditor, - private Timer -{ -public: - SurroundEditor (AudioProcessor& parent) - : AudioProcessorEditor (parent), - currentChannelLayout (AudioChannelSet::disabled()), - layoutTitle ("LayoutTitleLabel", getLayoutName()) + ProcessorWithLevels() + : AudioProcessor (BusesProperties().withInput ("Input", AudioChannelSet::stereo()) + .withInput ("Aux", AudioChannelSet::stereo(), false) + .withOutput ("Output", AudioChannelSet::stereo()) + .withOutput ("Aux", AudioChannelSet::stereo(), false)) { - layoutTitle.setJustificationType (Justification::centred); - addAndMakeVisible (layoutTitle); - addAndMakeVisible (noChannelsLabel); - - setSize (600, 100); - - lastSuspended = ! getAudioProcessor()->isSuspended(); - timerCallback(); - startTimer (500); + startTimerHz (60); + applyBusLayouts (getBusesLayout()); } - void resized() override + ~ProcessorWithLevels() override { - auto r = getLocalBounds(); + stopTimer(); + cancelPendingUpdate(); + } - layoutTitle.setBounds (r.removeFromBottom (16)); + void prepareToPlay (double, int) override + { + samplesToPlay = (int) getSampleRate(); + reset(); + } - noChannelsLabel.setBounds (r); + void processBlock (AudioBuffer& audio, MidiBuffer&) override { processAudio (audio); } + void processBlock (AudioBuffer& audio, MidiBuffer&) override { processAudio (audio); } - if (channelButtons.size() > 0) - { - auto buttonWidth = r.getWidth() / channelButtons.size(); - for (auto channelButton : channelButtons) - channelButton->setBounds (r.removeFromLeft (buttonWidth)); - } - } + void releaseResources() override { reset(); } - void paint (Graphics& g) override + float getLevel (int bus, int channel) const { - g.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId)); + return readableLevels[(size_t) getChannelIndexInProcessBlockBuffer (true, bus, channel)]; } - void updateButton (Button* btn) + bool isBusesLayoutSupported (const BusesLayout& layouts) const override { - if (auto* textButton = dynamic_cast (btn)) + const auto isSetValid = [] (const AudioChannelSet& set) { - auto channelIndex = channelButtons.indexOf (textButton); + return ! set.isDisabled() + && ! (set.isDiscreteLayout() && set.getChannelIndexForType (AudioChannelSet::discreteChannel0) == -1); + }; - if (auto* listener = dynamic_cast (getAudioProcessor())) - listener->channelButtonClicked (channelIndex); - } + return isSetValid (layouts.getMainOutputChannelSet()) + && isSetValid (layouts.getMainInputChannelSet()); } - void updateGUI() + void reset() override { - const auto& channelSet = getAudioProcessor()->getChannelLayoutOfBus (false, 0); + channelClicked = 0; + samplesPlayed = samplesToPlay; + } - if (channelSet != currentChannelLayout) - { - currentChannelLayout = channelSet; + bool applyBusLayouts (const BusesLayout& layouts) override + { + // Some very badly-behaved hosts will call this during processing! + const SpinLock::ScopedLockType lock (levelMutex); - layoutTitle.setText (currentChannelLayout.getDescription(), NotificationType::dontSendNotification); - channelButtons.clear(); - activeChannels.resize (currentChannelLayout.size()); + const auto result = AudioProcessor::applyBusLayouts (layouts); - if (currentChannelLayout == AudioChannelSet::disabled()) - { - noChannelsLabel.setVisible (true); - } - else - { - auto numChannels = currentChannelLayout.size(); + size_t numInputChannels = 0; - for (auto i = 0; i < numChannels; ++i) - { - auto channelName = - AudioChannelSet::getAbbreviatedChannelTypeName (currentChannelLayout.getTypeOfChannel (i)); + for (auto i = 0; i < getBusCount (true); ++i) + numInputChannels += (size_t) getBus (true, i)->getLastEnabledLayout().size(); - TextButton* newButton; - channelButtons.add (newButton = new TextButton (channelName, channelName)); + incomingLevels = readableLevels = std::vector (numInputChannels, 0.0f); - newButton->onClick = [this, newButton] { updateButton (newButton); }; - addAndMakeVisible (newButton); - } + triggerAsyncUpdate(); + return result; + } - noChannelsLabel.setVisible (false); - resized(); - } + //============================================================================== + const String getName() const override { return "Surround PlugIn"; } + bool acceptsMidi() const override { return false; } + bool producesMidi() const override { return false; } + double getTailLengthSeconds() const override { return 0; } - if (auto* listener = dynamic_cast (getAudioProcessor())) - { - auto activeColour = getLookAndFeel().findColour (Slider::thumbColourId); - auto inactiveColour = getLookAndFeel().findColour (Slider::trackColourId); + //============================================================================== + int getNumPrograms() override { return 1; } + int getCurrentProgram() override { return 0; } + void setCurrentProgram (int) override {} + const String getProgramName (int) override { return "None"; } + void changeProgramName (int, const String&) override {} - for (auto i = 0; i < activeChannels.size(); ++i) - { - auto isActive = listener->isChannelActive (i); - activeChannels.getReference (i) = isActive; - channelButtons[i]->setColour (TextButton::buttonColourId, isActive ? activeColour : inactiveColour); - channelButtons[i]->repaint(); - } - } - } + //============================================================================== + void getStateInformation (MemoryBlock&) override {} + void setStateInformation (const void*, int) override {} + + void channelButtonClicked (int bus, int channelIndex) + { + channelClicked = getChannelIndexInProcessBlockBuffer (false, bus, channelIndex); + samplesPlayed = 0; } + std::function updateEditor; + private: - String getLayoutName() const + void handleAsyncUpdate() override { - if (auto* p = getAudioProcessor()) - return p->getChannelLayoutOfBus (false, 0).getDescription(); - - return "Unknown"; + NullCheckedInvocation::invoke (updateEditor); } - void timerCallback() override + template + void processAudio (AudioBuffer& audio) { - if (getAudioProcessor()->isSuspended() != lastSuspended) { - lastSuspended = getAudioProcessor()->isSuspended(); - updateGUI(); - } + SpinLock::ScopedTryLockType lock (levelMutex); - if (! lastSuspended) - { - if (auto* listener = dynamic_cast (getAudioProcessor())) + if (lock.isLocked()) { - auto activeColour = getLookAndFeel().findColour (Slider::thumbColourId); - auto inactiveColour = getLookAndFeel().findColour (Slider::trackColourId); + const auto numInputChannels = (size_t) getTotalNumInputChannels(); - for (auto i = 0; i < activeChannels.size(); ++i) + for (size_t i = 0; i < numInputChannels; ++i) { - auto isActive = listener->isChannelActive (i); - if (activeChannels.getReference (i) != isActive) - { - activeChannels.getReference (i) = isActive; - channelButtons[i]->setColour (TextButton::buttonColourId, isActive ? activeColour : inactiveColour); - channelButtons[i]->repaint(); - } + const auto minMax = audio.findMinMax ((int) i, 0, audio.getNumSamples()); + const auto newMax = (float) std::max (std::abs (minMax.getStart()), std::abs (minMax.getEnd())); + + auto& toUpdate = incomingLevels[i]; + toUpdate = jmax (toUpdate, newMax); } } } + + audio.clear (0, audio.getNumSamples()); + + auto fillSamples = jmin (samplesToPlay - samplesPlayed, audio.getNumSamples()); + + if (isPositiveAndBelow (channelClicked, audio.getNumChannels())) + { + auto* channelBuffer = audio.getWritePointer (channelClicked); + auto freq = (float) (440.0 / getSampleRate()); + + for (auto i = 0; i < fillSamples; ++i) + channelBuffer[i] += std::sin (MathConstants::twoPi * freq * (float) samplesPlayed++); + } } - AudioChannelSet currentChannelLayout; - Label noChannelsLabel { "noChannelsLabel", "Input disabled" }, - layoutTitle; - OwnedArray channelButtons; - Array activeChannels; + void timerCallback() override + { + const SpinLock::ScopedLockType lock (levelMutex); - bool lastSuspended; + for (size_t i = 0; i < readableLevels.size(); ++i) + readableLevels[i] = std::max (readableLevels[i] * 0.95f, std::exchange (incomingLevels[i], 0.0f)); + } + + SpinLock levelMutex; + std::vector incomingLevels; + std::vector readableLevels; + + int channelClicked; + int samplesPlayed; + int samplesToPlay; }; //============================================================================== -class SurroundProcessor : public AudioProcessor, - public ChannelClickListener, - private AsyncUpdater +const Colour textColour = Colours::white.withAlpha (0.8f); + +inline void drawBackground (Component& comp, Graphics& g) +{ + g.setColour (comp.getLookAndFeel().findColour (ResizableWindow::backgroundColourId).darker (0.8f)); + g.fillRoundedRectangle (comp.getLocalBounds().toFloat(), 4.0f); +} + +inline void configureLabel (Label& label, const AudioProcessor::Bus* layout) +{ + const auto text = layout != nullptr + ? (layout->getName() + ": " + layout->getCurrentLayout().getDescription()) + : ""; + label.setText (text, dontSendNotification); + label.setJustificationType (Justification::centred); + label.setColour (Label::textColourId, textColour); +} + +class InputBusViewer : public Component, + private Timer { public: - SurroundProcessor() - : AudioProcessor(BusesProperties().withInput ("Input", AudioChannelSet::stereo()) - .withOutput ("Output", AudioChannelSet::stereo())) - {} + InputBusViewer (ProcessorWithLevels& proc, int busNumber) + : processor (proc), + bus (busNumber) + { + configureLabel (layoutName, processor.getBus (true, bus)); + addAndMakeVisible (layoutName); - //============================================================================== - void prepareToPlay (double sampleRate, int samplesPerBlock) override + startTimerHz (60); + } + + ~InputBusViewer() override { - channelClicked = 0; - sampleOffset = static_cast (std::ceil (sampleRate)); + stopTimer(); + } - auto numChannels = getChannelCountOfBus (true, 0); - channelActive.resize (numChannels); - alphaCoeffs .resize (numChannels); - reset(); + void paint (Graphics& g) override + { + drawBackground (*this, g); - triggerAsyncUpdate(); + auto* layout = processor.getBus (true, bus); - ignoreUnused (samplesPerBlock); - } + if (layout == nullptr) + return; - void releaseResources() override { reset(); } + const auto channelSet = layout->getCurrentLayout(); + const auto numChannels = channelSet.size(); - void processBlock (AudioBuffer& buffer, MidiBuffer&) override - { - for (auto ch = 0; ch < buffer.getNumChannels(); ++ch) + Grid grid; + + grid.autoFlow = Grid::AutoFlow::column; + grid.autoColumns = grid.autoRows = Grid::TrackInfo (Grid::Fr (1)); + grid.items.insertMultiple (0, GridItem(), numChannels); + grid.performLayout (getLocalBounds()); + + const auto minDb = -50.0f; + const auto maxDb = 6.0f; + + for (auto i = 0; i < numChannels; ++i) { - auto& channelTime = channelActive.getReference (ch); - auto& alpha = alphaCoeffs .getReference (ch); + g.setColour (Colours::orange.darker()); - for (auto j = 0; j < buffer.getNumSamples(); ++j) - { - auto sample = buffer.getReadPointer (ch)[j]; - alpha = (0.8f * alpha) + (0.2f * sample); + const auto levelInDb = Decibels::gainToDecibels (processor.getLevel (bus, i), minDb); + const auto fractionOfHeight = jmap (levelInDb, minDb, maxDb, 0.0f, 1.0f); + const auto bounds = grid.items[i].currentBounds; + const auto trackBounds = bounds.withSizeKeepingCentre (16, bounds.getHeight() - 10).toFloat(); + g.fillRect (trackBounds.withHeight (trackBounds.proportionOfHeight (fractionOfHeight)).withBottomY (trackBounds.getBottom())); - if (std::abs (alpha) >= 0.1f) - channelTime = static_cast (getSampleRate() / 2.0); - } + g.setColour (textColour); - channelTime = jmax (0, channelTime - buffer.getNumSamples()); + g.drawText (channelSet.getAbbreviatedChannelTypeName (channelSet.getTypeOfChannel (i)), + bounds, + Justification::centredBottom); } + } - auto fillSamples = jmin (static_cast (std::ceil (getSampleRate())) - sampleOffset, - buffer.getNumSamples()); + void resized() override + { + layoutName.setBounds (getLocalBounds().removeFromTop (20)); + } - if (isPositiveAndBelow (channelClicked, buffer.getNumChannels())) - { - auto* channelBuffer = buffer.getWritePointer (channelClicked); - auto freq = (float) (440.0 / getSampleRate()); + int getNumChannels() const + { + if (auto* b = processor.getBus (true, bus)) + return b->getCurrentLayout().size(); - for (auto i = 0; i < fillSamples; ++i) - channelBuffer[i] += std::sin (MathConstants::twoPi * freq * static_cast (sampleOffset++)); - } + return 0; } - using AudioProcessor::processBlock; +private: + void timerCallback() override { repaint(); } - //============================================================================== - AudioProcessorEditor* createEditor() override { return new SurroundEditor (*this); } - bool hasEditor() const override { return true; } + ProcessorWithLevels& processor; + int bus = 0; + Label layoutName; +}; - //============================================================================== - bool isBusesLayoutSupported (const BusesLayout& layouts) const override +//============================================================================== +class OutputBusViewer : public Component +{ +public: + OutputBusViewer (ProcessorWithLevels& proc, int busNumber) + : processor (proc), + bus (busNumber) { - return ((! layouts.getMainInputChannelSet() .isDiscreteLayout()) - && (! layouts.getMainOutputChannelSet().isDiscreteLayout()) - && (layouts.getMainInputChannelSet() == layouts.getMainOutputChannelSet()) - && (! layouts.getMainInputChannelSet().isDisabled())); + auto* layout = processor.getBus (false, bus); + + configureLabel (layoutName, layout); + addAndMakeVisible (layoutName); + + if (layout == nullptr) + return; + + const auto& channelSet = layout->getCurrentLayout(); + + const auto numChannels = channelSet.size(); + + for (auto i = 0; i < numChannels; ++i) + { + const auto channelName = channelSet.getAbbreviatedChannelTypeName (channelSet.getTypeOfChannel (i)); + + channelButtons.emplace_back (channelName, channelName); + + auto& newButton = channelButtons.back(); + newButton.onClick = [&proc = processor, bus = bus, i] { proc.channelButtonClicked (bus, i); }; + addAndMakeVisible (newButton); + } + + resized(); } - void reset() override + void paint (Graphics& g) override { - for (auto& channel : channelActive) - channel = 0; + drawBackground (*this, g); } - //============================================================================== - const String getName() const override { return "Surround PlugIn"; } - bool acceptsMidi() const override { return false; } - bool producesMidi() const override { return false; } - double getTailLengthSeconds() const override { return 0; } + void resized() override + { + auto b = getLocalBounds(); - //============================================================================== - int getNumPrograms() override { return 1; } - int getCurrentProgram() override { return 0; } - void setCurrentProgram (int) override {} - const String getProgramName (int) override { return "None"; } - void changeProgramName (int, const String&) override {} + layoutName.setBounds (b.removeFromBottom (20)); - //============================================================================== - void getStateInformation (MemoryBlock&) override {} - void setStateInformation (const void*, int) override {} + Grid grid; + grid.autoFlow = Grid::AutoFlow::column; + grid.autoColumns = grid.autoRows = Grid::TrackInfo (Grid::Fr (1)); - void channelButtonClicked (int channelIndex) override + for (auto& channelButton : channelButtons) + grid.items.add (GridItem (channelButton)); + + grid.performLayout (b.reduced (2)); + } + + int getNumChannels() const { - channelClicked = channelIndex; - sampleOffset = 0; + if (auto* b = processor.getBus (false, bus)) + return b->getCurrentLayout().size(); + + return 0; } - bool isChannelActive (int channelIndex) override +private: + ProcessorWithLevels& processor; + int bus = 0; + Label layoutName; + std::list channelButtons; +}; + +//============================================================================== +class SurroundEditor : public AudioProcessorEditor +{ +public: + explicit SurroundEditor (ProcessorWithLevels& parent) + : AudioProcessorEditor (parent), + customProcessor (parent), + scopedUpdateEditor (customProcessor.updateEditor, [this] { updateGUI(); }) { - return channelActive[channelIndex] > 0; + updateGUI(); + setResizable (true, true); } - void handleAsyncUpdate() override + void resized() override { - if (auto* editor = getActiveEditor()) - if (auto* surroundEditor = dynamic_cast (editor)) - surroundEditor->updateGUI(); + auto r = getLocalBounds(); + doLayout (inputViewers, r.removeFromTop (proportionOfHeight (0.5f))); + doLayout (outputViewers, r); + } + + void paint (Graphics& g) override + { + g.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId)); } private: - Array channelActive; - Array alphaCoeffs; - int channelClicked; - int sampleOffset; + template + void doLayout (Range& range, Rectangle bounds) const + { + FlexBox fb; - //============================================================================== - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SurroundProcessor) + for (auto& viewer : range) + { + if (viewer.getNumChannels() != 0) + { + fb.items.add (FlexItem (viewer) + .withFlex ((float) viewer.getNumChannels()) + .withMargin (4.0f)); + } + } + + fb.performLayout (bounds); + } + + void updateGUI() + { + inputViewers.clear(); + outputViewers.clear(); + + const auto inputBuses = getAudioProcessor()->getBusCount (true); + + for (auto i = 0; i < inputBuses; ++i) + { + inputViewers.emplace_back (customProcessor, i); + addAndMakeVisible (inputViewers.back()); + } + + const auto outputBuses = getAudioProcessor()->getBusCount (false); + + for (auto i = 0; i < outputBuses; ++i) + { + outputViewers.emplace_back (customProcessor, i); + addAndMakeVisible (outputViewers.back()); + } + + const auto channels = jmax (processor.getTotalNumInputChannels(), + processor.getTotalNumOutputChannels()); + setSize (jmax (150, channels * 40), 200); + + resized(); + } + + ProcessorWithLevels& customProcessor; + ScopedValueSetter> scopedUpdateEditor; + std::list inputViewers; + std::list outputViewers; +}; + +//============================================================================== +struct SurroundProcessor : public ProcessorWithLevels +{ + AudioProcessorEditor* createEditor() override { return new SurroundEditor (*this); } + bool hasEditor() const override { return true; } };