Assignment 2: Making Filterbanks

Due: TBD
Deliverable: A video of you interacting with your filterbank 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 analyzer, but now using Faust instead of the juce FFT library. Through this assignment you will learn how to:

As usual, feel free to go wild! However, your implementation must have the following components:

Getting Started

This project will build off of Assignment 1. Your GUI from the previous assignment can be reused. Just make sure to remove all references to the JUCE FFT library. You will now be generating your data via Faust.

1. Filterbanks

To analyze our input signal we will use the Faust library's mth_octave_analyzer functions from their analyzers.lib library.

1.1 Mth-Octave Analyzer

The Mth-Octave Analyzer implements a filterbank which splits the signal into a parallel bank of signals, one for each spectral band. For our purposes, we will use the default analyzer which in turn uses bandpass filters based off of 6th-order elliptic filters:

mth_octave_analyzer_default = mth_octave_analyzer6e;

mth_octave_analyzer_default has three parameters:

With M equal to 1 we will produce N octave bands, starting with a highpass band crossing over to the next band below at frequency ftop, and band-splitting downward from there by octaves. To see the block diagram of this filter bank, say (at the CLI)
faust2firefox ~/faust/examples/filtering/filterBank.dsp
and click down three levels to see the octave filter-bank architecture.

For a simpler example, consider:

process = an.mth_octave_analyzer_default(1, 10000, 2);
which results in the following block-diagram:

This process splits the spectrum into two bands having a crossover frequency at ftop = 10 kHz. In this case, only a lowpass and highpass are used (no bandpass sections). If we increase N to 3, then the lowpass band gets split into a new lowpass cutting off at 5 kHz, introducing a bandpass from 5 to 10 kHz, and so forth

The following Faust program can be adapted to check your filter bank in Python, Octave, or MATLAB, etc.:

import("stdfaust.lib");

N = 3;
fTop = 10000;
bandsPerOctave = 1;
displayOffsetDB = 5;

process = 1-1' : an.mth_octave_analyzer_default(bandsPerOctave, fTop, N)
: par(k,N,*(pow(10.0,(k*displayOffsetDB)/20.0)));
This Faust program, as written, outputs three signals containing the band impulse-responses.
Taking an FFT and displaying them overlaid yields the following magnitude frequency response overlay:

The parameter M will change the size of the octave band being used. This can be used to implement fractional octave smoothing. If we wanted 1/3-octave smoothing we would then set M to 3. With M equal to 3 and N both equal to 5 we produce three 1/3rd octave bands between 10 kHz and 20 kHz with two additional highpass and lowpass bands above 20 kHz and below 10 kHz.

Try implementing an.mth_octave_analyzer_default(3, 10000, 14) and listening to the different channels!

1.2 si.smooth Operator

In our previous assignment we used a leaky integrator to smooth the output of FFT. Here we will do the same using a combination of the si.smooth() and ba.tau2pole() functions.

si.smooth is in Faust's signals library and implements a dc-unity gain one-pole lowpass filter, which is essentially our leaky integrator. We need to provide the si.smooth() function with our pole location. To do this we call ba.tau2pole() and provide our time constant to the function. Since we want to smooth N bands we use the par integrator

par(i, N,si.smooth(ba.tau2pole(tau))
Since we want to take the power of our signal before smoothing we'll use the par iterator and an absolute value before we smooth our functions par(i, N, abs(_)^2).

1.3 hslider

We want to control our \(\tau\) parameter like before. Add an hslider to your Faust code.

tau = hslider("smoothTime", 0, 0, 10, 0.1)*0.001;

Implement a your filterbank in Faust and compile it to C++ and add it into your JUCE project as in Assignment 1.5.

2. JUCE

It could be helpful to delete the old FFT size macros and replace them with macros that define the size of our Faust filterbank

#define N 12 // yourNumberOfBands
#define M 3 // yourBandSlices

We also need to create new float arrays to use in our visualizer plot:

float fbData[N];
float frequencies[N];
Attach fbData to your Visualizer in your plugin constructor. In prepareToPlay calculate the center frequencies of your frequency bands. Hint: look at the Faust implementation of an.mth_octave_analyzer

To store the output of Faust filterbank we will declare a juce::AudioBuffer<float>private member. Based on our Faust code our audio buffer member should have N channels (the number filterbank bands) and match the number of samples of our input buffer. So we will declare

juce::AudioBuffer<float> fbBuffer

in our header file. And in prepareToPlay we will set our buffer size

fbBuffer.setSize(nBands, samplesPerBlock)

Now we can allocate a double pointer, say float **faustOutputPtr in our header, and in prepareToPlay attach your Faust audio buffer to this pointer

faustOutputPtr = fbBuffer.getArrayOfWritePointers(); 

Use this buffer and pointer combination to call our compute function on the first channel of our input in processBlock

fDSP->compute(buffer.getNumSamples(), buffer.getArrayOfWritePointers(), faustOutputPtr);

3. Last Steps

After calling compute we need to write our output to our filterbank plot

for (int band = 0; band < N; band++)
{
    for (int samp = 0; samp < buffer.getNumSamples(), samp++)
    {
        fbData[bin] = faustOutputPtrs[bin][samp];
        plot->update();
    }
}
Now we can update our filterbank plot at every sample! Trying adding in the other parameters your used in your previous assignment to your Faust code and hooking them up in JUCE!