This is a C implementation of the consensus contour algorithm described by Yoonseob Lim, Barbara Shinn-Cunningham and Tim Gardner. The basic algorithm is documented on his website as well as in the IEEE Sparse Contour Representations of Sound publication. This implementation was inspired by the acontour repository that contains a MATLAB implementation of the algorithm.
This implementation relies heavily on the macOS Accelerate framework (which uses vectorized instructions to optimize common calculations, such as the FFT and the complex ratio required for reassignment).
Currently, this code has a few limitations:
- The original consensus contour work suggests eliminating all but the longest contours. In order to reduce memory usage and extend this method to realtime audio processing, this code preserves all contours.
- This just returns a consensus contour spectrogram, but does not provide programmatic access to the identified contours.
Advantages to this code:
- The code supports both single and double precision.
- The code includes a mex file implementation to allow usage in MATLAB.
- For a 5 second audio file, the
ccontourimplementation is 68x faster than the
acontour(MATLAB) implementation. On most modern computers, it is sufficiently fast to calculate contours in realtime.
The API is modeled off the Accelerate framework, specifically using an opaque pointer to a structure to hold all the memory and pre-calculated values required for the algorithm. During setup, there is a configuration phase where a mutable opaque pointer is updated with configuration information used by the algorithm. Next, the configuration is turned into a setup, which acts as an immutable opaque pointer that has memory and pre-calculated values necessary for implementing the algorithm.
To get a default configuration, calll:
This configuration is a good, out-of-the box setup and can be used immediately. Alternatively, you can specify any of the following values:
void cccConfigSetFFTLength(CCCConfig config, unsigned long fft_length); // default: 1,024 // must be a power of 2 void cccConfigSetFFTShift(CCCConfig config, unsigned long fft_shift); // default: 19 void cccConfigSetWeightByPower(CCCConfig config, bool pow_weight); // default: true void cccConfigSetSampleRate(CCCConfig config, float fs); // default: 44,100 Hz void cccConfigSetTimescales(CCCConfig config, unsigned long num_timescales, const float timescales); // default: 0.5ms, 0.7ms, 0.9ms, 1.1ms, 1.3ms, 1.5ms, 1.7ms, 1.9ms, 2.1ms void cccConfigSetAngles(CCCConfig config, unsigned long num_angles, const float angles); // default: [eight angles in radians composing full circle]
Once you have completed configuration, you can turn the configuration into an immutable setup that allocates all memory needed for the algorithm and pre-calculates a number of values.
To perform the setup:
CCCSetup createCCCSetup(const CCCConfig config);
During the setup process, any necessary values are copied and the configuration is no longer required. You can de-allocate the configuration by calling:
void destroyCCCConfig(CCCConfig config);
You can then use the
CCCSetup variable to calculate contours for a single column (the setup will hold information from the previous column, so you will need one setup per signal that will be processed simultaneously). To do so, pass a pointer to the start of the signal (where the signal has at least
fft_length elements) and a pointer to the output vector (that has
fft_length / 2 elements):
void cccColumn(const CCCSetup setup, const float *signal, float *contour);
You can also calculate the full spectrogram for a signal in one call. The program provides a
struct ConsensusContourSize type that contains size information to help in pre-allocating the output memory. To get the required dimensions, call:
struct ConsensusContourSize dim = cccSizeSetup(const CCCSetup setup, const unsigned long signal_len);
struct ConsensusContourSize contains fields
dim.bytes telling you how many bytes are required for the full spectrogram, as well as
dim.rows (the number of rows based on the
fft_shift) and the
dim.cols (equal to
fft_length / 2).
Once you allocate an output vector with sufficient space (see
bytes above), you can calculate the spectrogram all at once:
void cccSpectrogram(const CCCSetup setup, const struct ConsensusContourSize dim, const float *signal, float *consensus_contours);
Behind the scenes, this calls
cccColumn while moving through the signal.
Note that both
cccSpectrogram will zero out the output vector before proceeding.
The library provides a helper function for calculating the frequency and time bins that correspond with the rows and columns in the spectrogram. To generate these values, use:
void cccBins(const CCCSetup setup, const struct ConsensusContourSize dim, float *freqs, float *times);
Note that you must allocate
freqs with sufficient space to contain
malloc(sizeof(float) * dim.rows)) and
times with sufficient space to contain
malloc(sizeof(float) * dim.cols)). Either
freqs can be NULL, if you are just interested in the values for the other parameter. Note that the returned frequency bins are in Hz; the returned time bins are in seconds and correspond with the center of the FFT window.
Finally, when you are done with the setup, you can release all allocated memory by calling:
void destroyCCCSetupD(CCCSetup setup);
To use the MATLAB mex function, you must first compile it. Again, because of the dependency on the Accelerate framework, this is only supported for macOS.
To compile, run:
Once compiled, you can run:
[consensus_spectrogram, f, t] = ccontour(signal, fs);
signal is a 1D vector of type
fs is the sample rate in Hz. The function returns a consensus contour spectrogram matrix, where rows correspond with frequency bins (enumerated in optional output
f in Hz) and where columns correspond with time bins (enumerated in optional output
t in seconds, corresponding with the center of the bin).
You can optionally pass additional configuration parameters as character value pairs. For help on all options available via the MATLAB interface, use the help function: