Skip to content

Spectrogram on a logarithmic frequency axis

The spectrogram of an exponential sine sweep from 20 Hz to 20 kHz played through a loudspeaker that adds harmonic distortion, as in an acoustic measurement. The sweep rises by the same number of decades every second, so on a logarithmic frequency axis it is a straight line, and its second, third and fourth harmonics are parallel lines above it. The power is drawn as a surface in a two-dimensional axes, whose nodes are placed through the logarithmic scale. An image could not be drawn here: a raster of evenly spaced pixels has no placement on a logarithmic axis, and validation refuses an image whose plane has one.

Spectrogram on a logarithmic frequency axis

The figure above is live, drawn in the browser by the same engine that draws the viewer: drag it to pan, and click it to zoom with the wheel. Download the PDF for the vector figure that IronLAB exports, or read how to embed a figure of your own.

Source

The figure is built by the following code.

use ironlab::prelude::*;

pub const TITLE: &str = "Spectrogram on a logarithmic frequency axis";
pub const DESCRIPTION: &str = "The spectrogram of an exponential sine sweep from 20 Hz to 20 kHz played through a \
     loudspeaker that adds harmonic distortion, as in an acoustic measurement. The sweep rises by the same number \
     of decades every second, so on a logarithmic frequency axis it is a straight line, and its second, third and \
     fourth harmonics are parallel lines above it. The power is drawn as a surface in a two-dimensional axes, whose \
     nodes are placed through the logarithmic scale. An image could not be drawn here: a raster of evenly spaced \
     pixels has no placement on a logarithmic axis, and validation refuses an image whose plane has one.";

/// The duration of the sweep in seconds.
const DURATION_S: f64 = 8.0;

/// The frequencies at which the sweep starts and ends, in hertz.
const START_HZ: f64 = 20.0;
const END_HZ: f64 = 20_000.0;

/// The power of each harmonic relative to the fundamental, in decibels, starting with the fundamental itself.
const HARMONICS_DB: [f64; 4] = [0.0, -25.0, -35.0, -45.0];

/// The relative bandwidth of the analysis: a constant-Q analysis resolves a tone into a peak whose standard
/// deviation is a fixed fraction of its frequency.
const RELATIVE_BANDWIDTH: f64 = 0.02;

/// The noise floor of the measurement, in decibels relative to the fundamental.
const NOISE_FLOOR_DB: f64 = -70.0;

/// The frequency of the sweep at time `t` in seconds: it rises exponentially, by the same ratio every second.
fn sweep_hz(t: f64) -> f64 {
    START_HZ * (END_HZ / START_HZ).powf(t / DURATION_S)
}

/// The power at time `t` in seconds and frequency `f` in hertz, in decibels relative to the fundamental: a peak at
/// each harmonic of the sweep above the noise floor.
fn power_db(t: f64, f: f64) -> f64 {
    let fundamental = sweep_hz(t);
    let tones: f64 = HARMONICS_DB
        .iter()
        .enumerate()
        .map(|(k, level_db)| {
            let centre = (k + 1) as f64 * fundamental;
            let sigma = RELATIVE_BANDWIDTH * centre;
            10f64.powf(level_db / 10.0) * (-0.5 * ((f - centre) / sigma).powi(2)).exp()
        })
        .sum();
    10.0 * (tones + 10f64.powf(NOISE_FLOOR_DB / 10.0)).log10()
}

pub fn figure() -> Figure {
    // The analysis is evaluated 30 times a second and at frequencies spaced evenly in their logarithm, 80 to a
    // decade.
    let time_s = linspace(0.0, DURATION_S, 241);
    let frequency_hz = logspace(START_HZ.log10(), END_HZ.log10(), 241);
    let power = Matrix::from_fn(frequency_hz.len(), time_s.len(), |row, col| {
        power_db(time_s[col], frequency_hz[row])
    });

    let mut fig = Figure::new()
        .size_mm(160.0, 90.0)
        .title("Spectrogram of an exponential sine sweep with harmonic distortion")
        .tag("surface")
        .tag("log")
        .tag("pcolor")
        .tag("signals")
        .tag("spectrogram")
        .parameter("dimensionality", "2D")
        .parameter("artists", 1)
        .parameter("data_points", 58_563)
        .parameter("has_legend", false);
    let mut ax = fig.axes(0, 0);
    // A surface in a two-dimensional axes is a pseudocolour plot. Its field belongs to the nodes of the grid, which
    // the logarithmic frequency axis places one by one, so the faces between them are as tall as the spacing of the
    // analysis frequencies makes them.
    ax.surface(&time_s, &frequency_hz, &power).edge_color(None);
    ax.color_scale()
        .colormap(Colormap::Magma)
        .limits(NOISE_FLOOR_DB, 0.0);
    ax.yscale(Scale::Log)
        .xlabel("Time $t$ (s)")
        .ylabel("Frequency $f$ (Hz)");
    fig
}

Back to the gallery