1use std::path::{Path, PathBuf};
4
5use sva_engine::{DEFAULT_FRAME_SECS, Horizon, Representation, Silent};
6
7use crate::cli_error::CliError;
8
9pub const REPRESENTATIONS: [&str; 16] = [
10 "lines",
11 "atoms",
12 "spectrum",
13 "envelope",
14 "derivative",
15 "samples",
16 "ledger",
17 "pitch",
18 "formants",
19 "stereo",
20 "bands",
21 "crest",
22 "loudness",
23 "alias",
24 "bindings",
25 "arguments",
26];
27
28pub const RETIRED: [(&str, &str); 2] = [
29 ("exact-envelope", "envelope, symbolic on a closed form"),
30 ("exact-derivative", "derivative, symbolic on a closed form"),
31];
32
33#[derive(Clone, Debug, PartialEq)]
35pub struct Asked {
36 pub name: String,
37 pub representation: Representation,
38 pub dest: Option<PathBuf>,
39}
40
41pub fn is_wav(path: &Path) -> bool {
42 path.extension()
43 .is_some_and(|e| e.eq_ignore_ascii_case("wav"))
44}
45
46pub const DEFAULT_OVERSAMPLE: u32 = 4;
47pub const DEFAULT_MAX_PEAKS: usize = 16;
48pub const DEFAULT_LEDGER_DEPTH: usize = 3;
49
50#[derive(Clone, Copy, Debug, PartialEq)]
51pub struct Shaping {
52 pub frame_secs: Option<f64>,
53 pub depth: usize,
54 pub peaks: usize,
55 pub oversample: u32,
56}
57
58impl Default for Shaping {
59 fn default() -> Shaping {
60 Shaping {
61 frame_secs: None,
62 depth: DEFAULT_LEDGER_DEPTH,
63 peaks: DEFAULT_MAX_PEAKS,
64 oversample: DEFAULT_OVERSAMPLE,
65 }
66 }
67}
68
69pub fn representation_for(name: &str, shape: Shaping) -> Option<Representation> {
70 let frame = shape.frame_secs.unwrap_or(DEFAULT_FRAME_SECS);
71 Some(match name {
72 "spectrum" => Representation::Spectrum {
73 max_peaks: shape.peaks,
74 frame_secs: shape.frame_secs,
75 },
76 "envelope" => Representation::Envelope {
77 frame_secs: shape.frame_secs,
78 },
79 "pitch" => Representation::Pitch {
80 max_notes: shape.peaks,
81 frame_secs: frame,
82 },
83 "formants" => Representation::Formants {
84 max_formants: shape.peaks,
85 frame_secs: frame,
86 },
87 "stereo" => Representation::Stereo { frame_secs: frame },
88 "ledger" => Representation::Ledger { depth: shape.depth },
89 "alias" => Representation::Alias {
90 oversample: shape.oversample,
91 },
92 other => Representation::from_name(other)?,
93 })
94}
95
96pub fn retired(name: &str) -> Option<&'static str> {
97 RETIRED
98 .iter()
99 .find(|(gone, _)| *gone == name)
100 .map(|(_, write)| *write)
101}
102
103#[derive(Clone, Copy, Debug, PartialEq)]
106pub enum WindowEdge {
107 Secs(f64),
108 Bars(f64),
109 End,
110}
111
112pub const DEFAULT_SILENT_BITS: u32 = sva_engine::PSYCHOACOUSTIC_V1.precision_bits as u32;
114
115pub const DEFAULT_SILENT_MAX_SECS: f64 = 60.0;
117
118pub fn silent_edge(raw: &str) -> Option<Result<Option<u32>, CliError>> {
120 if raw == "silent" {
121 return Some(Ok(None));
122 }
123 let bits = raw.strip_prefix("silent:")?;
124 Some(bits.parse::<u32>().map(Some).map_err(|_| {
125 CliError::Usage(format!(
126 "--to silent takes whole bits, as `silent:16`, got `{raw}`"
127 ))
128 }))
129}
130
131pub fn silence(bits: Option<u32>, max_secs: Option<f64>) -> Result<Silent, CliError> {
133 let bits = bits.unwrap_or(DEFAULT_SILENT_BITS);
134 if !(1..=53).contains(&bits) {
135 return Err(CliError::Usage(format!(
136 "silence takes whole bits from 1 to 53, got {bits}"
137 )));
138 }
139 let max_secs = max_secs.unwrap_or(DEFAULT_SILENT_MAX_SECS);
140 if !(max_secs.is_finite() && max_secs > 0.0) {
141 return Err(CliError::Usage(format!(
142 "silence is looked for until a time past zero, got {max_secs}"
143 )));
144 }
145 Ok(Silent { bits, max_secs })
146}
147
148pub fn window_edge(raw: &str, flag: &str) -> Result<WindowEdge, CliError> {
149 let refuse = || {
150 CliError::Usage(format!(
151 "{flag} needs a time — a number of seconds, `<n>s`, `<n>b`, or `end`, got `{raw}`"
152 ))
153 };
154 if raw == "end" {
155 return Ok(WindowEdge::End);
156 }
157 let (number, wrap): (&str, fn(f64) -> WindowEdge) = match raw.strip_suffix('b') {
158 Some(bars) => (bars, WindowEdge::Bars),
159 None => (raw.strip_suffix('s').unwrap_or(raw), WindowEdge::Secs),
160 };
161 match number.parse::<f64>() {
162 Ok(v) if v.is_finite() && v >= 0.0 => Ok(wrap(v)),
163 _ => Err(refuse()),
164 }
165}
166
167fn edge(
168 given: Option<WindowEdge>,
169 flag: &str,
170 unset: f64,
171 seconds_per_bar: Option<f64>,
172) -> Result<f64, CliError> {
173 let secs = match given {
174 None | Some(WindowEdge::End) => return Ok(unset),
175 Some(WindowEdge::Secs(v)) => v,
176 Some(WindowEdge::Bars(v)) => match seconds_per_bar {
177 Some(secs) => v * secs,
178 None => {
179 return Err(CliError::BadTempo(format!(
180 "{flag} is written in bars and nothing here declares a tempo; \
181 state `variables/bpm` and `variables/meter`, or write seconds"
182 )));
183 }
184 },
185 };
186 match secs.is_finite() && secs >= 0.0 {
187 true => Ok(secs),
188 false => Err(CliError::Usage(format!(
189 "{flag} needs a time at or after zero, got `{secs}`"
190 ))),
191 }
192}
193
194pub fn window_for(
197 from: Option<WindowEdge>,
198 to: Option<WindowEdge>,
199 seconds_per_bar: Option<f64>,
200) -> Result<Horizon, CliError> {
201 Ok(Horizon::secs(
202 edge(from, "--from", 0.0, seconds_per_bar)?,
203 edge(to, "--to", f64::INFINITY, seconds_per_bar)?,
204 ))
205}