Example 2: Frequency-Domain Filtering

Introduction

This example shows how to apply a filter to an audio file using
the Gaborator library, by turning the audio into spectrogram
coefficients, modifying the coefficients, and resynthesizing audio
from them.

The specific filter implemented here is a 3 dB/octave lowpass
filter. This is sometimes called a pinking filter because it
can be used to produce pink noise from white noise. In practice, the
3 dB/octave slope is only applied above some minimum frequency, for
example 20 Hz, because otherwise the gain of the filter would approach
infinity as the frequency approaches 0, and the impulse response would
have to be infinitely wide.

Since the slope of this filter is not a multiple of 6 dB/octave, it
is difficult to implement as an analog filter, but by filtering
digitally in the frequency domain, arbitrary filter responses such as
this can easily be achieved.

Spectrum Analysis Parameters

The spectrum analysis works much the same as in Example 1,
but uses slightly different parameters.
We use a larger number of frequency bands per octave (100)
to minimize ripple in the frequency response, and the
reference frequency argument is omitted as we don't care about the
exact alignment of the bands with respect to a musical scale.

Precalculating Gains

The filtering will be done by multiplying each spectrogram
coefficient with a frequency-dependent gain. To avoid having to
calculate the gain on the fly for each coefficient, which would
be slow, we will precalculate the gains into a vector band_gains
of one gain value per band, including one for the
special lowpass band that contains the frequencies from 0 to 20 Hz.

std::vector<float> band_gains(analyzer.bands_end());

First, we calculate the gains for the bandpass bands.
For a 3 dB/octave lowpass filter, the voltage gain needs to be
proportional to the square root of the inverse of the frequency.
To get the frequency of each band, we call the
analyzer method band_ff(), which
returns the center frequency of the band in units of the
sampling frequency. The gain is normalized to unity at 20 Hz.

De-interleaving

To handle stereo and other multi-channel audio files,
we will loop over the channels and filter each channel separately.
Since libsndfile produces interleaved samples, we first
de-interleave the current channel into a temporary vector called
channel:

Spectrum Analysis

Filtering

The filtering is done using the function
gaborator::apply(), which applies a user-defined function to
each spectrogram coefficient. Here, that user-defined function is a
lambda expression that multiplies the coefficient by the appropriate
precalculated frequency-dependent gain, modifying the coefficient in
place. The unused int64_t argument is the time in units
of samples; this could be use to implement a time-varying filter if desired.

Resynthesis

We can now resynthesize audio from the filtered coefficients by
calling synthesize(). This is a mirror image of the call to
analyze(): now the coefficients are the input, and
the buffer of samples is the output. The channel
vector that originally contained the input samples for the channel
is now reused to hold the output samples.

analyzer.synthesize(coefs, 0, channel.size(), channel.data());

Re-interleaving

The audio vector that contained the
original interleaved audio is reused for the interleaved
filtered audio. This concludes the loop over the channels.

Writing the Audio

The filtered audio is written using libsndfile,
using code that closely mirrors that for reading.
Note that we use SFC_SET_CLIPPING
to make sure that any samples too loud for the file format
will saturate; by default, libsndfile makes them
wrap around, which sounds really bad.