diff --git a/Examples/SignalGenerator/SignalGenerator.jucer b/Examples/SignalGenerator/SignalGenerator.jucer index dee0af20..51fbe5a0 100644 --- a/Examples/SignalGenerator/SignalGenerator.jucer +++ b/Examples/SignalGenerator/SignalGenerator.jucer @@ -5,7 +5,8 @@ pluginVSTCategory="kPlugCategGenerator" projectLineFeed=" " companyName="Foleys Finest Audio" companyWebsite="https://foleysfinest.com" bundleIdentifier="com.foleysfinest.signalgenerator" pluginManufacturerCode="FFAU" - addUsingNamespaceToJuceHeader="0" jucerFormatVersion="1" version="1.3.2"> + addUsingNamespaceToJuceHeader="0" jucerFormatVersion="1" version="1.3.2" + displaySplashScreen="1"> (sourceID)); + + auto decay = float (getProperty (pDecay)); + plot.setDecayFactor (decay); + + auto gradient = configNode.getProperty (pGradient, juce::String()).toString(); + plot.setGradientFromString (gradient, magicBuilder.getStylesheet()); + + auto triggeredPos = bool (getProperty (pTriggeredPos)); + plot.setTriggeredPos (triggeredPos); + + auto triggeredNeg = bool (getProperty (pTriggeredNeg)); + plot.setTriggeredNeg (triggeredNeg); + + auto overlay = bool (getProperty (pOverlay)); + plot.setOverlay (overlay); + + auto normalize = bool (getProperty (pNormalize)); + plot.setNormalize (normalize); + + auto latch = bool (getProperty (pLatch)); + plot.setLatch (latch); + + auto channel = int (getProperty (pChannel)); + plot.setChannel (channel); + + auto numChannels = int (getProperty (pNumChannels)); + plot.setNumChannels (numChannels); + + auto plotLength = int (getProperty (pPlotLength)); + plot.setPlotLength (plotLength); + + auto plotOffset = float (getProperty (pPlotOffset)); + plot.setPlotOffset (plotOffset); + } + + std::vector getSettableProperties() const override + { + std::vector props; //? { AudioPlotItem::getSettableProperties() }; + props.push_back ({ configNode, IDs::source, SettableProperty::Choice, {}, magicBuilder.createObjectsMenuLambda() }); + props.push_back ({ configNode, pDecay, SettableProperty::Number, {}, {} }); + props.push_back ({ configNode, pGradient, SettableProperty::Gradient, {}, {} }); + props.push_back ({ configNode, pTriggeredPos, SettableProperty::Toggle, {}, {} }); + props.push_back ({ configNode, pTriggeredNeg, SettableProperty::Toggle, {}, {} }); + props.push_back ({ configNode, pOverlay, SettableProperty::Toggle, {}, {} }); + props.push_back ({ configNode, pNormalize, SettableProperty::Toggle, {}, {} }); + props.push_back ({ configNode, pLatch, SettableProperty::Toggle, {}, {} }); + props.push_back ({ configNode, pChannel, SettableProperty::Number, {}, {} }); + props.push_back ({ configNode, pNumChannels, SettableProperty::Number, {}, {} }); + props.push_back ({ configNode, pPlotLength, SettableProperty::Number, {}, {} }); + props.push_back ({ configNode, pPlotOffset, SettableProperty::Number, {}, {} }); + return props; + } + + juce::Component* getWrappedComponent() override + { + return &plot; + } + +private: + MagicAudioPlotComponent plot; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioPlotItem) +}; +const juce::Identifier AudioPlotItem::pDecay {"plot-decay"}; +const juce::Identifier AudioPlotItem::pGradient {"plot-gradient"}; +const juce::Identifier AudioPlotItem::pTriggeredPos {"plot-triggered-pos"}; +const juce::Identifier AudioPlotItem::pTriggeredNeg {"plot-triggered-neg"}; +const juce::Identifier AudioPlotItem::pOverlay {"plot-overlay"}; +const juce::Identifier AudioPlotItem::pNormalize {"plot-normalize"}; +const juce::Identifier AudioPlotItem::pLatch {"plot-latch"}; +const juce::Identifier AudioPlotItem::pChannel {"plot-channel"}; +const juce::Identifier AudioPlotItem::pNumChannels {"plot-num-channels"}; +const juce::Identifier AudioPlotItem::pPlotLength {"plot-length"}; +const juce::Identifier AudioPlotItem::pPlotOffset {"plot-offset"}; + +//============================================================================== + class XYDraggerItem : public GuiItem { public: @@ -990,6 +1105,7 @@ void MagicGUIBuilder::registerJUCEFactories() registerFactory (IDs::toggleButton, &ToggleButtonItem::factory); registerFactory (IDs::label, &LabelItem::factory); registerFactory (IDs::plot, &PlotItem::factory); + registerFactory (IDs::audioPlot, &AudioPlotItem::factory); registerFactory (IDs::xyDragComponent, &XYDraggerItem::factory); registerFactory (IDs::keyboardComponent, &KeyboardItem::factory); registerFactory (IDs::drumpadComponent, &DrumpadItem::factory); diff --git a/modules/foleys_gui_magic/General/foleys_StringDefinitions.h b/modules/foleys_gui_magic/General/foleys_StringDefinitions.h index b07b9b77..c33e7b3f 100644 --- a/modules/foleys_gui_magic/General/foleys_StringDefinitions.h +++ b/modules/foleys_gui_magic/General/foleys_StringDefinitions.h @@ -53,6 +53,7 @@ namespace IDs static juce::Identifier comboBox { "ComboBox" }; static juce::Identifier meter { "Meter" }; static juce::Identifier plot { "Plot" }; + static juce::Identifier audioPlot { "AudioPlot" }; static juce::Identifier xyDragComponent { "XYDragComponent" }; static juce::Identifier keyboardComponent { "KeyboardComponent" }; static juce::Identifier drumpadComponent { "DrumpadComponent" }; diff --git a/modules/foleys_gui_magic/Visualisers/foleys_MagicAudioPlotSource.h b/modules/foleys_gui_magic/Visualisers/foleys_MagicAudioPlotSource.h new file mode 100644 index 00000000..f07c11e2 --- /dev/null +++ b/modules/foleys_gui_magic/Visualisers/foleys_MagicAudioPlotSource.h @@ -0,0 +1,354 @@ +/* + ============================================================================== + Copyright (c) 2023 Julius Smith and Foleys Finest Audio - Daniel Walz + All rights reserved. + + **BSD 3-Clause License** + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + 3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + + ============================================================================== + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + OF THE POSSIBILITY OF SUCH DAMAGE. + ============================================================================== + */ + +#pragma once + +#include +#include + +/** + * @file foleys_MagicAudioPlotSource.h + * @brief API-compatible replacement for foleys_MagicPlotSource.h + * + * This file contains proposed extensions for MagicPlotSource + * motivated by various audio needs. It is in a separate file with a + * different class name simply so that merges are never necessary + * until adoption, if ever. + * + * @author Julius Smith + * @date 2023-08-03 + * @version 0.1 + */ + +namespace foleys +{ + +class MagicAudioPlotComponent; + +/** + The MagicAudioPlotSources act as an interface, so the GUI can visualise an arbitrary plot + of data. To create a specific new plot, create a subclass and implement drawPlot. + */ +class MagicAudioPlotSource // not worth the trouble : public MagicPlotSource +{ +public: + + /** Default Constructor. */ + MagicAudioPlotSource()=default; + + /** Constructor allowing specification of a channel to display, or -1 to indicate all channels. */ + MagicAudioPlotSource(int channelToDisplay) : plotChannel(std::max(0,channelToDisplay)) {} + + /** Destructor. */ + virtual ~MagicAudioPlotSource()=default; + + /** + Set whether plot is triggered by a positive-going zero-crossing. + @param isTriggeredPos, if true, means each plot begins at an upward zero-crossing. + Otherwise, the latest samples received are plotted for each audio buffer. + */ + virtual void setTriggeredPos (bool isTriggeredPos) { triggeredPos = isTriggeredPos; } + + /** + Set whether plot is triggered by a negative-going zero-crossing. + @param isTriggeredNeg, if true, means each plot begins at an downward zero-crossing. + Otherwise, the latest samples received are plotted for each audio buffer. + if both isTriggeredPos and isTriggeredNeg are true, than any zero-crossing + will trigger the plot. + */ + virtual void setTriggeredNeg (bool isTriggeredNeg) { triggeredNeg = isTriggeredNeg; } + + /** + Set whether a multichannel plot is an overlay, with incrementing plot offset, or a sum of all channels. + Normalization, if any, is applied to the final sum, not the individual channels. + */ + virtual void setOverlay (bool overlay) { overlayPlots = overlay; } + + /** + Set whether each channel, or the average of all channels, is renormalized to full range. + @param isNormalizing, if true, means each audio channel, or sum of channels, is divided by + its maximum magnitude each plot, unless the maximum falls below -80 dBFS. + */ + virtual void setNormalize (bool isNormalizing) { normalize = isNormalizing; } + + /** + Set whether plot is latch when it would otherwise become zero. + @param isLatching, if true, means repeat the current plot if the next plot would be zero. + */ + virtual void setLatch (bool isLatching) { latch = isLatching; } + + /** + Set first audio channel to plot (numbering from 0). + */ + virtual void setChannel (int channel) { + plotChannel = std::max(0,channel); + } + + /** + Set number of audio channels to plot in overlay mode, or to average if not overlaid. + */ + virtual void setNumChannels (int nChans) + { + numPlotChannels = std::max(0,nChans); + if (nChans > samples.getNumChannels()) + { + samples.setSize (nChans, static_cast (sampleRate)); + samples.clear(); + } + } + + /** + Set default audio plot length in samples. + */ + virtual void setPlotLength (int pl) + { + plotLength = std::max(0,pl); + if (plotLength > samples.getNumSamples()) + samples.setSize(samples.getNumChannels(), plotLength); // circular buffer used for plotting + } + + /** + Set dynamic audio plot length in samples. This is normally set in pushSamples() for each audio buffer, + but this function is useful for setting it back to zero to return to the default plot length. + */ + virtual void setPlotLengthNow (int pln) + { + plotLengthNow = std::max(0,pln); + if (plotLengthNow > samples.getNumSamples()) + samples.setSize(samples.getNumChannels(), plotLengthNow); + writePosition.store (0); // when plotLengthNow>0, we only write each plot from 0 + } + + public: + + /** + Set offset between plots as a fractional value between 0 and 1 or higher, with 1 meaning adjacent nonoverlapping lanes. + */ + virtual void setPlotOffset (float po) + { + plotOffset = std::max(0.0f,po); + } + + /** + This method is called by the MagicProcessorState to allow the plot computation to be set up + */ + virtual void prepareToPlay (double sampleRateToUse, int samplesPerBlockExpected) + { + sampleRate = sampleRateToUse; + samples.setSize (1, static_cast (sampleRate)); + samples.clear(); + writePosition.store (0); + } + + /** + This is the callback whenever new sample data arrives. It is the subclasses + responsibility to put that into a FIFO and return as quickly as possible. + */ + virtual void pushSamples (const juce::AudioBuffer& buffer, int currentPlotLength=0)=0; + virtual void pushSamples (const juce::AudioBuffer& bufR, int channelToPlot, + int numChannelsToPlot, int currentPlotLength) + { + pushSamples(bufR,0); // FIXME: TODO + } + virtual void pushSamples (const std::shared_ptr> bufSP) { pushSamples(*bufSP.get(),0); } + virtual void pushSamples (const std::shared_ptr> bufSP, int channelToPlot=0, + int numChannelsToPlot=1, int currentPlotLength=0) + { + pushSamples(*bufSP.get(),0); // FIXME: TODO + } + + /** + This form of the pushSamples() callback provides two channels of + plot data, needed for XY scatterplots. + + @param bufferX is the audio buffer to serve as the X axis of the scatterplot. + @param channelX is the audio channel number (from 0) to use for the X axis of the scatterplot. + @param bufferY is the audio buffer to serve as the Y axis of the scatterplot. + @param channelY is the audio channel number (from 0) to use for the Y axis of the scatterplot. + @param currentPlotLength specifies the desired length of plots involving this audio buffer. + Default is 0 meaning take the default (which itself defaults to 10 ms of audio data). + A good setting for this is one period in samples, if you know what that is. + */ + virtual void pushSamples (const juce::AudioBuffer& bufferX, int channelX, + const juce::AudioBuffer& bufferY, int channelY, + const int currentPlotLength=0) { } + virtual void pushSamples (const std::shared_ptr> bufSPX, int channelX, + const std::shared_ptr> bufSPY, int channelY, + const int currentPlotLength=0) { } + + /** + This is the callback that creates the plot for drawing. + + @param path is the path instance that is constructed by the MagicPlotSource + @param filledPath is the path instance that is constructed by the MagicPlotSource to be filled + @param bounds the bounds of the plot + @param component grants access to the plot component, e.g. to find the colours from it + */ + virtual void createPlotPaths (juce::Path& path, juce::Path& filledPath, juce::Rectangle bounds, MagicAudioPlotComponent& component) = 0; + + /** + You can add an active state to your plot to allow to paint in different colours + */ + virtual bool isActive() const { return active; } + virtual void setActive (bool shouldBeActive) { active = shouldBeActive; } + + /** + Time in ms of last plot buffering. + You can use this information to invalidate your plot drawing after some number of ms. + */ + juce::int64 getLastDataUpdate() const { return lastData.load(); } + + /** + Call this to reset to the current time in ms. + */ + void resetLastDataFlag() { lastData.store (juce::Time::currentTimeMillis()); } + + /** + If your plot needs background processing, return here a pointer to your TimeSliceClient, + and it will automatically be added to the common background thread. + */ + virtual juce::TimeSliceClient* getBackgroundJob() { return nullptr; } + +protected: + double sampleRate = 0.0; + juce::AudioBuffer samples; + std::atomic writePosition; + bool triggeredPos = false; + bool triggeredNeg = false; + bool overlayPlots = false; // true => plot channels individually (optionally offset); false => plot one sum of specified channels + float plotOffset = 0; // for overlays only, offset used from plot to plot + bool normalize = false; // normalize each plot in overlay, or final sum when not overlaying + bool latch = false; + int plotChannel = 0; // first channel to plot + int numPlotChannels = 0; // number of channels to plot + int plotLength = 0; // fixed default plot length for every plot + int plotLengthNow = 0; // overrides plotLength when nonzero (limited to circular buffer size) + + inline void averageAllChannelsToSamplesChannel0(const juce::AudioBuffer& buffer) + { + int w = writePosition.load(); + const auto available = samples.getNumSamples() - w; + + const auto numSamples = buffer.getNumSamples(); + const auto numChannels = buffer.getNumChannels(); + jassert(numChannels > 0); + const auto gain = 1.0f / numChannels; + int topPlotChannel = std::min(numChannels, plotChannel + numPlotChannels) - 1; + if (available >= numSamples) + { + // samples.copyFrom (destChannel, destStartSample, ...) + samples.copyFrom (0, w, buffer.getReadPointer (plotChannel), numSamples, gain); + for (int c = plotChannel+1; c <= topPlotChannel; ++c) + samples.addFrom (0, w, buffer.getReadPointer (c), numSamples, gain); + } + else + { + samples.copyFrom (0, w, buffer.getReadPointer (plotChannel), available, gain); + samples.copyFrom (0, 0, buffer.getReadPointer (plotChannel), numSamples - available, gain); + for (int c = plotChannel + 1; c <= topPlotChannel; ++c) + { + samples.addFrom (0, w, buffer.getReadPointer (c), available, gain); + samples.addFrom (0, 0, buffer.getReadPointer (c), numSamples - available, gain); + } + } + } + + int getReadPosition(const float* data, const int pos0) + { + if (! triggeredPos && ! triggeredNeg) + return (pos0 >= 0 ? pos0 : pos0+samples.getNumSamples()); + + int posW = pos0; + if (posW < 0) + posW += samples.getNumSamples(); + int posP = posW; + int posN = posW; + int distP = 0; + int distN = 0; + + if (triggeredNeg) // search backward for negative-going zero-crossing + { // in circular plot-buffer samples, giving up after 50 ms <-> 20 Hz fundamental: + auto nonNeg = data [posN] >= 0.0f; + auto bail = int (sampleRate / 20.0f); + + while (nonNeg == false && --bail > 0) // search back to the last negative-going zero-crossing + { + if (--posN < 0) + posN += samples.getNumSamples(); + distP++; + nonNeg = data [posN] >= 0.0f; + } + } + if (triggeredPos) + { + auto nonPos = data [posP] <= 0.0f; + auto bail = samples.getNumSamples(); + while (nonPos == true && --bail > 0) // search back to the first positive-going zero-crossing + { + if (--posP < 0) + posP += samples.getNumSamples(); + distN++; + nonPos = data [posP] >= 0.0f; + } + if (bail==0) + DBG("Set samples-zero flag here and clear it in pushSamples"); + } + int pos; + if (triggeredPos && triggeredNeg) { + pos = (distN > distP ? posP : posN); + } else { + pos = (triggeredPos ? posP : posN); + } + if (pos < 0) + pos += samples.getNumSamples(); + return pos; + } + + /* internal utility for uniformly determining current plot length */ + int getNumToDisplay() { + if (plotLength <= 0) + setPlotLength(int (0.01 * sampleRate)); + jassert(samples.getNumSamples()>0); + int numToDisplay = (plotLengthNow > 0 ? std::min(plotLengthNow,samples.getNumSamples()) : plotLength); + return numToDisplay; + } + +private: + std::atomic lastData { 0 }; + bool active = true; + + JUCE_DECLARE_WEAK_REFERENCEABLE (MagicAudioPlotSource) + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MagicAudioPlotSource) +}; // MagicAudioPlotSource + +} // namespace foleys diff --git a/modules/foleys_gui_magic/Visualisers/foleys_MagicFilterPlot.cpp b/modules/foleys_gui_magic/Visualisers/foleys_MagicFilterPlot.cpp index 8bee54eb..4de09f9c 100644 --- a/modules/foleys_gui_magic/Visualisers/foleys_MagicFilterPlot.cpp +++ b/modules/foleys_gui_magic/Visualisers/foleys_MagicFilterPlot.cpp @@ -38,33 +38,53 @@ namespace foleys { -MagicFilterPlot::MagicFilterPlot() +static const int NUM_FREQS { 300 }; + +static void initFrequencies(std::vector& freqs, double freqMin, double freqMax) { - frequencies.resize (300); - for (size_t i = 0; i < frequencies.size(); ++i) - frequencies [i] = 20.0 * std::pow (2.0, i / 30.0); + unsigned long numFreqs = freqs.size(); + double freqRatio = std::pow( 2.0, ( std::log2(freqMax) - std::log2(freqMin) ) / double(numFreqs) ); + double freq = freqMin; + for (size_t i = 0; i < numFreqs-1; ++i) + { + freqs [i] = freq; + freq *= freqRatio; + } + freqs[numFreqs-1] = freqMax; // avoid roundoff error +} +MagicFilterPlot::MagicFilterPlot(double minFreqHz, double maxFreqHz) +{ + frequencies.resize (NUM_FREQS); + initFrequencies(frequencies, minFreqHz, maxFreqHz); magnitudes.resize (frequencies.size()); } -void MagicFilterPlot::setIIRCoefficients (juce::dsp::IIR::Coefficients::Ptr coefficients, float maxDBToDisplay) +MagicFilterPlot::MagicFilterPlot() : MagicFilterPlot(20.0, 20000.0) {} + +void MagicFilterPlot::setIIRCoefficients (juce::dsp::IIR::Coefficients::Ptr coefficients, float maxDBToDisplay, juce::String name) { + filterName = name; + if (sampleRate < 20.0) return; const juce::ScopedWriteLock writeLock (plotLock); + DBG("MagicFilterPlot:: setIIRCoefficients() for filter " << filterName); + maxDB = maxDBToDisplay; coefficients->getMagnitudeForFrequencyArray (frequencies.data(), magnitudes.data(), frequencies.size(), sampleRate); - resetLastDataFlag(); } -void MagicFilterPlot::setIIRCoefficients (float gain, std::vector::Ptr> coefficients, float maxDBToDisplay) +void MagicFilterPlot::setIIRCoefficients (float gain, std::vector::Ptr> coefficients, float maxDBToDisplay, juce::String name) { + filterName = name; + if (sampleRate < 20.0) return; @@ -96,21 +116,29 @@ void MagicFilterPlot::createPlotPaths (juce::Path& path, juce::Path& filledPath, const auto yFactor = 2.0f * bounds.getHeight() / juce::Decibels::decibelsToGain (maxDB); const auto xFactor = static_cast (bounds.getWidth()) / frequencies.size(); + DBG("MagicFilterPlot:: createPlotPaths() for filter " << getName()); + path.clear(); path.startNewSubPath (bounds.getX(), float (magnitudes [0] > 0 ? bounds.getCentreY() - yFactor * std::log (magnitudes [0]) / std::log (2) : bounds.getBottom())); for (size_t i=1; i < frequencies.size(); ++i) path.lineTo (float (bounds.getX() + i * xFactor), float (magnitudes [i] > 0 ? bounds.getCentreY() - yFactor * std::log (magnitudes [i]) / std::log (2) : bounds.getBottom())); +#if 1 + filledPath.clear(); // need for speed +#else filledPath = path; filledPath.lineTo (bounds.getBottomRight()); filledPath.lineTo (bounds.getBottomLeft()); filledPath.closeSubPath(); +#endif } void MagicFilterPlot::prepareToPlay (double sampleRateToUse, int) { - sampleRate = sampleRateToUse; + sampleRate = sampleRateToUse; + if (sampleRate/2.0 < 20000.0) + initFrequencies( frequencies, 20.0, sampleRate/2.0 ); } } // namespace foleys diff --git a/modules/foleys_gui_magic/Visualisers/foleys_MagicFilterPlot.h b/modules/foleys_gui_magic/Visualisers/foleys_MagicFilterPlot.h index cfd288cf..e6fe7cfa 100644 --- a/modules/foleys_gui_magic/Visualisers/foleys_MagicFilterPlot.h +++ b/modules/foleys_gui_magic/Visualisers/foleys_MagicFilterPlot.h @@ -49,13 +49,15 @@ class MagicFilterPlot : public MagicPlotSource MagicFilterPlot(); + MagicFilterPlot(double minFreqHz, double maxFreqHz); + /** Set new coefficients to calculate the frequency response from. @param coefficients the coefficients to calculate the frequency response for @param maxDB is the maximum level in dB, that the curve will display */ - void setIIRCoefficients (juce::dsp::IIR::Coefficients::Ptr coefficients, float maxDB); + void setIIRCoefficients (juce::dsp::IIR::Coefficients::Ptr coefficients, float maxDB, juce::String name="Unnamed Filter"); /** Set new coefficients to calculate the frequency response from. @@ -64,10 +66,15 @@ class MagicFilterPlot : public MagicPlotSource @param coefficients a vector of coefficients to sum up (multiply) to calculate the frequency response for @param maxDB is the maximum level in dB, that the curve will display */ - void setIIRCoefficients (float gain, std::vector::Ptr> coefficients, float maxDB); + void setIIRCoefficients (float gain, std::vector::Ptr> coefficients, float maxDB, juce::String name="Unnamed Filter"); + + /** + Gets the filter name, if any. + */ + juce::String getName() { return filterName; } /** - Does nothing in this class + Does nothing in this class, as the plotted samples are computed from the coefficients when they are set. */ void pushSamples (const juce::AudioBuffer& buffer) override; @@ -90,6 +97,7 @@ class MagicFilterPlot : public MagicPlotSource std::vector magnitudes; float maxDB = 100.0f; double sampleRate = 0.0; + juce::String filterName = { "Unnamed Filter" }; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MagicFilterPlot) }; diff --git a/modules/foleys_gui_magic/Visualisers/foleys_MagicOscilloscopeAudio.cpp b/modules/foleys_gui_magic/Visualisers/foleys_MagicOscilloscopeAudio.cpp new file mode 100644 index 00000000..c570399f --- /dev/null +++ b/modules/foleys_gui_magic/Visualisers/foleys_MagicOscilloscopeAudio.cpp @@ -0,0 +1,324 @@ +/* + ============================================================================== + Copyright (c) 2023 Julius Smith and Foleys Finest Audio - Daniel Walz + All rights reserved. + + **BSD 3-Clause License** + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + 3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + + ============================================================================== + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + OF THE POSSIBILITY OF SUCH DAMAGE. + ============================================================================== + */ + +#include "foleys_MagicOscilloscopeAudio.h" + +namespace foleys +{ + + +MagicOscilloscopeAudio::MagicOscilloscopeAudio (int channelToDisplay) + : MagicAudioPlotSource(channelToDisplay) +{ +} + +void MagicOscilloscopeAudio::checkAudioBufferForNaNs (juce::AudioBuffer& buffer) +{ // Check for and clear any NaNs in : + int nChans = buffer.getNumChannels(); + int nSamps = buffer.getNumSamples(); + int nNaNs = 0; // NaNs usually indicate parameters not getting set (no init, etc.) + for (int c=0; c0) { + std::cerr << "*** MagicOscilloscopeAudio.cpp: Have " << nNaNs << " NaNs!\n"; + } +} + +void MagicOscilloscopeAudio::pushSamples (const std::shared_ptr> bufSP, + int firstChannelToPlotIn, int numChannelsToPlotIn, int plotLengthIn) +{ + float* const* readPointers = (float*const*)(bufSP->getArrayOfReadPointers()); + int numChannelsIn = bufSP->getNumChannels(); + int firstChannelToPlot = std::min(firstChannelToPlotIn, numChannelsIn-1); + int numChansClipped = std::min(numChannelsToPlotIn,numChannelsIn-firstChannelToPlot); + // AudioBuffer (Type *const *dataToReferTo, int numChannelsToUse, int numSamples) + juce::AudioBuffer buffer(readPointers+firstChannelToPlot, numChansClipped, bufSP->getNumSamples() ); + pushSamples (buffer, plotLengthIn); +} + +void MagicOscilloscopeAudio::pushSamples (const juce::AudioBuffer& bufR, + int firstChannelToPlotIn, int numChannelsToPlotIn, int plotLengthIn) +{ + float* const* readPointers = (float*const*)(bufR.getArrayOfReadPointers()); + int numChannelsIn = bufR.getNumChannels(); + int firstChannelToPlot = std::min(firstChannelToPlotIn, numChannelsIn-1); + int numChansClipped = std::min(numChannelsToPlotIn,numChannelsIn-firstChannelToPlot); + // AudioBuffer (Type *const *dataToReferTo, int numChannelsToUse, int numSamples) + juce::AudioBuffer buffer(readPointers+firstChannelToPlot, numChansClipped, bufR.getNumSamples() ); + pushSamples (buffer, plotLengthIn); +} + +void MagicOscilloscopeAudio::pushSamples (const juce::AudioBuffer& buffer, int plotLengthIn) +{ + const int numSamples = buffer.getNumSamples(); + +#if DEBUG + float maxAmp = buffer.getMagnitude(0,numSamples); + if (maxAmp > 0.0f) { + // DBG("MagicOscilloscopeAudio::pushSamples: Buffer Nonzero"); + } +#endif + + plotLengthNow = std::max(0,plotLengthIn); + if ( plotLengthNow > 0 && writePosition.load() >= plotLengthNow ) // already have a plot waiting to go + return; + const int numChannelsIn = buffer.getNumChannels(); + int firstChannelToPlot = juce::jlimit(0,numChannelsIn-1,plotChannel); + int lastChannelToPlot = std::min ( numChannelsIn-1, firstChannelToPlot + numPlotChannels ); + if (overlayPlots) { + numChannelsOut = lastChannelToPlot - firstChannelToPlot + 1; + } else { + averageAllChannelsToSamplesChannel0(buffer); + numChannelsOut = 1; + } + + // juce::AudioBuffer* bufferP = &buffer; + std::vector firstAudibleSample (numChannelsIn); + std::vector lastAudibleSample (numChannelsIn); + int startSample = 0; + int numSamplesTrimmed = numSamples; + if (latch) { // When latching, we don't push samples when they are inaudible (least-work method) + // bufferP = std::unique_ptr>(numChannelsIn,numSamples); + if (buffer.hasBeenCleared()) + return; // push nothing - else find out if anything is audible: + // float magnitude = buffer.getMagnitude(/* startSample */ 0, numSamples); + // bool audible = (magnitude > 1.0E-4); // -80 dB threshold + bool audible = false; + for (int c=firstChannelToPlot; c<=lastChannelToPlot; c++) { + firstAudibleSample[c] = 0; + for (int s=0; s 1.0E-4) { // -80 dB threshold + firstAudibleSample[c] = s; + audible = true; + break; // This is faster than calling getMagnitude() + } + } + } + if (! audible) + return; + for (int c=firstChannelToPlot; c<=lastChannelToPlot; c++) { + lastAudibleSample[c] = firstAudibleSample[c]; + for (int s=numSamples-1; s>=firstAudibleSample[c]; s--) { + if (fabsf(buffer.getReadPointer(c)[s]) > 1.0E-4) { // -80 dB threshold + lastAudibleSample[c] = s; + break; + } + } + } + int firstAudibleSampleAllChannels = firstAudibleSample[firstChannelToPlot]; + int lastAudibleSampleAllChannels = lastAudibleSample[firstChannelToPlot]; + for (int c=firstChannelToPlot+1; c<=lastChannelToPlot; c++) { + firstAudibleSampleAllChannels = std::min ( firstAudibleSampleAllChannels, firstAudibleSample[c] ); + lastAudibleSampleAllChannels = std::max ( lastAudibleSampleAllChannels, lastAudibleSample[c] ); + } + startSample = firstAudibleSampleAllChannels; + numSamplesTrimmed = lastAudibleSampleAllChannels - firstAudibleSampleAllChannels + 1; + jassert (numSamplesTrimmed >= 0 && numSamplesTrimmed <= numSamples); + } + + // Copy buffer samples to circular plot buffer: + int w = writePosition.load(); // index of next write to samples ringbuffer + + const auto samplesBeforeWrap = samples.getNumSamples() - w; // Number of samples we can write without ringbuffer index wrap-around + if (plotLengthNow > 0 || samplesBeforeWrap >= numSamplesTrimmed) // We can copy without index-wrapping + { + // Copy all of the input buffer into our local ring buffer at its current write position w: + samples.copyFrom (0, w, buffer, firstChannelToPlot, startSample, numSamplesTrimmed); + if (numChannelsOut>1) // must also copy higher channels + { + if (numChannelsOut>samples.getNumChannels()) { + setNumChannels(numChannelsOut); + } + for (int c=firstChannelToPlot+1; c <= lastChannelToPlot; c++) + { + samples.copyFrom (c-firstChannelToPlot, w, buffer, c, startSample, numSamplesTrimmed); + } + } + } + else // must break up the copy into two pieces due to wraparound in the ring buffer: + { + samples.copyFrom (0, w, buffer, firstChannelToPlot, startSample, samplesBeforeWrap); + samples.copyFrom (0, 0, buffer, firstChannelToPlot, startSample + samplesBeforeWrap, numSamplesTrimmed - samplesBeforeWrap); + if (numChannelsOut>1) // must also copy higher channels + { + for (int c=firstChannelToPlot+1; c < firstChannelToPlot+numChannelsOut; c++) + { + samples.copyFrom (c-firstChannelToPlot, w, buffer, c, startSample, samplesBeforeWrap); + samples.copyFrom (c-firstChannelToPlot, 0, buffer, c, startSample + samplesBeforeWrap, numSamplesTrimmed - samplesBeforeWrap); + } + } + } + + checkAudioBufferForNaNs(samples); + + w += numSamplesTrimmed; + if (plotLengthNow == 0 && samplesBeforeWrap <= numSamplesTrimmed) + w -= samples.getNumSamples(); + writePosition.store (w); + resetLastDataFlag(); // store current time (ms) in lastData flag +} + +void MagicOscilloscopeAudio::createPlotPaths (juce::Path& path, juce::Path& filledPath, juce::Rectangle bounds, MagicAudioPlotComponent&) +{ + if (sampleRate < 20.0f || numPlotChannels < 1) + return; + + if (plotLengthNow>0 && writePosition.load() < plotLengthNow) + return; // Waiting for a complete plot to be available in plotLengthNow mode - use last plot in the meantime + + int numPlotSamplesAvailable = samples.getNumSamples(); + int numToDisplay = getNumToDisplay(); // either plotLength or plotLengthNow - defined in ./foleys_MagicAudioPlotSource.h + + auto* data = samples.getReadPointer (0); // samples holds channels "plotChannel" to "plotChannel + numPlotChannels-1" + + int pos0 = 0; + int pos = pos0; + if (plotLengthNow == 0) // no ringbuffer operation in this mode: + { + pos0 = writePosition.load() - numToDisplay; // plot most recent numToDisplay samples in ringbuffer +#if 0 + int nBufs = int(pos0 / numToDisplay); // number of full buffers in samples ringbuffer + pos = nBufs * numToDisplay; // start at the last one and stay synchronous +#else + pos = getReadPosition(data, pos0); // go back to previous zero-transition if in triggered mode +#endif + } + + // Normalize all plotted channels if requested: + if (normalize) { + for (int c=0; c(maxAmp, samples.getMagnitude(c,0,numToDisplay-numToEnd)); + } + if (maxAmp > 1.0e-4) { // let go at -80 dB + float ampScale = 1.0f / maxAmp; + if (pos+numToDisplay <= numPlotSamplesAvailable) { + samples.applyGain(c,pos,numToDisplay,ampScale); // assuming plotted sections do not overlap + } else { + int numToEnd = numPlotSamplesAvailable-pos; + samples.applyGain(c,pos,numToEnd,ampScale); + samples.applyGain(c,0,numToDisplay-numToEnd,ampScale); + } + } else { + // DBG("MagicOscilloscopeAudio::createPlotPaths: Signal is silent"); + } + } + } + + // Plot first channel: + + float plotMinX = bounds.getX(); + float plotMaxX = bounds.getRight(); + float plotMinY = bounds.getBottom(); + float plotMaxY = bounds.getY(); // (0,0) = upper-left corner => Min > Max in order to FLIP Y UPRIGHT + + float aPlotHeight = plotMaxY - plotMinY; // "algebraic" plot height - NEGATIVE since (0,0) is UPPER-left corner + float plotOffsetY = (overlayPlots ? 0.0f : plotOffset * aPlotHeight); + jassert(numPlotChannels>0); + // float plotScaleY = 1.0f / float(numPlotChannels); + // float plotHeightY = plotScaleY * aPlotHeight; // NEGATIVE - add overlapFactor? + + path.clear(); + path.startNewSubPath (plotMinX, juce::jmap (data [pos], -1.0f, 1.0f, plotMinY, plotMaxY)); // FLIPS Y + + for (int i = 1; i < numToDisplay; ++i) + { + ++pos; + if (pos >= numPlotSamplesAvailable) + pos -= numPlotSamplesAvailable; + + static bool sawNonzero = false; + if (! sawNonzero && data[pos] != 0.0f) + { + sawNonzero = true; + DBG("MagicOscilloscopeAudio::createPlotPaths: First nonzero sample to plot is " << data[pos]); + } + // FIXME: MAKE DOT-DASHED with 1 dot/channel, i.e., numPlotChannels dots per dash + // Draw next point of bottom plotted channel: + path.lineTo (juce::jmap (float (i), 0.0f, float (numToDisplay-1), plotMinX, plotMaxX), + juce::jmap (data [pos], -1.0f, 1.0f, plotMinY, plotMaxY)); + } // 1st channel plot completed + + // Fill below first-channel plot only: + filledPath = path; + filledPath.lineTo (plotMaxX,plotMinY); + filledPath.lineTo (plotMinX,plotMinY); + filledPath.closeSubPath(); // includes path.lineTo (plotMinX,data[pos]) + + // path.closeSubPath(); // draw from end of plot back to beginning (ok if both at minY or maxY) + + // Plot higher channels, if any: + for (int c=1; c= numPlotSamplesAvailable) + pos -= numPlotSamplesAvailable; + + // FIXME: MAKE DOT-DASHED with 1 dot/channel, i.e., numPlotChannels dots per dash + path.lineTo (juce::jmap (float (i), 0.0f, float (numToDisplay-1), plotMinX, plotMaxX), + juce::jmap (data [pos], -1.0f, 1.0f, plotMinY+c*plotOffsetY, plotMaxY+c*plotOffsetY)); + } + // FIXME: Consider fill here + // path.closeSubPath(); // draw from end of plot back to beginning (ok if both at minY or maxY) + } + + if (plotLengthNow > 0) + writePosition.store(0); // reset for next plot +} + +void MagicOscilloscopeAudio::prepareToPlay (double sampleRateToUse, int samplesPerBlockExpected) +{ + MagicAudioPlotSource::prepareToPlay(sampleRateToUse, samplesPerBlockExpected); + // Anything else needed goes here: +} + + +} // namespace foleys diff --git a/modules/foleys_gui_magic/Visualisers/foleys_MagicOscilloscopeAudio.h b/modules/foleys_gui_magic/Visualisers/foleys_MagicOscilloscopeAudio.h new file mode 100644 index 00000000..dfdf74ea --- /dev/null +++ b/modules/foleys_gui_magic/Visualisers/foleys_MagicOscilloscopeAudio.h @@ -0,0 +1,85 @@ +/* + ============================================================================== + Copyright (c) 2023 Julius Smith and Foleys Finest Audio - Daniel Walz + All rights reserved. + + **BSD 3-Clause License** + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + 3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + + ============================================================================== + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + OF THE POSSIBILITY OF SUCH DAMAGE. + ============================================================================== + */ + +#pragma once + +namespace foleys +{ + +class MagicAudioPlotComponent; + +/** + This class collects your samples in a circular buffer and allows the GUI to + draw it in the style of an oscilloscope + */ +class MagicOscilloscopeAudio : public MagicAudioPlotSource +{ +public: + + /** + Create an oscilloscope adapter to push samples into for later display in the GUI. + + @param channel lets you select the channel to analyse. -1 means summing all together (the default) + */ + MagicOscilloscopeAudio (int channelToDisplay=-1); + + static void checkAudioBufferForNaNs (juce::AudioBuffer& buffer); + + /** + Push samples of an AudioBuffer to be visualised. + */ + void pushSamples (const juce::AudioBuffer& buffer, int plotLength=0) override; + void pushSamples (const std::shared_ptr> bufSP, int channelToPlot=0, + int numChannelsToPlot=1, int plotLength=0) override; + void pushSamples (const juce::AudioBuffer& bufR, int channelToPlot, + int numChannelsToPlot, int plotLength=0) override; + + /** + This is the callback that creates the frequency plot for drawing. + + @param path is the path instance that is constructed by the MagicAudioPlotSource + @param filledPath is the path instance that is constructed by the MagicAudioPlotSource to be filled + @param bounds the bounds of the plot + @param component grants access to the plot component, e.g. to find the colours from it + */ + void createPlotPaths (juce::Path& path, juce::Path& filledPath, juce::Rectangle bounds, MagicAudioPlotComponent& component) override; + + void prepareToPlay (double sampleRate, int samplesPerBlockExpected) override; + +private: + int numChannelsOut = 0; // == numChannelsIn or 1 when channels are averaged + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MagicOscilloscopeAudio) +}; + +} // namespace foleys diff --git a/modules/foleys_gui_magic/Visualisers/foleys_MagicScatterPlot.cpp b/modules/foleys_gui_magic/Visualisers/foleys_MagicScatterPlot.cpp new file mode 100644 index 00000000..0877c8ad --- /dev/null +++ b/modules/foleys_gui_magic/Visualisers/foleys_MagicScatterPlot.cpp @@ -0,0 +1,156 @@ +/* + ============================================================================== + Copyright (c) 2023 Julius Smith and Foleys Finest Audio - Daniel Walz + All rights reserved. + + **BSD 3-Clause License** + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + 3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + + ============================================================================== + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + OF THE POSSIBILITY OF SUCH DAMAGE. + ============================================================================== + */ + +#include "foleys_MagicScatterPlot.h" + +namespace foleys +{ + +void MagicScatterPlot::pushSamples (const juce::AudioBuffer& bufferIn, int currentPlotLengthIn) +{ + int numChannels = bufferIn.getNumChannels(); + int chanX = std::min(0,numChannels-1); + int chanY = std::min(1,numChannels-1); + pushSamples(/* bufferX */ bufferIn, chanX, /* bufferY */ bufferIn, chanY, currentPlotLengthIn); +} + +void MagicScatterPlot::pushSamples (const juce::AudioBuffer& bufferX, int channelX, + const juce::AudioBuffer& bufferY, int channelY, + int plotLengthOverride) +{ + auto w = writePosition.load(); + + plotLengthNow = std::max(0,plotLengthOverride); + + const auto numSamples = bufferX.getNumSamples(); + jassert(numSamples == bufferY.getNumSamples()); + const auto available = samplesX.getNumSamples() - w; + + const auto numChannels = bufferX.getNumChannels(); + jassert(numChannels == bufferY.getNumChannels()); + + // plot (channelX,channelY): + + if (available >= numSamples) + { + samplesX.copyFrom (0, w, bufferX.getReadPointer (channelX), numSamples); + samplesY.copyFrom (0, w, bufferY.getReadPointer (channelY), numSamples); + } + else + { + samplesX.copyFrom (0, w, bufferX.getReadPointer (channelX), available); + samplesY.copyFrom (0, w, bufferY.getReadPointer (channelY), available); + samplesX.copyFrom (0, 0, bufferX.getReadPointer (channelX, available), numSamples - available); + samplesY.copyFrom (0, 0, bufferY.getReadPointer (channelY, available), numSamples - available); + } + + if (available > numSamples) + writePosition.store (w + numSamples); + else + writePosition.store (numSamples - available); + + resetLastDataFlag(); +} + +// ==================================================================================================== + +void MagicScatterPlot::createPlotPaths (juce::Path& path, juce::Path& filledPath, juce::Rectangle bounds, MagicAudioPlotComponent&) +{ + if (sampleRate < 20.0f) + return; + + const auto numToDisplay = getNumToDisplay(); // nominally plotLengthNow - defined in ./foleys_MagicAudioPlotSource.h + const auto* dataX = samplesX.getReadPointer (0); + const auto* dataY = samplesY.getReadPointer (0); + + auto position = writePosition.load() - numToDisplay; + if (position < 0) + position += samplesX.getNumSamples(); + + if (triggeredPos || triggeredNeg) // find first zero-crossing in circular plot-buffer samplesX, giving up after 50 ms <-> 20 Hz fundamental: + { + auto positive = dataX [position] > 0.0f; + auto bail = int (sampleRate / 20.0f); + + while (positive == false && --bail > 0) + { + if (--position < 0) + position += samplesX.getNumSamples(); + + positive = dataX [position] > 0.0f; + } + + while (positive == true && --bail > 0) + { + if (--position < 0) + position += samplesX.getNumSamples(); + + positive = dataX [position] > 0.0f; + } + } + + path.clear(); + path.startNewSubPath (juce::jmap (dataX [position], -1.0f, 1.0f, bounds.getX(), bounds.getRight()), + juce::jmap (dataY [position], -1.0f, 1.0f, bounds.getBottom(), bounds.getY())); + + for (int i = 1; i < numToDisplay; ++i) + { + ++position; + if (position >= samplesX.getNumSamples()) + position -= samplesX.getNumSamples(); + + path.lineTo (juce::jmap (dataX [position], -1.0f, 1.0f, bounds.getX(), bounds.getRight()), + juce::jmap (dataY [position], -1.0f, 1.0f, bounds.getBottom(), bounds.getY())); + } + + filledPath = path; + filledPath.lineTo (bounds.getBottomRight()); + filledPath.lineTo (bounds.getBottomLeft()); + filledPath.closeSubPath(); +} + +void MagicScatterPlot::prepareToPlay (double sampleRateToUse, int) +{ + sampleRate = sampleRateToUse; + + samplesX.setSize (1, static_cast (sampleRate)); + samplesX.clear(); + + samplesY.setSize (1, static_cast (sampleRate)); + samplesY.clear(); + + writePosition.store (0); +} + + +} // namespace foleys diff --git a/modules/foleys_gui_magic/Visualisers/foleys_MagicScatterPlot.h b/modules/foleys_gui_magic/Visualisers/foleys_MagicScatterPlot.h new file mode 100644 index 00000000..67602667 --- /dev/null +++ b/modules/foleys_gui_magic/Visualisers/foleys_MagicScatterPlot.h @@ -0,0 +1,93 @@ +/* + ============================================================================== + Copyright (c) 2023 Julius Smith and Foleys Finest Audio - Daniel Walz + All rights reserved. + + **BSD 3-Clause License** + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + 3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + + ============================================================================== + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + OF THE POSSIBILITY OF SUCH DAMAGE. + ============================================================================== + */ + +#pragma once + +namespace foleys +{ + +class MagicAudioPlotComponent; + +/** + This class collects two buffers of samples in a circular buffer and + allows the GUI to draw them in the style of an scatterplot, or + XY-plot. For example, sin(t) and cos(t) produce a circle. + */ +class MagicScatterPlot : public MagicAudioPlotSource +{ +public: + + /** + Create an XY ScatterPlot adapter to push samples into for later display in the GUI. + */ + MagicScatterPlot () : MagicAudioPlotSource() {} + + /** + Push samples to a buffer to be visualised as a scatterplot (XY plot) of channels 0 (X) and 1 (Y). + */ + void pushSamples (const juce::AudioBuffer& buffer, int currentPlotLength) override; + + /** + Push samples to a buffer to be visualised as a scatterplot (XY plot). + + @param bufferX is plotted as the X-axis coordinate. + @param bufferY is plotted as the Y-axis coordinate. + @param plotLength, if positive, gives the preferred length of + the next plot in samples (e.g., one period). + Otherwise, 10 ms of samples is plotted. + */ + void pushSamples (const juce::AudioBuffer& bufferX, int channelX, + const juce::AudioBuffer& bufferY, int channelY, + const int plotLengthOverride=0) override; + + /** + This is the callback that creates the frequency plot for drawing. + + @param path is the path instance that is constructed by the MagicPlotSource + @param filledPath is the path instance that is constructed by the MagicPlotSource to be filled + @param bounds the bounds of the plot + @param component grants access to the plot component, e.g. to find the colours from it + */ + virtual void createPlotPaths (juce::Path& path, juce::Path& filledPath, juce::Rectangle bounds, MagicAudioPlotComponent& component) override; + + virtual void prepareToPlay (double sampleRate, int samplesPerBlockExpected) override; + +private: + + juce::AudioBuffer samplesX; + juce::AudioBuffer samplesY; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MagicScatterPlot) +}; + +} // namespace foleys diff --git a/modules/foleys_gui_magic/Widgets/foleys_MagicAudioPlotComponent.cpp b/modules/foleys_gui_magic/Widgets/foleys_MagicAudioPlotComponent.cpp new file mode 100644 index 00000000..199acda3 --- /dev/null +++ b/modules/foleys_gui_magic/Widgets/foleys_MagicAudioPlotComponent.cpp @@ -0,0 +1,228 @@ +/* + ============================================================================== + Copyright (c) 2023 Julius Smith and Foleys Finest Audio - Daniel Walz + All rights reserved. + + **BSD 3-Clause License** + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + 3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + + ============================================================================== + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + OF THE POSSIBILITY OF SUCH DAMAGE. + ============================================================================== + */ + +namespace foleys +{ + + +MagicAudioPlotComponent::MagicAudioPlotComponent() +{ + setColour (plotColourId, juce::Colours::orange); + setColour (plotFillColourId, juce::Colours::orange.withAlpha (0.5f)); + setColour (plotInactiveColourId, juce::Colours::orange.darker()); + setColour (plotInactiveFillColourId, juce::Colours::orange.darker().withAlpha (0.5f)); + + setOpaque (false); + setPaintingIsUnclipped (true); +} + +void MagicAudioPlotComponent::setPlotSource (MagicAudioPlotSource* source) +{ + plotSource = source; +} + +void MagicAudioPlotComponent::setDecayFactor (float decayFactor) +{ + decay = decayFactor; + updateGlowBufferSize(); +} + +void MagicAudioPlotComponent::setGradientFromString (const juce::String& cssString, Stylesheet& stylesheet) +{ + if (cssString.isNotEmpty()) + { + gradient = std::make_unique(); + gradient->setup(cssString, stylesheet); + } + else + { + gradient.reset(); + } +} + +void MagicAudioPlotComponent::setTriggeredPos (bool t) +{ + triggeredPos = t; + if (plotSource) + plotSource->setTriggeredPos (triggeredPos); +} + +void MagicAudioPlotComponent::setTriggeredNeg (bool t) +{ + triggeredNeg = t; + if (plotSource) + plotSource->setTriggeredNeg (triggeredNeg); +} + +void MagicAudioPlotComponent::setOverlay (bool o) +{ + overlay = o; + if (plotSource) + plotSource->setOverlay (overlay); +} + +void MagicAudioPlotComponent::setNormalize (bool t) +{ + normalize = t; + if (plotSource) + plotSource->setNormalize (normalize); +} + +void MagicAudioPlotComponent::setLatch (bool t) +{ + latch = t; + if (plotSource) + plotSource->setLatch (latch); +} + +void MagicAudioPlotComponent::setChannel (int c) +{ + channel = c; + if (plotSource) + plotSource->setChannel (channel); +} + +void MagicAudioPlotComponent::setNumChannels (int nc) +{ + numChannels = nc; + if (plotSource) + plotSource->setNumChannels (numChannels); +} + +void MagicAudioPlotComponent::setPlotLength (int pl) +{ + plotLength = pl; + if (plotSource) + plotSource->setPlotLength (plotLength); +} + +void MagicAudioPlotComponent::setPlotOffset (float pl) +{ + plotOffset = pl; + if (plotSource) + plotSource->setPlotOffset (plotOffset); +} + +void MagicAudioPlotComponent::paint (juce::Graphics& g) +{ + if (plotSource == nullptr) + return; + + const auto lastUpdate = plotSource->getLastDataUpdate(); + if (lastUpdate > lastDataTimestamp) + { + if (plotSource) { // these may be have been set before plotSource existed: + plotSource->setTriggeredPos (triggeredPos); + plotSource->setTriggeredNeg (triggeredNeg); + plotSource->setOverlay (overlay); + plotSource->setNormalize (normalize); + plotSource->setLatch (latch); + plotSource->setChannel (channel); + plotSource->setNumChannels (numChannels); + plotSource->setPlotLength (plotLength); + plotSource->setPlotOffset (plotOffset); + } + plotSource->createPlotPaths (path, filledPath, getLocalBounds().toFloat(), *this); + lastDataTimestamp = lastUpdate; + } + + if (gradient) + gradient->setupGradientFill (g, getLocalBounds().toFloat()); + + if (! glowBuffer.isNull()) + drawPlotGlowing (g); + else + { + drawPlot (g); + } +} + +void MagicAudioPlotComponent::drawPlot (juce::Graphics& g) +{ + const auto active = plotSource->isActive(); + auto colour = findColour (active ? plotFillColourId : plotInactiveFillColourId); + + if (!gradient && colour.isTransparent() == false) + g.setColour (colour); + + if (gradient || colour.isTransparent()) + g.fillPath (filledPath); + + colour = findColour (active ? plotColourId : plotInactiveColourId); + if (colour.isTransparent() == false) + { + g.setColour (colour); + g.strokePath (path, juce::PathStrokeType (2.0)); + } +} + +void MagicAudioPlotComponent::drawPlotGlowing (juce::Graphics& g) +{ + if (decay < 1.0f) + glowBuffer.multiplyAllAlphas (decay); + + juce::Graphics glow (glowBuffer); + drawPlot (glow); + + g.drawImageAt (glowBuffer, 0, 0); +} + +void MagicAudioPlotComponent::updateGlowBufferSize() +{ + const auto w = getWidth(); + const auto h = getHeight(); + + if (decay > 0.0f && w > 0 && h > 0) + { + if (glowBuffer.getWidth() != w || glowBuffer.getHeight() != h) + glowBuffer = juce::Image (juce::Image::ARGB, w, h, true); + } + else + { + glowBuffer = juce::Image(); + } +} + +bool MagicAudioPlotComponent::needsUpdate() const +{ + return plotSource ? (lastDataTimestamp < plotSource->getLastDataUpdate()) : false; +} + +void MagicAudioPlotComponent::resized() +{ + lastDataTimestamp = 0; + updateGlowBufferSize(); +} + + +} // namespace foleys diff --git a/modules/foleys_gui_magic/Widgets/foleys_MagicAudioPlotComponent.h b/modules/foleys_gui_magic/Widgets/foleys_MagicAudioPlotComponent.h new file mode 100644 index 00000000..18049c55 --- /dev/null +++ b/modules/foleys_gui_magic/Widgets/foleys_MagicAudioPlotComponent.h @@ -0,0 +1,108 @@ +/* + ============================================================================== + Copyright (c) 2023 Julius Smith and Foleys Finest Audio - Daniel Walz + All rights reserved. + + **BSD 3-Clause License** + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + 3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + + ============================================================================== + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + OF THE POSSIBILITY OF SUCH DAMAGE. + ============================================================================== + */ + +#pragma once + +#include +#include "../Visualisers/foleys_MagicAudioPlotSource.h" + +namespace foleys +{ + +/** + The MagicAudioPlotComponent allows drawing the data from a MagicAudioPlotSource. + */ +class MagicAudioPlotComponent : public juce::Component, + juce::SettableTooltipClient +{ +public: + + enum ColourIds + { + plotColourId = 0x2001000, + plotInactiveColourId, + plotFillColourId, + plotInactiveFillColourId + }; + + MagicAudioPlotComponent(); + + void setPlotSource (MagicAudioPlotSource* source); + void setDecayFactor (float decayFactor); + void setGradientFromString (const juce::String& cssString, Stylesheet& stylesheet); + + void paint (juce::Graphics& g) override; + void resized() override; + + bool hitTest (int, int) override { return false; } + + bool needsUpdate() const; + + void setTriggeredPos (bool triggeredPos); + void setTriggeredNeg (bool triggeredNeg); + void setOverlay (bool overlay); + void setNormalize (bool triggered); + void setLatch (bool latch); + void setChannel (int channel); + void setNumChannels (int numChannels); + void setPlotLength (int plotLength); + void setPlotOffset (float plotOffset); + + private: + void drawPlot (juce::Graphics& g); + void drawPlotGlowing (juce::Graphics& g); + void updateGlowBufferSize(); + + juce::WeakReference plotSource; + juce::Path path; + juce::Path filledPath; + std::unique_ptr gradient; + + juce::int64 lastDataTimestamp = 0; + juce::Image glowBuffer; + float decay = 0.0f; + + bool triggeredPos = false; + bool triggeredNeg = false; + bool overlay = false; + bool normalize = false; + bool latch = false; + int channel = 0; + int numChannels = 0; + int plotLength = 0; + float plotOffset = 0; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MagicAudioPlotComponent) +}; + +} // namespace foleys diff --git a/modules/foleys_gui_magic/foleys_gui_magic.cpp b/modules/foleys_gui_magic/foleys_gui_magic.cpp index 3ec75f10..9be89a07 100644 --- a/modules/foleys_gui_magic/foleys_gui_magic.cpp +++ b/modules/foleys_gui_magic/foleys_gui_magic.cpp @@ -66,9 +66,12 @@ #include "Visualisers/foleys_MagicFilterPlot.cpp" #include "Visualisers/foleys_MagicAnalyser.cpp" #include "Visualisers/foleys_MagicOscilloscope.cpp" +#include "Visualisers/foleys_MagicOscilloscopeAudio.cpp" +#include "Visualisers/foleys_MagicScatterPlot.cpp" #include "Widgets/foleys_MagicLevelMeter.cpp" #include "Widgets/foleys_MagicPlotComponent.cpp" +#include "Widgets/foleys_MagicAudioPlotComponent.cpp" #include "Widgets/foleys_XYDragComponent.cpp" #include "Widgets/foleys_FileBrowserDialog.cpp" #include "Widgets/foleys_MidiLearnComponent.cpp" diff --git a/modules/foleys_gui_magic/foleys_gui_magic.h b/modules/foleys_gui_magic/foleys_gui_magic.h index 9d51cbd3..52d16b97 100644 --- a/modules/foleys_gui_magic/foleys_gui_magic.h +++ b/modules/foleys_gui_magic/foleys_gui_magic.h @@ -119,13 +119,19 @@ #include "Visualisers/foleys_MagicLevelSource.h" #include "Visualisers/foleys_MagicPlotSource.h" +#include "Visualisers/foleys_MagicAudioPlotSource.h" #include "Visualisers/foleys_MagicFilterPlot.h" #include "Visualisers/foleys_MagicAnalyser.h" #include "Visualisers/foleys_MagicOscilloscope.h" +#include "Visualisers/foleys_MagicOscilloscopeAudio.h" +#include "Visualisers/foleys_MagicScatterPlot.h" +// Helps with writing code compatible with older PGM versions: +#define HAVE_SCATTER_PLOT #include "Widgets/foleys_AutoOrientationSlider.h" #include "Widgets/foleys_MagicLevelMeter.h" #include "Widgets/foleys_MagicPlotComponent.h" +#include "Widgets/foleys_MagicAudioPlotComponent.h" #include "Widgets/foleys_XYDragComponent.h" #include "Widgets/foleys_FileBrowserDialog.h" #include "Widgets/foleys_MidiLearnComponent.h"