Hi all,
I'm working on a tool to batch generate spectrograms for a bunch of sound files. I'm using rustfft to generate the fourier transforms, and then plotting them using plotters.
Currently my plotting code is not very sophisticated - it basically splits the DrawingArea into freq x n_samples rectangles and then colors them individually for scaling. There are a number of issues with this approach, but it works as a hacky MVP.
pub fn plot_spectrogram<DB: DrawingBackend>(spectrogram: &Array2<f32>, drawing_area: &DrawingArea<DB, Shift>) {
// get some dimensions for drawing
// The shape is in [nrows, ncols], meaning [n_samples, n_freqbins], but we want to transpose this
// so that in our graph, x = time, y = frequency.
let (num_samples, num_freq_bins) = match spectrogram.shape() {
&[num_rows, num_columns] => (num_rows, num_columns),
_ => panic!("Spectrogram is a {}D array, expected a 2D array.", spectrogram.ndim())
};
println!("...from a spectrogram with {} samples x {} frequency bins.", num_samples, num_freq_bins);
let spectrogram_cells = drawing_area.split_evenly((num_freq_bins, num_samples));
// Scaling values
let windows_scaled = spectrogram.map(|i| i.abs()/(num_freq_bins as f32));
let highest_spectral_density = windows_scaled.max_skipnan();
// transpose and flip around to prepare for graphing
/* the array is currently oriented like this:
t = 0 |
|
|
|
|
t = n +-------------------
f = 0 f = m
so it needs to be flipped...
t = 0 |
|
|
|
|
t = n +-------------------
f = m f = 0
...and transposed...
f = m |
|
|
|
|
f = 0 +-------------------
t = 0 t = n
... in order to look like a proper spectrogram
*/
let windows_flipped = windows_scaled.slice(ndarray::s![.., ..; -1]); // flips the
let windows_flipped = windows_flipped.t();
// Finally add a color scale
let color_scale = colorous::MAGMA;
// fill the cells with the appropriate color
for (cell, spectral_density) in spectrogram_cells.iter().zip(windows_flipped.iter()) {
let spectral_density_scaled = spectral_density.sqrt() / highest_spectral_density.sqrt();
let color = color_scale.eval_continuous(spectral_density_scaled as f64);
cell.fill(&RGBColor(color.r, color.g, color.b)).unwrap();
};
}
One issue with this approach is that I'm plotting the frequencies on a linear scale, not on a logarithmic scale. Compare these two spectrograms, one using sonogram, one generated with my tool:
(sound clip is the last 15 seconds of Aphex Twin's Windowlicker)
Most spectrograms are using a logarithmic scale, because human perception of pitch is logarithmic. But how do I do that with plotters? I see that plotters has some provision for log scaling with the LogCoord struct and similar, but if I'm trying to create an image, rather than something like a line graph, do I need to do something special to fill in an area, rather than just plot a point?
And if there's a better-suited library for this type of plotting, I'd love to know that as well.

