In this assignment you are asked to create a spectrum analysis plug-in. Through this assignment you will learn how to:
Feel free to go wild with your GUI and have fun! However, your implementation must have the following components:
In Projucer create a new audio-plugin project with PGM support like you did in 0.5.
Once you've set up your project, grab the starter code from the
class repository. 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 if you try to plot your fftPlot
, nothing
happens yet.
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. The spectral centoid is defined as \[ \text{Spectral Centroid} = \frac{\sum_{i=0}^N f_i * |X_i|}{\sum_{i=0}^N|X_i|} \] Where \( f_i\) is the frequency value and \( |X_i| \) is the FFT magnitude coefficient at index \( i \).
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.