use std::path::{Path, PathBuf};
use sva_engine::{DEFAULT_FRAME_SECS, Horizon, Representation};
use crate::cli_error::CliError;
pub const REPRESENTATIONS: [&str; 16] = [
"lines",
"atoms",
"spectrum",
"envelope",
"derivative",
"samples",
"ledger",
"pitch",
"formants",
"stereo",
"bands",
"crest",
"loudness",
"alias",
"bindings",
"arguments",
];
pub const RETIRED: [(&str, &str); 2] = [
("exact-envelope", "envelope, symbolic on a closed form"),
("exact-derivative", "derivative, symbolic on a closed form"),
];
#[derive(Clone, Debug, PartialEq)]
pub struct Asked {
pub name: String,
pub representation: Representation,
pub dest: Option<PathBuf>,
}
pub fn is_wav(path: &Path) -> bool {
path.extension()
.is_some_and(|e| e.eq_ignore_ascii_case("wav"))
}
pub const DEFAULT_OVERSAMPLE: u32 = 4;
pub const DEFAULT_MAX_PEAKS: usize = 16;
pub const DEFAULT_LEDGER_DEPTH: usize = 3;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Shaping {
pub frame_secs: Option<f64>,
pub depth: usize,
pub peaks: usize,
pub oversample: u32,
}
impl Default for Shaping {
fn default() -> Shaping {
Shaping {
frame_secs: None,
depth: DEFAULT_LEDGER_DEPTH,
peaks: DEFAULT_MAX_PEAKS,
oversample: DEFAULT_OVERSAMPLE,
}
}
}
pub fn representation_for(name: &str, shape: Shaping) -> Option<Representation> {
let frame = shape.frame_secs.unwrap_or(DEFAULT_FRAME_SECS);
Some(match name {
"spectrum" => Representation::Spectrum {
max_peaks: shape.peaks,
frame_secs: shape.frame_secs,
},
"envelope" => Representation::Envelope {
frame_secs: shape.frame_secs,
},
"pitch" => Representation::Pitch {
max_notes: shape.peaks,
frame_secs: frame,
},
"formants" => Representation::Formants {
max_formants: shape.peaks,
frame_secs: frame,
},
"stereo" => Representation::Stereo { frame_secs: frame },
"ledger" => Representation::Ledger { depth: shape.depth },
"alias" => Representation::Alias {
oversample: shape.oversample,
},
other => Representation::from_name(other)?,
})
}
pub fn retired(name: &str) -> Option<&'static str> {
RETIRED
.iter()
.find(|(gone, _)| *gone == name)
.map(|(_, write)| *write)
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum WindowEdge {
Secs(f64),
Bars(f64),
End,
}
pub fn window_edge(raw: &str, flag: &str) -> Result<WindowEdge, CliError> {
let refuse = || {
CliError::Usage(format!(
"{flag} needs a time — a number of seconds, `<n>s`, `<n>b`, or `end`, got `{raw}`"
))
};
if raw == "end" {
return Ok(WindowEdge::End);
}
let (number, wrap): (&str, fn(f64) -> WindowEdge) = match raw.strip_suffix('b') {
Some(bars) => (bars, WindowEdge::Bars),
None => (raw.strip_suffix('s').unwrap_or(raw), WindowEdge::Secs),
};
match number.parse::<f64>() {
Ok(v) if v.is_finite() && v >= 0.0 => Ok(wrap(v)),
_ => Err(refuse()),
}
}
fn edge(
given: Option<WindowEdge>,
flag: &str,
unset: f64,
seconds_per_bar: Option<f64>,
) -> Result<f64, CliError> {
let secs = match given {
None | Some(WindowEdge::End) => return Ok(unset),
Some(WindowEdge::Secs(v)) => v,
Some(WindowEdge::Bars(v)) => match seconds_per_bar {
Some(secs) => v * secs,
None => {
return Err(CliError::BadTempo(format!(
"{flag} is written in bars and nothing here declares a tempo; \
state `variables/bpm` and `variables/meter`, or write seconds"
)));
}
},
};
match secs.is_finite() && secs >= 0.0 {
true => Ok(secs),
false => Err(CliError::Usage(format!(
"{flag} needs a time at or after zero, got `{secs}`"
))),
}
}
pub fn window_for(
from: Option<WindowEdge>,
to: Option<WindowEdge>,
seconds_per_bar: Option<f64>,
) -> Result<Horizon, CliError> {
Ok(Horizon::secs(
edge(from, "--from", 0.0, seconds_per_bar)?,
edge(to, "--to", f64::INFINITY, seconds_per_bar)?,
))
}