In this assignment you are asked to create a spectrum analyzer, but now using Faust instead of the JUCE FFT library. This assignment introduces the following topics:
As usual, feel free to go wild with added features! However, your implementation must have the following components:
This project will build off of Assignment 1-FFT. Your GUI from that assignment can be reused. However, be sure to remove all references to the JUCE FFT library. You will now be generating your spectrum data using a filter-bank from the Faust Libraries.
To analyze our input signal we can use the Faust
library's mth_octave_analyzer
functions from their analyzers.lib
library.
Note that in the Faust Libraries, a filter bank is
defined as an analyzer with delay compensation.
The delay compensation allows the filter bands to be added
back together without phase anomalies at the crossover
frequencies between bands. We are only doing display of
spectral power, so we do not need delay compensation, so
we use an analyzer. That said, feel free to
use fi.filterbank
instead and experiment with
real-time constant-Q audio processing effects.
The default Mth-Octave Analyzer in the Faust
Libaries, an.mth_octave_analyzer_default(M,fTop,N)
,
implements
a constant-Q
audio filter bank which splits the signal into a
parallel bank of signals, one for each spectral band, with
the band-splits done using 6th-order elliptic bandpass
filters.
mth_octave_analyzer_default
has three parameters:
M
= number of band-slices per octave (1,2,3,...) fTop
= upper bandlimit in Hz of the Mth-octave bands (< ma.SR/2
)N
= total number of bands = number of output signalsN
is a compile-time
parameter used in
Faust's
recursive pattern-matching while fTop
and M
are free to vary at run-time,
although it would be very strange to vary M
and not N
. In practice, one can precompile
the Faust code for all values of N
needed,
and switch among them at run time (muting the audio
during the switch, of course).
While mth_octave_analyzer_default
is hardwired
to use a 6th-order elliptic lowpass/highpass pair for each
band split (the strongest and sharpest available in the
Faust Libraries), the more
general
an.mth_octave_analyzer(Order,M,fTop,N)
will use a Butterworth band-split, for any
odd Order
, where the Order
is a
compile-time constant like number-of-bands N
.
Special cases of Butterworth band-splits are used in
Linkwitz-Riley audio crossover filters. Order 3 gives a
nice and gently rolling-off set of audio bands, which can be
preferred when using fi.filterbank()
to process
audio. For spectrum analyzers, however, the 6th-order
elliptic band-splits are normally preferable, unless they
are too expensive computationally for real-time performance
on small CPUs.
With M=1
we get N
octave
bands, starting with a highpass band crossing over to the next band below at frequency
ftop
Hz, and band-splitting downward from there by octaves.
To see the block diagram of this filter bank, say (in a shell)
faust2firefox ~/faust/examples/filtering/filterBank.dsp
and click down three levels to see the octave filter-bank architecture.
(You will not use the delay compensation delayeq
unless you plan to recombine the channels.)
For a simple example, consider a 2-band octave-analyzer having its single crossover frequency at 10 kHz:
process = an.mth_octave_analyzer_default(1, 10000, 2);
This gives following block-diagram:process
function splits the input
signal into two bands at crossover
frequency ftop
= 10 kHz. In this case, only a
lowpass and highpass are used (no internal bandpass
sections). If we increase N
to 3, then the
lowpass band gets split into a new lowpass/highpass pair
crossing over at 5 kHz, creating a bandpass from 5 to 10
kHz, and so forth.
The following complete Faust program can be used to check your filter-bank in Python, Octave, or MATLAB, etc.:
import("stdfaust.lib");
bBands = 3;
fTop = 10000; // Hz
bandsPerOctave = 1;
displayOffsetDB = 5;
impulse = 1-1';
analyzer = an.mth_octave_analyzer_default(bandsPerOctave, fTop, nBands);
displayScaling = par(k,nBands,*(pow(10.0,(k*displayOffsetDB)/20.0)));
process = impulse : analyzer : displayScaling;
This Faust program emits three signals containing the three band
impulse-responses.faust2octave
output, e.g., yields the following
magnitude frequency-response overlay:
The third-octave filter-bank has been
a basic
staple for audio spectrum analysis since the early days of Bell
Labs more than a century ago (implemented in analog then, of
course). Setting bandsPerOctave = 3
above gives us a
third-octave filter bank.
The number of bands nBands
then determines how low our analysis goes in frequency.
Try implementing an.mth_octave_analyzer_default(3, 10000, 14)
and listening to the different channels!
(On a Mac, faust2caqt
is a convenient script for creating a Faust standalone app for that.)
si.smooth
Operator
In our previous assignment we used a leaky integrator written in C++ to smooth the FFT power.
Now we will do the same using a combination of Faust library functions
si.smooth()
and ba.tau2pole()
.
si.smooth
is in Faust's library signals.lib
, 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 prefer to smooth squared amplitude to obtain a short-term average
power, we can use the par
primitive:
par(i, N, ^(2))
.
hslider
We want to control our \(\tau\) parameter like before. Add an hslider
to your Faust code.
tau = hslider("smoothTime [unit: ms]", 0, 0, 10, 0.1) * 0.001;
The "[unit: ms]" substring generates
Faust meta-data
in the compiler output that does nothing unless the architecture file supports it.
Implement your analyzer in Faust, compile it to C++, and add it to your JUCE project as in Assignment 1.5.
It could be helpful to delete the old FFT size macros and replace them with macros that define the size of our Faust filter-bank
#define N 12 // your number-of-bands
#define M 3 // your number-of-bands-per-octave
If you want to change these at run time, it is necessary
to manage a set of separate Faust compilations, one for
each N
(and ditto for the filter-order if you
want to change that).
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's filter-bank we will
declare a juce::AudioBuffer<float>
private
member. Based on our Faust code, our audio buffer member
should have N
channels (the number filter-bank
bands) and match the number of samples of our input
buffer. So we declare
juce::AudioBuffer<float> fbBuffer
in our header file, and in prepareToPlay
we 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 the 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);
After calling compute
we need to write our output to our analyzer 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 spectrum plot every sample!