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