#[cfg(feature = "image")]
use anyhow::Context;
use anyhow::Result;
#[derive(Debug, Clone, Copy, Default)]
pub enum Colormap {
#[default]
Viridis,
Magma,
Inferno,
Plasma,
Gray,
}
fn apply_colormap(value: f32, colormap: Colormap) -> [u8; 3] {
let v = value.clamp(0.0, 1.0);
match colormap {
Colormap::Viridis => viridis(v),
Colormap::Magma => magma(v),
Colormap::Inferno => inferno(v),
Colormap::Plasma => plasma(v),
Colormap::Gray => {
let gray = (v * 255.0) as u8;
[gray, gray, gray]
}
}
}
fn viridis(t: f32) -> [u8; 3] {
let r = 0.267004
+ t * (0.004874
+ t * (2.827049
+ t * (-14.180787 + t * (23.635307 + t * (-18.567452 + t * 5.024689)))));
let g = 0.004975
+ t * (0.408503
+ t * (2.820819 + t * (-11.188263 + t * (14.949977 + t * (-9.419796 + t * 2.192326)))));
let b = 0.329415
+ t * (1.586134 + t * (-3.126778 + t * (6.283825 + t * (-7.528898 + t * 3.456374))));
[(r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8]
}
fn magma(t: f32) -> [u8; 3] {
let r = 0.001462
+ t * (0.062542
+ t * (4.777823
+ t * (-16.339474 + t * (26.246972 + t * (-20.125255 + t * 5.376646)))));
let g = 0.001512
+ t * (0.142844
+ t * (2.433026 + t * (-9.125778 + t * (13.268_85 + t * (-10.347974 + t * 2.848073)))));
let b = 0.013952
+ t * (1.581272 + t * (-4.124382 + t * (8.499725 + t * (-10.004358 + t * 4.043804))));
[(r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8]
}
fn inferno(t: f32) -> [u8; 3] {
let r = 0.001788
+ t * (0.200942
+ t * (3.526908
+ t * (-11.642545 + t * (17.366_99 + t * (-12.351348 + t * 3.914439)))));
let g = 0.000262
+ t * (0.034534
+ t * (2.445295 + t * (-9.520904 + t * (14.683786 + t * (-11.257943 + t * 3.114629)))));
let b = 0.014227
+ t * (1.534345 + t * (-5.444_98 + t * (11.756179 + t * (-14.117606 + t * 6.258159))));
[(r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8]
}
fn plasma(t: f32) -> [u8; 3] {
let r = 0.050383
+ t * (1.022623
+ t * (0.676103 + t * (-2.828869 + t * (5.187948 + t * (-3.728022 + t * 0.999914)))));
let g = 0.029803
+ t * (0.237302
+ t * (2.524446 + t * (-9.892_82 + t * (14.450_55 + t * (-10.611676 + t * 2.963018)))));
let b = 0.527975
+ t * (1.415069 + t * (-4.203048 + t * (8.238769 + t * (-9.742888 + t * 4.764384))));
[(r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8]
}
#[cfg(feature = "image")]
pub fn save_spectrogram_image(
spectrogram: &[Vec<f32>],
output_path: &str,
colormap: Colormap,
) -> Result<()> {
use image::{ImageBuffer, Rgb};
let n_freq_bins = spectrogram.len();
let n_frames = spectrogram[0].len();
let log_values: Vec<Vec<f32>> = spectrogram
.iter()
.map(|row| row.iter().map(|&v| (v + 1.0).ln()).collect())
.collect();
let min_val = log_values
.iter()
.flatten()
.copied()
.fold(f32::INFINITY, f32::min);
let max_val = log_values
.iter()
.flatten()
.copied()
.fold(f32::NEG_INFINITY, f32::max);
let range = max_val - min_val;
let mut img = ImageBuffer::new(n_frames as u32, n_freq_bins as u32);
for (freq_idx, row) in log_values.iter().enumerate() {
for (time_idx, &value) in row.iter().enumerate() {
let normalized = if range > 0.0 {
(value - min_val) / range
} else {
0.5
};
let rgb = apply_colormap(normalized, colormap);
let y = (n_freq_bins - 1 - freq_idx) as u32;
let x = time_idx as u32;
img.put_pixel(x, y, Rgb(rgb));
}
}
img.save(output_path)
.with_context(|| format!("Failed to save image to {}", output_path))?;
Ok(())
}
#[cfg(not(feature = "image"))]
pub fn save_spectrogram_image(
_spectrogram: &[Vec<f32>],
_output_path: &str,
_colormap: Colormap,
) -> Result<()> {
anyhow::bail!("Image feature not enabled. Compile with --features image to use this function.")
}