In this assignment you are asked to create a spectrum analysis plug-in, including the following:
dsp
moduleFeel free to go wild with your GUI and have fun! However, your implementation should have the following components:
In Projucer, create a new JUCE audio plugin project with PGM support like you did in the previous assignment on PGM.
Once you've set up your project, grab the
hwFFT starter code.
Review
the differences between the
Projucer-generated PluginProcessor.h/cpp
and
the starter code, which now inherits
from foleys::MagicProcessor
instead
of juce::AudioProcessor
directly. In the
starter code we have hooked up your fftData
and frequencies
arrays to a new and incomplete
Visualizer
class. Upon building and running
your plugin, you should see the PGM GUI editor, but the
PGM Plot
display is blank even
though fftPlot
is connected as
the source
.
The following refers to the 2023 version of the starter code. We need to update it to refer to the 2024 version, which gives you a bit more to start with. Until then, start with the 2023 version, and switch to the 2024 version as your actual starting code, perhaps checking out the diffs as needed along the way.
In your starter code you have been provided with the files Visualizer.h
and
Visualizer.cpp
that implement the class Visualizer
. This class is derived
from foleys::MagicPlotSource
. The documentation for the parent class can be found here.
The Visualizer
class has two private members
which are float
pointers: data
and x
.
void createPlotPaths (juce::Path& path, juce::Path& filledPath, juce::Rectangle bounds, foleys::MagicPlotComponent& component);
which generates the plot of data
vs x
.
The function createPlotPaths
updates the
arguments path
and filledPath
,
and these updates are used
in foleys::MagicPlotComponent
's paint
method.
The bounds
argument is
a juce::Rectangle
containing the bounding
box of your plot. Methods such as getX()
and getY()
will be useful for getting its
location on the screen and drawing your plot. The
juce::Rectangle
class
reference documentation is useful. Every JUCE
Component
,
which represents a rectangular region on the screen,
uses juce::Rectangle
to hold its bounds.
See the
Point, Line, and Rectangle Classes Tutorial if
anything is not clear from the reference doc
for Rectangle
.
It is up to you to implement this method. A helpful example is the implementation of foleys::MagicFilterPlot
.
If you choose to do a straightforward plot, please
plot data
versus x
on a log-log
scale with your magnitude spectra in dB. This will make
your plot look more like what we hear.
After implementing your method, check it using
the frequencies
array generated
in prepareToPlay
for (int i = 0; i < FFT_SIZE/2; ++i)
frequencies[i] = i;
and the fftData
array generated in processBlock
for (int i = 0; i < FFT_SIZE/2; ++i)
fftData[i] = std::pow(10, 3)*std::sin(24.8* i/FFT_SIZE);
are now plotting in your GUI. Does your plot make sense given the generated data?
Now that you have a working plot, we want to feed it real-world data by using the JUCE library FFT. Carefully read the class documentation and review this tutorial.
After capturing your FFT data, plot (at least) the magnitude spectrum in a way that is clear, and please make sure to window your data!
Allocate the necessary buffers in PluginProcessor.h
and implement your signal processing code in PluginProcessor.cpp
in the processBlock
method. Update the fftData
array with your FFT data. Change the freq
array instantiation with the correct frequency lines values.
At this point you should have a working spectrum analyzer! But it could be better... it could be controllable!
In this section you will add parameters to your plugin, which will allow you to control the time smoothing of your FFT plot and display the spectral centroid. See this tutorial for a more in depth guide on how to add parameter and parameter listeners.
The first parameter you will implement is a leaky integrator that smooths the output of your FFT data by computing a running mean. The leaky integrator is a one-pole lowpass filter with unity gain at dc and is defined by a smoothing time-constant \(\tau\) in seconds.
Determine the leakage factor \( \lambda \) (which is the pole of your filter) as a function of \(\tau \), the sampling rate and your FFT buffer size. Your FFT update equation is then \[ FFT_{old} = \lambda * FFT_{old} + (1 - \lambda)* FFT_{new}\] Where \( FFT_{old} \) stores updated the time-averaged FFT plot and \( FFT_{new} \) is the incoming FFT data.
After implementing your leaky integrator, try hard-coding a \( \tau = \) 10 ms into you code. After compiling your code you should see less jitter and a smoother plot in your spectrum analyzer.
To change the value of \( \tau \) you will need to instantiate an AudioParameter
in the provided createParameterLayout
function.
layout.add(std::make_unique< juce::AudioParameterFloat >(paramID, paramName, juce::NormalisableRange< float >(min, max, step), default));
This line of code adds a new floating point audio parameter juce::AudioParameterFloat
with a unique string ID paramID
and string name paramName
. Note that paramID
must be a unique string and is used by the value tree to correctly identify parameters. paramName
will be the name that comes up in your PGM editor. See more documentation on AudioParameters and the different types of parameters here.
juce::NormalisableRange<float>
creates a floating point range based on a minimum value, maximum value, and step size. The default
value is the value the parameter is initialized as. Documentation on the range class can be found here.
After creating your parameter, it is important to add a listener to your value tree state. This listener will notify the plugin when the value of your parameter is externally changed. Implement the listener in your plugin's constructor.
treeState.addListener(paramID, this)
paramID
is once again your parameter's unique string identifier. this
is a pointer to the current class and does not need to be edited.
When your parameter changes, the function parameterChanged(const juce::String &parameterID, float newValue)
is automatically called. The newValue
for a certain parameter with unqiue parameterID
is passed into this function. Based on what parameter string is passed we can update the behavior of our plugin.
In the case of our leaky integrator, say we get a new value of \( \tau \) from our GUI, in parameterChanged
we then update the value of \( \lambda \) that is used in our FFT update equation.
if (parameterID == "tau")
{
// Update lambda
}
You are now going to compute and print the spectral centroid in your plugin. You can think of the spectral centroid as a measure of brightness or center of gravity of the spectral magnitude. It may be defined as \[ \text{Spectral Centroid} = \frac{\sum_{i=0}^N f_i * |X_i|^2}{\sum_{i=0}^N|X_i|^2} \] Where \( f_i\) is the \(i\)th frequency and \( |X_i| \) is the FFT magnitude coefficient at frequency index \( i \). Many authors do not square the spectral magnitude samples, but doing so gives results proportional to power rather than amplitude.
In physics, the centroid of a uniform mass distribution gives its center of mass and may be calculated as the first moment of the mass distribution about the origin (the numerator in our spectral centroid calculation) divided by its total mass (our denominator). Thus, spectral power corresponds to mass distributed across frequencies.
Add a spectral centroid parameter to your parameter layout, but do not create a listener. This is because we don't want our spectral centroid value to change as the result of an external control. Parameters can be manually updated using the following code:
juce::Value paramToEdit = treeState.getParameterAsValue(paramID);
paramToEdit = myCalculatedValue;
This code creates a juce::Value
object that can be can be updated internally and will then update the AudioParameter
value in our GUI.
In your processBlock
function, compute your spectral centroid after computing your FFT. Use getParameterAsValue()
to create a juce::Value
item based spectral centroid paramID
. By assigning your computed spectral centroid value to the juce::Value
item you will update the GUI parameter.
Use the PGM Label
item and attach it to your spectral centroid parameter to print the spectral centroid in your plugin.
Now that you have a working smoothing paramter add some new parameters of your choice! Some ideas:
Congrats! You now have your own working spectrum analyzer. Play around with it and submit a video of you having some fun. Make sure that you can clearly correlate what you are doing audio-wise to what is happening on screen.
dsp
module introClass recordings disussing this assignment last year: