use anyhow::{Context, Result};
use clap::Parser;
use rayon::prelude::*;
use spectrs::io::audio::{read_audio_file_mono, resample};
use spectrs::io::image::{Colormap, save_spectrogram_image};
use spectrs::spectrogram::mel::{MelScale, convert_to_mel, par_convert_to_mel};
use spectrs::spectrogram::stft::{SpectrogramType, compute_spectrogram, par_compute_spectrogram};
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
#[derive(Parser)]
#[command(version, about, long_about = None)]
pub struct Cli {
#[arg(required = true)]
pub input: String,
#[arg(long)]
pub output_dir: Option<String>,
#[arg(long)]
pub sr: Option<u32>,
#[arg(long, default_value = "2048")]
pub n_fft: usize,
#[arg(long, default_value = "512")]
pub hop_length: usize,
#[arg(long, default_value = "2048")]
pub win_length: usize,
#[arg(long, default_value = "true")]
pub center: bool,
#[arg(long, default_value = "power")]
pub spec_type: SpectrogramType,
#[arg(long)]
pub n_mels: Option<usize>,
#[arg(long, default_value = "0.0")]
pub f_min: Option<f32>,
#[arg(long)]
pub f_max: Option<f32>,
#[arg(long, default_value = "slaney")]
pub mel_scale: MelScale,
#[arg(long, default_value = "viridis")]
pub colormap: Colormap,
}
#[allow(clippy::too_many_arguments)]
fn par_create_spectrogram(
input: &Path,
output: &Path,
sr: Option<u32>,
n_fft: usize,
hop_length: usize,
win_length: usize,
center: bool,
spec_type: SpectrogramType,
n_mels: Option<usize>,
f_min: Option<f32>,
f_max: Option<f32>,
mel_scale: MelScale,
colormap: Colormap,
) -> Result<()> {
let (mut audio, original_sr) =
read_audio_file_mono(input).with_context(|| "Failed to read audio")?;
let target_sr = match sr {
Some(sample_rate) if sample_rate != original_sr => {
audio = resample(audio, original_sr, sample_rate)
.with_context(|| "Failed to resample audio")?;
sample_rate
}
Some(sample_rate) => sample_rate,
None => original_sr,
};
let mut spec =
par_compute_spectrogram(&audio, n_fft, hop_length, win_length, center, spec_type);
if let Some(n_mels_value) = n_mels {
spec = par_convert_to_mel(
&spec,
target_sr,
n_fft,
n_mels_value,
f_min,
f_max,
mel_scale,
);
}
save_spectrogram_image(&spec, output.to_path_buf(), colormap)
.with_context(|| "Failed to save spectogram")?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn create_spectrogram(
input: &Path,
output: &Path,
sr: Option<u32>,
n_fft: usize,
hop_length: usize,
win_length: usize,
center: bool,
spec_type: SpectrogramType,
n_mels: Option<usize>,
f_min: Option<f32>,
f_max: Option<f32>,
mel_scale: MelScale,
colormap: Colormap,
) -> Result<()> {
let (mut audio, original_sr) =
read_audio_file_mono(input).with_context(|| "Failed to read audio")?;
let target_sr = match sr {
Some(sample_rate) if sample_rate != original_sr => {
audio = resample(audio, original_sr, sample_rate)
.with_context(|| "Failed to resample audio")?;
sample_rate
}
Some(sample_rate) => sample_rate,
None => original_sr,
};
let mut spec = compute_spectrogram(&audio, n_fft, hop_length, win_length, center, spec_type);
if let Some(n_mels_value) = n_mels {
spec = convert_to_mel(
&spec,
target_sr,
n_fft,
n_mels_value,
f_min,
f_max,
mel_scale,
);
}
save_spectrogram_image(&spec, output.to_path_buf(), colormap)
.with_context(|| "Failed to save spectogram")?;
Ok(())
}
fn compute_output_path(
file_path: &Path,
base_path: &Path,
output_dir: Option<&str>,
) -> Result<PathBuf> {
if let Some(out_dir) = output_dir {
let relative = if file_path == base_path {
file_path
.file_name()
.ok_or_else(|| anyhow::anyhow!("Invalid file path: {}", file_path.display()))?
.as_ref()
} else {
file_path.strip_prefix(base_path).with_context(|| {
format!(
"Failed to compute relative path for: {}",
file_path.display()
)
})?
};
Ok(Path::new(out_dir).join(relative).with_extension("png"))
} else {
Ok(file_path.with_extension("png"))
}
}
fn main() -> Result<()> {
let args = Cli::parse();
let input = Path::new(&args.input);
if !input.exists() {
anyhow::bail!("Input path does not exist: {}", input.display());
}
if input.is_file() && input.extension().and_then(|ext| ext.to_str()) == Some("wav") {
let output = compute_output_path(input, input, args.output_dir.as_deref())?;
par_create_spectrogram(
input,
&output,
args.sr,
args.n_fft,
args.hop_length,
args.win_length,
args.center,
args.spec_type,
args.n_mels,
args.f_min,
args.f_max,
args.mel_scale,
args.colormap,
)
.with_context(|| "Failed to create spectrogram")?;
}
else {
let files: Vec<_> = WalkDir::new(input)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().and_then(|ext| ext.to_str()) == Some("wav"))
.map(|e| e.path().to_path_buf())
.collect();
files
.par_iter()
.try_for_each(|file| -> Result<()> {
let output = compute_output_path(file, input, args.output_dir.as_deref())?;
create_spectrogram(
file,
&output,
args.sr,
args.n_fft,
args.hop_length,
args.win_length,
args.center,
args.spec_type,
args.n_mels,
args.f_min,
args.f_max,
args.mel_scale,
args.colormap,
)
})
.with_context(|| "Failed to create spectrogram")?;
};
Ok(())
}