From 5f62f8241ba744d9f5046198c832fab09561f51c Mon Sep 17 00:00:00 2001 From: Julius Smith Date: Thu, 3 Aug 2023 03:14:03 -0700 Subject: [PATCH 01/12] scatter-plot-for-main: add scatter-plot to main branch --- .../Visualisers/foleys_MagicScatterPlot.cpp | 163 ++++++++++++++++++ .../Visualisers/foleys_MagicScatterPlot.h | 96 +++++++++++ modules/foleys_gui_magic/foleys_gui_magic.h | 1 + 3 files changed, 260 insertions(+) create mode 100644 modules/foleys_gui_magic/Visualisers/foleys_MagicScatterPlot.cpp create mode 100644 modules/foleys_gui_magic/Visualisers/foleys_MagicScatterPlot.h 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..4b1d8b60 --- /dev/null +++ b/modules/foleys_gui_magic/Visualisers/foleys_MagicScatterPlot.cpp @@ -0,0 +1,163 @@ +/* + ============================================================================== + Copyright (c) 2019-2021 Foleys Finest Audio - Daniel Walz + All rights reserved. + + License for non-commercial projects: + + 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. + + License for commercial products: + + To sell commercial products containing this module, you are required to buy a + License from https://foleysfinest.com/developer/pluginguimagic/ + + 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; + } + } + + // FIXME: Sum channels here if X and Y are multichannel and overlay is false + + 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())); + } + + // FIXME: Make more paths here if X and Y are multichannel and overlay is true + + 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..45a8e893 --- /dev/null +++ b/modules/foleys_gui_magic/Visualisers/foleys_MagicScatterPlot.h @@ -0,0 +1,96 @@ +/* + ============================================================================== + Copyright (c) 2019-2021 Foleys Finest Audio - Daniel Walz + All rights reserved. + + License for non-commercial projects: + + 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. + + License for commercial products: + + To sell commercial products containing this module, you are required to buy a + License from https://foleysfinest.com/developer/pluginguimagic/ + + 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/foleys_gui_magic.h b/modules/foleys_gui_magic/foleys_gui_magic.h index 9d51cbd3..db5e47b3 100644 --- a/modules/foleys_gui_magic/foleys_gui_magic.h +++ b/modules/foleys_gui_magic/foleys_gui_magic.h @@ -122,6 +122,7 @@ #include "Visualisers/foleys_MagicFilterPlot.h" #include "Visualisers/foleys_MagicAnalyser.h" #include "Visualisers/foleys_MagicOscilloscope.h" +#include "Visualisers/foleys_MagicScatterPlot.h" #include "Widgets/foleys_AutoOrientationSlider.h" #include "Widgets/foleys_MagicLevelMeter.h" From 1851883dcaf0eed3b8b5d54d5cce0f786fffab8b Mon Sep 17 00:00:00 2001 From: Julius Smith Date: Thu, 3 Aug 2023 03:21:55 -0700 Subject: [PATCH 02/12] scatter-plot-for-main: update copyright text --- .../Visualisers/foleys_MagicScatterPlot.cpp | 13 +++++-------- .../Visualisers/foleys_MagicScatterPlot.h | 9 +++------ 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/modules/foleys_gui_magic/Visualisers/foleys_MagicScatterPlot.cpp b/modules/foleys_gui_magic/Visualisers/foleys_MagicScatterPlot.cpp index 4b1d8b60..bf80163e 100644 --- a/modules/foleys_gui_magic/Visualisers/foleys_MagicScatterPlot.cpp +++ b/modules/foleys_gui_magic/Visualisers/foleys_MagicScatterPlot.cpp @@ -1,9 +1,9 @@ /* ============================================================================== - Copyright (c) 2019-2021 Foleys Finest Audio - Daniel Walz + Copyright (c) 2023 Julius Smith and Foleys Finest Audio - Daniel Walz All rights reserved. - License for non-commercial projects: + **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: @@ -16,10 +16,7 @@ may be used to endorse or promote products derived from this software without specific prior written permission. - License for commercial products: - - To sell commercial products containing this module, you are required to buy a - License from https://foleysfinest.com/developer/pluginguimagic/ + ============================================================================== 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 @@ -122,7 +119,7 @@ void MagicScatterPlot::createPlotPaths (juce::Path& path, juce::Path& filledPath } } - // FIXME: Sum channels here if X and Y are multichannel and overlay is false + // ToDo: Sum channels here if X and Y are multichannel and overlay is false path.clear(); path.startNewSubPath (juce::jmap (dataX [position], -1.0f, 1.0f, bounds.getX(), bounds.getRight()), @@ -138,7 +135,7 @@ void MagicScatterPlot::createPlotPaths (juce::Path& path, juce::Path& filledPath juce::jmap (dataY [position], -1.0f, 1.0f, bounds.getBottom(), bounds.getY())); } - // FIXME: Make more paths here if X and Y are multichannel and overlay is true + // ToDo: Make more paths here if X and Y are multichannel and overlay is true filledPath = path; filledPath.lineTo (bounds.getBottomRight()); diff --git a/modules/foleys_gui_magic/Visualisers/foleys_MagicScatterPlot.h b/modules/foleys_gui_magic/Visualisers/foleys_MagicScatterPlot.h index 45a8e893..67602667 100644 --- a/modules/foleys_gui_magic/Visualisers/foleys_MagicScatterPlot.h +++ b/modules/foleys_gui_magic/Visualisers/foleys_MagicScatterPlot.h @@ -1,9 +1,9 @@ /* ============================================================================== - Copyright (c) 2019-2021 Foleys Finest Audio - Daniel Walz + Copyright (c) 2023 Julius Smith and Foleys Finest Audio - Daniel Walz All rights reserved. - License for non-commercial projects: + **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: @@ -16,10 +16,7 @@ may be used to endorse or promote products derived from this software without specific prior written permission. - License for commercial products: - - To sell commercial products containing this module, you are required to buy a - License from https://foleysfinest.com/developer/pluginguimagic/ + ============================================================================== 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 From ad879c2df2180714ca9b6605f947a525d8b1c58d Mon Sep 17 00:00:00 2001 From: Julius Smith Date: Thu, 3 Aug 2023 03:27:00 -0700 Subject: [PATCH 03/12] scatter-plot-for-main: remove unnecessary comments suggesting future generalization --- .../foleys_gui_magic/Visualisers/foleys_MagicScatterPlot.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/modules/foleys_gui_magic/Visualisers/foleys_MagicScatterPlot.cpp b/modules/foleys_gui_magic/Visualisers/foleys_MagicScatterPlot.cpp index bf80163e..0877c8ad 100644 --- a/modules/foleys_gui_magic/Visualisers/foleys_MagicScatterPlot.cpp +++ b/modules/foleys_gui_magic/Visualisers/foleys_MagicScatterPlot.cpp @@ -119,8 +119,6 @@ void MagicScatterPlot::createPlotPaths (juce::Path& path, juce::Path& filledPath } } - // ToDo: Sum channels here if X and Y are multichannel and overlay is false - 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())); @@ -135,8 +133,6 @@ void MagicScatterPlot::createPlotPaths (juce::Path& path, juce::Path& filledPath juce::jmap (dataY [position], -1.0f, 1.0f, bounds.getBottom(), bounds.getY())); } - // ToDo: Make more paths here if X and Y are multichannel and overlay is true - filledPath = path; filledPath.lineTo (bounds.getBottomRight()); filledPath.lineTo (bounds.getBottomLeft()); From 19fb2de12987e028dc2862164d00b1dff5eca191 Mon Sep 17 00:00:00 2001 From: Julius Smith Date: Thu, 3 Aug 2023 03:52:07 -0700 Subject: [PATCH 04/12] scatter-plot-for-main: add MagicAudioPlotSource required by MagicScatterPlot --- .../SignalGenerator/SignalGenerator.jucer | 3 +- .../Visualisers/foleys_MagicAudioPlotSource.h | 343 ++++++++++++++++++ modules/foleys_gui_magic/foleys_gui_magic.h | 1 + 3 files changed, 346 insertions(+), 1 deletion(-) create mode 100644 modules/foleys_gui_magic/Visualisers/foleys_MagicAudioPlotSource.h 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"> +#include + +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: + + /** 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 (not triggeredPos and not 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/foleys_gui_magic.h b/modules/foleys_gui_magic/foleys_gui_magic.h index db5e47b3..e62326a8 100644 --- a/modules/foleys_gui_magic/foleys_gui_magic.h +++ b/modules/foleys_gui_magic/foleys_gui_magic.h @@ -119,6 +119,7 @@ #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" From f05908e733c29dae46bd7b098a0b9e5dd60a818c Mon Sep 17 00:00:00 2001 From: Julius Smith Date: Thu, 3 Aug 2023 05:35:46 -0700 Subject: [PATCH 05/12] scatter-plot-for-main: update copyright --- .../Visualisers/foleys_MagicAudioPlotSource.h | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/modules/foleys_gui_magic/Visualisers/foleys_MagicAudioPlotSource.h b/modules/foleys_gui_magic/Visualisers/foleys_MagicAudioPlotSource.h index 28361c4b..e661e350 100644 --- a/modules/foleys_gui_magic/Visualisers/foleys_MagicAudioPlotSource.h +++ b/modules/foleys_gui_magic/Visualisers/foleys_MagicAudioPlotSource.h @@ -1,9 +1,9 @@ /* ============================================================================== - Copyright (c) 2019-2021 Foleys Finest Audio - Daniel Walz + Copyright (c) 2023 Julius Smith and Foleys Finest Audio - Daniel Walz All rights reserved. - License for non-commercial projects: + **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: @@ -16,10 +16,7 @@ may be used to endorse or promote products derived from this software without specific prior written permission. - License for commercial products: - - To sell commercial products containing this module, you are required to buy a - License from https://foleysfinest.com/developer/pluginguimagic/ + ============================================================================== 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 @@ -39,6 +36,20 @@ #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 { @@ -52,7 +63,7 @@ class MagicAudioPlotSource // not worth the trouble : public MagicPlotSource { public: - /** Constructor. */ + /** Default Constructor. */ MagicAudioPlotSource()=default; /** Constructor allowing specification of a channel to display, or -1 to indicate all channels. */ From 60b9577ea213ab0dc5559d892fc2c3008f2c3acc Mon Sep 17 00:00:00 2001 From: Julius Smith Date: Thu, 3 Aug 2023 06:01:05 -0700 Subject: [PATCH 06/12] scatter-plot-for-main: Might as well MagicOscilloscopeAudio and MagicAudioPlotComponent since MagicAudioPlotSource was needed - they do fit well into a single PR --- .../foleys_MagicOscilloscopeAudio.cpp | 324 ++++++++++++++++++ .../foleys_MagicOscilloscopeAudio.h | 85 +++++ .../foleys_MagicAudioPlotComponent.cpp | 228 ++++++++++++ .../Widgets/foleys_MagicAudioPlotComponent.h | 108 ++++++ modules/foleys_gui_magic/foleys_gui_magic.cpp | 3 + modules/foleys_gui_magic/foleys_gui_magic.h | 4 + 6 files changed, 752 insertions(+) create mode 100644 modules/foleys_gui_magic/Visualisers/foleys_MagicOscilloscopeAudio.cpp create mode 100644 modules/foleys_gui_magic/Visualisers/foleys_MagicOscilloscopeAudio.h create mode 100644 modules/foleys_gui_magic/Widgets/foleys_MagicAudioPlotComponent.cpp create mode 100644 modules/foleys_gui_magic/Widgets/foleys_MagicAudioPlotComponent.h 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..14901495 --- /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; + int firstAudibleSample[numChannelsIn]; + int 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 (not 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 (not 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/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 e62326a8..52d16b97 100644 --- a/modules/foleys_gui_magic/foleys_gui_magic.h +++ b/modules/foleys_gui_magic/foleys_gui_magic.h @@ -123,11 +123,15 @@ #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" From 78212ae3953fc8aafbe1ed54b20b6510c3ffe299 Mon Sep 17 00:00:00 2001 From: Julius Smith Date: Thu, 3 Aug 2023 06:13:05 -0700 Subject: [PATCH 07/12] scatter-plot-for-main: also need AudioPlotItem, unless we decide to merge everything into PlotItem --- .../General/foleys_MagicJUCEFactories.cpp | 116 ++++++++++++++++++ .../General/foleys_StringDefinitions.h | 1 + 2 files changed, 117 insertions(+) diff --git a/modules/foleys_gui_magic/General/foleys_MagicJUCEFactories.cpp b/modules/foleys_gui_magic/General/foleys_MagicJUCEFactories.cpp index 4c49f6ce..10e4f655 100644 --- a/modules/foleys_gui_magic/General/foleys_MagicJUCEFactories.cpp +++ b/modules/foleys_gui_magic/General/foleys_MagicJUCEFactories.cpp @@ -39,6 +39,7 @@ #include "../Widgets/foleys_XYDragComponent.h" #include "../Widgets/foleys_MagicLevelMeter.h" #include "../Widgets/foleys_MagicPlotComponent.h" +#include "../Widgets/foleys_MagicAudioPlotComponent.h" #include "../Widgets/foleys_MidiLearnComponent.h" #include "../Widgets/foleys_MidiDrumpadComponent.h" #include "../Helpers/foleys_PopupMenuHelper.h" @@ -593,6 +594,120 @@ const juce::Identifier PlotItem::pGradient {"plot-gradient"}; //============================================================================== +class AudioPlotItem : public GuiItem //? : public PlotItem +{ +public: + + FOLEYS_DECLARE_GUI_FACTORY (AudioPlotItem) + + static const juce::Identifier pDecay; + static const juce::Identifier pGradient; + + static const juce::Identifier pTriggeredPos; + static const juce::Identifier pTriggeredNeg; + static const juce::Identifier pOverlay; + static const juce::Identifier pNormalize; + static const juce::Identifier pLatch; + static const juce::Identifier pChannel; + static const juce::Identifier pNumChannels; + static const juce::Identifier pPlotLength; + static const juce::Identifier pPlotOffset; + + AudioPlotItem (MagicGUIBuilder& builder, const juce::ValueTree& node) : GuiItem (builder, node) //? PlotItem (builder, node) { } + { + setColourTranslation ( + { + { "plot-color", MagicAudioPlotComponent::plotColourId }, + { "plot-fill-color", MagicAudioPlotComponent::plotFillColourId }, + { "plot-inactive-color", MagicAudioPlotComponent::plotInactiveColourId }, + { "plot-inactive-fill-color", MagicAudioPlotComponent::plotInactiveFillColourId } + }); + + addAndMakeVisible (plot); + } + + void update() override // and REPLACE, since plotSource must be our specific derived class MagicAudioPlotSource + { + auto sourceID = configNode.getProperty (IDs::source, juce::String()).toString(); + if (sourceID.isNotEmpty()) + plot.setPlotSource (getMagicState().getObjectWithType(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" }; From 93a85c004f69a8c83617962e6a2f0541a5e7c2f4 Mon Sep 17 00:00:00 2001 From: Julius Smith Date: Thu, 3 Aug 2023 06:32:00 -0700 Subject: [PATCH 08/12] scatter-plot-for-main: change not to '!' --- .../Visualisers/foleys_MagicAudioPlotSource.h | 2 +- .../Visualisers/foleys_MagicOscilloscopeAudio.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/foleys_gui_magic/Visualisers/foleys_MagicAudioPlotSource.h b/modules/foleys_gui_magic/Visualisers/foleys_MagicAudioPlotSource.h index e661e350..5247963a 100644 --- a/modules/foleys_gui_magic/Visualisers/foleys_MagicAudioPlotSource.h +++ b/modules/foleys_gui_magic/Visualisers/foleys_MagicAudioPlotSource.h @@ -285,7 +285,7 @@ class MagicAudioPlotSource // not worth the trouble : public MagicPlotSource int getReadPosition(const float* data, const int pos0) { - if (not triggeredPos and not triggeredNeg) + if (! triggeredPos and ! triggeredNeg) return (pos0 >= 0 ? pos0 : pos0+samples.getNumSamples()); int posW = pos0; diff --git a/modules/foleys_gui_magic/Visualisers/foleys_MagicOscilloscopeAudio.cpp b/modules/foleys_gui_magic/Visualisers/foleys_MagicOscilloscopeAudio.cpp index 14901495..58db4af4 100644 --- a/modules/foleys_gui_magic/Visualisers/foleys_MagicOscilloscopeAudio.cpp +++ b/modules/foleys_gui_magic/Visualisers/foleys_MagicOscilloscopeAudio.cpp @@ -131,7 +131,7 @@ void MagicOscilloscopeAudio::pushSamples (const juce::AudioBuffer& buffer } } } - if (not audible) + if (! audible) return; for (int c=firstChannelToPlot; c<=lastChannelToPlot; c++) { lastAudibleSample[c] = firstAudibleSample[c]; @@ -270,7 +270,7 @@ void MagicOscilloscopeAudio::createPlotPaths (juce::Path& path, juce::Path& fill pos -= numPlotSamplesAvailable; static bool sawNonzero = false; - if (not sawNonzero && data[pos] != 0.0f) + if (! sawNonzero && data[pos] != 0.0f) { sawNonzero = true; DBG("MagicOscilloscopeAudio::createPlotPaths: First nonzero sample to plot is " << data[pos]); From 01ba080344088595adf663bba6364b7b081dcd6f Mon Sep 17 00:00:00 2001 From: Julius Smith Date: Thu, 3 Aug 2023 08:01:57 -0700 Subject: [PATCH 09/12] scatter-plot-for-main: Windows does not allow writable arrays on the stack --- .../Visualisers/foleys_MagicOscilloscopeAudio.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/foleys_gui_magic/Visualisers/foleys_MagicOscilloscopeAudio.cpp b/modules/foleys_gui_magic/Visualisers/foleys_MagicOscilloscopeAudio.cpp index 58db4af4..c570399f 100644 --- a/modules/foleys_gui_magic/Visualisers/foleys_MagicOscilloscopeAudio.cpp +++ b/modules/foleys_gui_magic/Visualisers/foleys_MagicOscilloscopeAudio.cpp @@ -110,8 +110,8 @@ void MagicOscilloscopeAudio::pushSamples (const juce::AudioBuffer& buffer } // juce::AudioBuffer* bufferP = &buffer; - int firstAudibleSample[numChannelsIn]; - int lastAudibleSample[numChannelsIn]; + 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) From dc8e7c7be8f61a151668236e6c7f645ff4fd5e73 Mon Sep 17 00:00:00 2001 From: Julius Smith Date: Thu, 3 Aug 2023 08:42:58 -0700 Subject: [PATCH 10/12] scatter-plot-for-main: must say '&&' not 'and' for Windows --- .../foleys_gui_magic/Visualisers/foleys_MagicAudioPlotSource.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/foleys_gui_magic/Visualisers/foleys_MagicAudioPlotSource.h b/modules/foleys_gui_magic/Visualisers/foleys_MagicAudioPlotSource.h index 5247963a..f07c11e2 100644 --- a/modules/foleys_gui_magic/Visualisers/foleys_MagicAudioPlotSource.h +++ b/modules/foleys_gui_magic/Visualisers/foleys_MagicAudioPlotSource.h @@ -285,7 +285,7 @@ class MagicAudioPlotSource // not worth the trouble : public MagicPlotSource int getReadPosition(const float* data, const int pos0) { - if (! triggeredPos and ! triggeredNeg) + if (! triggeredPos && ! triggeredNeg) return (pos0 >= 0 ? pos0 : pos0+samples.getNumSamples()); int posW = pos0; From ed58452138ebefb092b9e67293d7e492101fd7d7 Mon Sep 17 00:00:00 2001 From: Julius Smith <2200853+josmithiii@users.noreply.github.com> Date: Sun, 7 Jul 2024 08:34:24 -0700 Subject: [PATCH 11/12] scatter-plot-for-main: MagicFilterPlot: add plotID feature for checking frequency-response calculations in DEBUG mode --- .../Visualisers/foleys_MagicFilterPlot.cpp | 8 ++++++-- .../foleys_gui_magic/Visualisers/foleys_MagicFilterPlot.h | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/modules/foleys_gui_magic/Visualisers/foleys_MagicFilterPlot.cpp b/modules/foleys_gui_magic/Visualisers/foleys_MagicFilterPlot.cpp index 8bee54eb..be9d2140 100644 --- a/modules/foleys_gui_magic/Visualisers/foleys_MagicFilterPlot.cpp +++ b/modules/foleys_gui_magic/Visualisers/foleys_MagicFilterPlot.cpp @@ -47,8 +47,10 @@ MagicFilterPlot::MagicFilterPlot() magnitudes.resize (frequencies.size()); } -void MagicFilterPlot::setIIRCoefficients (juce::dsp::IIR::Coefficients::Ptr coefficients, float maxDBToDisplay) +void MagicFilterPlot::setIIRCoefficients (juce::dsp::IIR::Coefficients::Ptr coefficients, float maxDBToDisplay, juce::String plotID) { + DBG("MagicFilterPlot::setIIRCoefficients: Computing magnitude frequency response for filter `" << plotID << "'"); + if (sampleRate < 20.0) return; @@ -63,8 +65,10 @@ void MagicFilterPlot::setIIRCoefficients (juce::dsp::IIR::Coefficients::P 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 plotID) { + DBG("MagicFilterPlot::setIIRCoefficients: Computing magnitude frequency response for filter `" << plotID << "'"); + if (sampleRate < 20.0) return; diff --git a/modules/foleys_gui_magic/Visualisers/foleys_MagicFilterPlot.h b/modules/foleys_gui_magic/Visualisers/foleys_MagicFilterPlot.h index cfd288cf..6802ba6b 100644 --- a/modules/foleys_gui_magic/Visualisers/foleys_MagicFilterPlot.h +++ b/modules/foleys_gui_magic/Visualisers/foleys_MagicFilterPlot.h @@ -55,7 +55,7 @@ class MagicFilterPlot : public MagicPlotSource @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 plotID="Unnamed Filter"); /** Set new coefficients to calculate the frequency response from. @@ -64,7 +64,7 @@ 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 plotID="Unnamed Filter"); /** Does nothing in this class From 7051031216d7050184ae476ed26683ed7dac2167 Mon Sep 17 00:00:00 2001 From: Julius Smith <2200853+josmithiii@users.noreply.github.com> Date: Sun, 7 Jul 2024 08:42:18 -0700 Subject: [PATCH 12/12] scatter-plot-for-main: MagicFilterPlot: some small niceties --- .../Visualisers/foleys_MagicFilterPlot.cpp | 44 ++++++++++++++----- .../Visualisers/foleys_MagicFilterPlot.h | 14 ++++-- 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/modules/foleys_gui_magic/Visualisers/foleys_MagicFilterPlot.cpp b/modules/foleys_gui_magic/Visualisers/foleys_MagicFilterPlot.cpp index be9d2140..4de09f9c 100644 --- a/modules/foleys_gui_magic/Visualisers/foleys_MagicFilterPlot.cpp +++ b/modules/foleys_gui_magic/Visualisers/foleys_MagicFilterPlot.cpp @@ -38,36 +38,52 @@ 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, juce::String plotID) +MagicFilterPlot::MagicFilterPlot() : MagicFilterPlot(20.0, 20000.0) {} + +void MagicFilterPlot::setIIRCoefficients (juce::dsp::IIR::Coefficients::Ptr coefficients, float maxDBToDisplay, juce::String name) { - DBG("MagicFilterPlot::setIIRCoefficients: Computing magnitude frequency response for filter `" << plotID << "'"); + 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, juce::String plotID) +void MagicFilterPlot::setIIRCoefficients (float gain, std::vector::Ptr> coefficients, float maxDBToDisplay, juce::String name) { - DBG("MagicFilterPlot::setIIRCoefficients: Computing magnitude frequency response for filter `" << plotID << "'"); + filterName = name; if (sampleRate < 20.0) return; @@ -100,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 6802ba6b..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, juce::String plotID="Unnamed Filter"); + 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, juce::String plotID="Unnamed Filter"); + 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) };