Assignment 1: Seeing Frequencies

Due: April 15th, 2022
Deliverable: A video of you interacting with your spectrum analyzer
Email a link to your video to the teaching team (jos@ccrma.stanford.edu and champ@ccrma.stanford.edu)

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:

Getting Started

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.

1 The Visualizer

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.

The meat of the class, and your task, is the function:
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?

2 FFT

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!

3 Adding Parameters

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.

3.1 Smoothing with the Leaky Integrator

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.

3.2 Controlling the Averaging Time ( \( \tau \) )

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
}

3.3 Spectral Centroid

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.

3.4 More parameters, (hopefully not) more problems

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.

Handy References

Visualizer

FFT

Parameters