pub mod composite;
#[cfg(feature = "plotting")]
pub mod dsp_overlays;
pub mod spectrograms;
pub mod spectrum;
pub mod waveform;
pub use composite::{CompositeLayout, CompositePlot};
pub use spectrograms::{SpectrogramPlot, SpectrogramPlotParams, create_spectrogram_plot};
pub use spectrum::{MagnitudeSpectrumParams, create_magnitude_spectrum_plot};
pub use waveform::{WaveformPlot, WaveformPlotParams};
use core::num::NonZeroUsize;
use std::path::Path;
use crate::{
AudioSampleResult, AudioSamples, StandardSample,
operations::{AudioPlotting, create_waveform_plot},
};
pub(crate) const DECIMATE_THRESHOLD: NonZeroUsize = crate::nzu!(25000);
pub trait PlotUtils {
fn html(&self) -> AudioSampleResult<String>;
#[cfg(feature = "html_view")]
fn show(&self) -> AudioSampleResult<()>;
fn save<P: AsRef<Path>>(&self, path: P) -> AudioSampleResult<()>;
}
fn decimate_waveform(
time_data: &[f64],
amplitude_data: &[f64],
target_points: usize,
) -> (Vec<f64>, Vec<f64>) {
let n_samples = time_data.len();
if n_samples <= target_points {
return (time_data.to_vec(), amplitude_data.to_vec());
}
let n_bins = (target_points / 2).max(1);
let bin_size = n_samples / n_bins;
let mut decimated_time = Vec::with_capacity(n_bins * 2);
let mut decimated_amplitude = Vec::with_capacity(n_bins * 2);
for bin_idx in 0..n_bins {
let start_idx = bin_idx * bin_size;
let end_idx = if bin_idx == n_bins - 1 {
n_samples } else {
((bin_idx + 1) * bin_size).min(n_samples)
};
if start_idx >= end_idx {
break;
}
let mut min_idx = start_idx;
let mut max_idx = start_idx;
let mut min_val = amplitude_data[start_idx];
let mut max_val = amplitude_data[start_idx];
for (i, &val) in amplitude_data
.iter()
.enumerate()
.take(end_idx)
.skip(start_idx)
{
if val < min_val {
min_val = val;
min_idx = i;
}
if val > max_val {
max_val = val;
max_idx = i;
}
}
if min_idx < max_idx {
decimated_time.push(time_data[min_idx]);
decimated_amplitude.push(min_val);
decimated_time.push(time_data[max_idx]);
decimated_amplitude.push(max_val);
} else {
decimated_time.push(time_data[max_idx]);
decimated_amplitude.push(max_val);
decimated_time.push(time_data[min_idx]);
decimated_amplitude.push(min_val);
}
}
(decimated_time, decimated_amplitude)
}
pub(crate) fn configure_time_axis(
mut axis: plotly::layout::Axis,
title: Option<String>,
) -> plotly::layout::Axis {
let title_text = title.map_or_else(
|| "Time (s)".to_string(),
|t| {
if t.contains('(') {
t
} else {
format!("{t} (s)")
}
},
);
axis = axis.title(title_text);
axis = axis.tick_format(".2f");
axis
}
pub(crate) fn configure_frequency_axis(
mut axis: plotly::layout::Axis,
title: Option<String>,
max_freq: f64,
) -> plotly::layout::Axis {
let use_khz = max_freq > 1000.0;
let title_text = title.map_or_else(
|| {
if use_khz {
"Frequency (kHz)".to_string()
} else {
"Frequency (Hz)".to_string()
}
},
|t| {
if t.contains('(') {
t
} else if use_khz {
format!("{t} (kHz)")
} else {
format!("{t} (Hz)")
}
},
);
axis = axis.title(title_text);
if use_khz {
axis = axis.tick_format(".1f");
} else {
axis = axis.tick_format(".0f");
}
axis
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct FontSizes {
pub title: NonZeroUsize,
pub axis_labels: NonZeroUsize,
pub ticks: NonZeroUsize,
pub legend: NonZeroUsize,
pub super_title: NonZeroUsize,
pub misc: NonZeroUsize,
}
impl FontSizes {
#[inline]
#[must_use]
pub const fn new(
title: NonZeroUsize,
axis_labels: NonZeroUsize,
ticks: NonZeroUsize,
legend: NonZeroUsize,
super_title: NonZeroUsize,
misc: NonZeroUsize,
) -> Self {
Self {
title,
axis_labels,
ticks,
legend,
super_title,
misc,
}
}
}
impl Default for FontSizes {
#[inline]
fn default() -> Self {
Self {
title: crate::nzu!(16),
axis_labels: crate::nzu!(12),
ticks: crate::nzu!(10),
legend: crate::nzu!(12),
super_title: crate::nzu!(18),
misc: crate::nzu!(10),
}
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct PlotParams {
pub title: Option<String>,
pub x_label: Option<String>,
pub y_label: Option<String>,
pub font_sizes: Option<FontSizes>,
pub show_legend: bool,
pub legend_title: Option<String>,
pub super_title: Option<String>,
pub grid: bool,
}
impl PlotParams {
#[inline]
#[must_use]
pub const fn new(
title: Option<String>,
x_label: Option<String>,
y_label: Option<String>,
font_sizes: Option<FontSizes>,
show_legend: bool,
legend_title: Option<String>,
super_title: Option<String>,
grid: bool,
) -> Self {
Self {
title,
x_label,
y_label,
font_sizes,
show_legend,
legend_title,
super_title,
grid,
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum PlotType {
Waveform(Box<WaveformPlotParams>),
Spectrogram,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ChannelManagementStrategy {
Average,
Separate(Layout),
First,
Last,
Overlap,
}
impl Default for ChannelManagementStrategy {
#[inline]
fn default() -> Self {
Self::Separate(Layout::default())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum Layout {
#[default]
Vertical,
Horizontal,
}
impl<T> AudioPlotting for AudioSamples<'_, T>
where
T: StandardSample,
{
#[inline]
fn plot_waveform(&self, params: &WaveformPlotParams) -> AudioSampleResult<WaveformPlot> {
create_waveform_plot(self, params)
}
#[cfg(feature = "transforms")]
#[inline]
fn plot_spectrogram(
&self,
params: &SpectrogramPlotParams,
) -> AudioSampleResult<SpectrogramPlot> {
spectrograms::create_spectrogram_plot(self, params)
}
#[cfg(not(feature = "transforms"))]
#[inline]
fn plot_spectrogram(
&self,
_params: &SpectrogramPlotParams,
) -> AudioSampleResult<SpectrogramPlot> {
Err(crate::AudioSampleError::Feature(
crate::FeatureError::NotEnabled {
feature: "transforms".to_string(),
operation: "plot spectrograms".to_string(),
},
))
}
#[cfg(feature = "transforms")]
#[inline]
fn plot_magnitude_spectrum(
&self,
params: &spectrum::MagnitudeSpectrumParams,
) -> AudioSampleResult<spectrum::MagnitudeSpectrumPlot> {
spectrum::create_magnitude_spectrum_plot(self, params)
}
#[cfg(not(feature = "transforms"))]
#[inline]
fn plot_magnitude_spectrum(
&self,
_params: &spectrum::MagnitudeSpectrumParams,
) -> AudioSampleResult<spectrum::MagnitudeSpectrumPlot> {
Err(crate::AudioSampleError::Feature(
crate::FeatureError::NotEnabled {
feature: "transforms".to_string(),
operation: "plot magnitude spectrum".to_string(),
},
))
}
}