1use std::path::{Path, PathBuf};
4
5use sva_engine::{DEFAULT_FRAME_SECS, Representation};
6
7use crate::cli_error::CliError;
8
9pub const REPRESENTATIONS: [&str; 17] = [
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 "onsets",
24 "alias",
25 "bindings",
26 "arguments",
27];
28
29pub const RETIRED: [(&str, &str); 2] = [
30 ("exact-envelope", "envelope, symbolic on a closed form"),
31 ("exact-derivative", "derivative, symbolic on a closed form"),
32];
33
34pub const DEFAULT_OVERSAMPLE: u32 = 4;
35pub const DEFAULT_MAX_PEAKS: usize = 16;
36pub const DEFAULT_LEDGER_DEPTH: usize = 3;
37
38#[derive(Clone, Debug, PartialEq)]
41pub struct Call {
42 pub name: String,
43 pub args: Vec<(String, String)>,
44 pub dest: Option<PathBuf>,
46}
47
48#[derive(Clone, Debug, PartialEq)]
50pub struct Asked {
51 pub name: String,
52 pub representation: Representation,
53 pub dest: Option<PathBuf>,
54 pub node: Option<String>,
56 pub brief: bool,
58 pub skim: bool,
60}
61
62pub fn is_wav(path: &Path) -> bool {
63 path.extension()
64 .is_some_and(|e| e.eq_ignore_ascii_case("wav"))
65}
66
67pub fn retired(name: &str) -> Option<&'static str> {
68 RETIRED
69 .iter()
70 .find(|(gone, _)| *gone == name)
71 .map(|(_, write)| *write)
72}
73
74pub fn calls(text: &str) -> Result<Vec<Call>, CliError> {
76 split_outside(text, ',')?
77 .into_iter()
78 .map(|one| call(one.trim()))
79 .collect()
80}
81
82pub fn call(text: &str) -> Result<Call, CliError> {
84 let head_end = text.find(['(', '=']).unwrap_or(text.len());
85 let name = text[..head_end].trim().to_string();
86 if name.is_empty() {
87 return Err(unwritten(text, "it names no reading"));
88 }
89 let mut rest = &text[head_end..];
90 let mut args = Vec::new();
91 if rest.starts_with('(') {
92 let close = closing(rest).ok_or_else(|| unwritten(text, "its `(` is never closed"))?;
93 let inner = rest[1..close].trim();
94 if !inner.is_empty() {
95 for arg in split_outside(inner, ',')? {
96 let Some((key, value)) = arg.split_once('=') else {
97 return Err(unwritten(
98 text,
99 &format!("`{}` is no named argument; write `key=value`", arg.trim()),
100 ));
101 };
102 args.push((key.trim().to_string(), value.trim().to_string()));
103 }
104 }
105 rest = &rest[close + 1..];
106 }
107 let dest = match rest.trim_start().strip_prefix('=') {
108 Some("") => {
109 return Err(unwritten(
110 text,
111 "`=` names no destination; drop it to read the reading under \
112 `data.representations`",
113 ));
114 }
115 Some(path) => Some(PathBuf::from(path)),
116 None if rest.trim().is_empty() => None,
117 None => {
118 return Err(unwritten(
119 text,
120 &format!("`{}` follows the call", rest.trim()),
121 ));
122 }
123 };
124 Ok(Call { name, args, dest })
125}
126
127pub fn asked(call: &Call) -> Result<Asked, CliError> {
129 let name = call.name.as_str();
130 if let Some(write) = retired(name) {
131 return Err(CliError::Usage(format!(
132 "`{name}` left the language; write `{write}`"
133 )));
134 }
135 let mut out = Asked {
136 name: call.name.clone(),
137 representation: Representation::from_name(name).unwrap_or(Representation::Samples),
138 dest: call.dest.clone(),
139 node: None,
140 brief: false,
141 skim: false,
142 };
143 let (mut peaks, mut frame, mut depth, mut oversample) = (
144 DEFAULT_MAX_PEAKS,
145 None,
146 DEFAULT_LEDGER_DEPTH,
147 DEFAULT_OVERSAMPLE,
148 );
149 let takes: &[&str] = match name {
150 "spectrum" | "pitch" | "formants" => &["peaks", "frame"],
151 "envelope" | "stereo" => &["frame"],
152 "ledger" => &["depth", "brief", "skim"],
153 "alias" => &["oversample"],
154 "bindings" => &["node"],
155 known if Representation::from_name(known).is_some() => &[],
156 other => {
157 return Err(CliError::Usage(format!(
158 "unknown representation `{other}`; the representations are {}",
159 REPRESENTATIONS.join(", ")
160 )));
161 }
162 };
163 for (key, raw) in &call.args {
164 if !takes.contains(&key.as_str()) {
165 return Err(CliError::Usage(match takes {
166 [] => format!("`{name}` takes no arguments, not `{key}`"),
167 some => format!("`{name}` takes {}, not `{key}`", some.join(", ")),
168 }));
169 }
170 match key.as_str() {
171 "peaks" => peaks = count(raw, key)?,
172 "depth" => depth = count(raw, key)?,
173 "frame" => frame = Some(seconds(raw, key)?),
174 "oversample" => oversample = factor(raw)?,
175 "brief" => out.brief = switch(raw, key)?,
176 "skim" => out.skim = switch(raw, key)?,
177 _ => out.node = Some(node(raw)?),
178 }
179 }
180 if name == "bindings" && out.node.is_none() {
181 return Err(CliError::Usage(
182 "`bindings` reads one instance's bindings; name it, as `bindings(node=@voice)`"
183 .to_string(),
184 ));
185 }
186 let framed = frame.unwrap_or(DEFAULT_FRAME_SECS);
187 out.representation = match name {
188 "spectrum" => Representation::Spectrum {
189 max_peaks: peaks,
190 frame_secs: frame,
191 },
192 "envelope" => Representation::Envelope { frame_secs: frame },
193 "pitch" => Representation::Pitch {
194 max_notes: peaks,
195 frame_secs: framed,
196 },
197 "formants" => Representation::Formants {
198 max_formants: peaks,
199 frame_secs: framed,
200 },
201 "stereo" => Representation::Stereo { frame_secs: framed },
202 "ledger" => Representation::Ledger { depth },
203 "alias" => Representation::Alias { oversample },
204 _ => out.representation,
205 };
206 Ok(out)
207}
208
209fn split_outside(raw: &str, sep: char) -> Result<Vec<&str>, CliError> {
211 let (mut depth, mut from, mut out) = (0i32, 0, Vec::new());
212 for (at, c) in raw.char_indices() {
213 match c {
214 '(' => depth += 1,
215 ')' => depth -= 1,
216 c if c == sep && depth == 0 => {
217 out.push(&raw[from..at]);
218 from = at + c.len_utf8();
219 }
220 _ => {}
221 }
222 if depth < 0 {
223 return Err(unwritten(raw, "a `)` closes nothing"));
224 }
225 }
226 out.push(&raw[from..]);
227 Ok(out)
228}
229
230fn closing(text: &str) -> Option<usize> {
232 let mut depth = 0i32;
233 for (at, c) in text.char_indices() {
234 match c {
235 '(' => depth += 1,
236 ')' => {
237 depth -= 1;
238 if depth == 0 {
239 return Some(at);
240 }
241 }
242 _ => {}
243 }
244 }
245 None
246}
247
248fn unwritten(text: &str, why: &str) -> CliError {
249 CliError::Usage(format!(
250 "`{text}` is no reading: {why}. write a call, as `spectrum(peaks=8, frame=50ms)`, \
251 `=path` after it to write it to a file"
252 ))
253}
254
255pub fn wav_path(raw: &str) -> Result<PathBuf, CliError> {
256 match is_wav(Path::new(raw)) {
257 true => Ok(PathBuf::from(raw)),
258 false => Err(CliError::Usage(format!(
259 "`{raw}` is no `.wav` file; mp3 and every other format are out of scope"
260 ))),
261 }
262}
263
264fn count(raw: &str, key: &str) -> Result<usize, CliError> {
265 raw.parse::<usize>()
266 .map_err(|_| CliError::Usage(format!("`{key}` needs a whole count, got `{raw}`")))
267}
268
269fn factor(raw: &str) -> Result<u32, CliError> {
270 match raw.parse::<u32>() {
271 Ok(k) if (2..=16).contains(&k) && k.is_power_of_two() => Ok(k),
272 _ => Err(CliError::Usage(format!(
273 "`oversample` needs a power of two from 2 to 16, got `{raw}`"
274 ))),
275 }
276}
277
278pub fn seconds(raw: &str, key: &str) -> Result<f64, CliError> {
280 let held = match sva_ast::tokenize(raw).as_deref() {
281 Ok([one]) => match one.kind {
282 sva_ast::TokenKind::Num(v)
283 | sva_ast::TokenKind::Time(v, sva_ast::SpanUnit::Seconds) => Some(v),
284 _ => None,
285 },
286 _ => None,
287 };
288 match held {
289 Some(v) if v > 0.0 && v.is_finite() => Ok(v),
290 _ => Err(CliError::Usage(format!(
291 "`{key}` needs a positive time, as `50ms` or `0.05`, got `{raw}`"
292 ))),
293 }
294}
295
296fn switch(raw: &str, key: &str) -> Result<bool, CliError> {
297 match raw {
298 "1" => Ok(true),
299 "0" => Ok(false),
300 _ => Err(CliError::Usage(format!(
301 "`{key}` is `1` or `0`, got `{raw}`"
302 ))),
303 }
304}
305
306fn node(raw: &str) -> Result<String, CliError> {
308 match raw.strip_prefix('@') {
309 Some(path) if !path.is_empty() => Ok(path.to_string()),
310 _ => Err(CliError::Usage(format!(
311 "`node` names an instance as a ref, as `@voice`, not `{raw}`"
312 ))),
313 }
314}