1use std::path::{Path, PathBuf};
4
5use sva_engine::harmony::voice_leading::Norm;
6use sva_engine::{Arg, DEFAULT_BUFFER_SECS, Given, READINGS, Representation};
7
8use crate::cli_error::CliError;
9
10pub const RETIRED: [(&str, &str); 2] = [
11 ("exact-envelope", "envelope, symbolic on a closed form"),
12 ("exact-derivative", "derivative, symbolic on a closed form"),
13];
14
15#[derive(Clone, Debug, PartialEq)]
18pub struct Call {
19 pub name: String,
20 pub args: Vec<(String, String)>,
21 pub dest: Option<PathBuf>,
23}
24
25#[derive(Clone, Debug, PartialEq)]
28pub enum Destination {
29 File(PathBuf),
30 Alsa {
31 device: String,
33 buffer_secs: f64,
35 },
36}
37
38#[derive(Clone, Debug, PartialEq)]
40pub struct Asked {
41 pub name: String,
42 pub representation: Representation,
43 pub dest: Option<Destination>,
44 pub node: Option<String>,
46 pub brief: bool,
48 pub skim: bool,
50}
51
52pub fn is_wav(path: &Path) -> bool {
53 path.extension()
54 .is_some_and(|e| e.eq_ignore_ascii_case("wav"))
55}
56
57pub fn retired(name: &str) -> Option<&'static str> {
58 RETIRED
59 .iter()
60 .find(|(gone, _)| *gone == name)
61 .map(|(_, write)| *write)
62}
63
64pub fn calls(text: &str) -> Result<Vec<Call>, CliError> {
67 let mut joined: Vec<String> = Vec::new();
68 for one in split_outside(text, ',')? {
69 match joined.last_mut() {
70 Some(last) if names_a_device(last) && !names_a_reading(one) => {
71 last.push(',');
72 last.push_str(one);
73 }
74 _ => joined.push(one.to_string()),
75 }
76 }
77 joined.iter().map(|one| call(one.trim())).collect()
78}
79
80fn names_a_device(call: &str) -> bool {
81 let close = call.rfind(')').map_or(0, |at| at + 1);
82 call[close..]
83 .split_once('=')
84 .is_some_and(|(_, dest)| dest.trim_start().starts_with("alsa:"))
85}
86
87fn names_a_reading(text: &str) -> bool {
88 let head = text[..text.find(['(', '=']).unwrap_or(text.len())].trim();
89 READINGS.iter().any(|row| row.name == head) || retired(head).is_some()
90}
91
92pub fn call(text: &str) -> Result<Call, CliError> {
94 let head_end = text.find(['(', '=']).unwrap_or(text.len());
95 let name = text[..head_end].trim().to_string();
96 if name.is_empty() {
97 return Err(unwritten(text, "it names no reading"));
98 }
99 let mut rest = &text[head_end..];
100 let mut args = Vec::new();
101 if rest.starts_with('(') {
102 let close = closing(rest).ok_or_else(|| unwritten(text, "its `(` is never closed"))?;
103 let inner = rest[1..close].trim();
104 if !inner.is_empty() {
105 for arg in split_outside(inner, ',')? {
106 let Some((key, value)) = arg.split_once('=') else {
107 return Err(unwritten(
108 text,
109 &format!("`{}` is no named argument; write `key=value`", arg.trim()),
110 ));
111 };
112 args.push((key.trim().to_string(), value.trim().to_string()));
113 }
114 }
115 rest = &rest[close + 1..];
116 }
117 let dest = match rest.trim_start().strip_prefix('=') {
118 Some("") => {
119 return Err(unwritten(
120 text,
121 "`=` names no destination; drop it to read the reading under \
122 `data.representations`",
123 ));
124 }
125 Some(path) => Some(PathBuf::from(path)),
126 None if rest.trim().is_empty() => None,
127 None => {
128 return Err(unwritten(
129 text,
130 &format!("`{}` follows the call", rest.trim()),
131 ));
132 }
133 };
134 Ok(Call { name, args, dest })
135}
136
137pub fn asked_all(calls: &[Call]) -> Result<Vec<Asked>, CliError> {
140 let mut out: Vec<Asked> = Vec::with_capacity(calls.len());
141 for call in calls {
142 let one = asked(call)?;
143 if out.iter().any(|held| held.name == one.name) {
144 return Err(CliError::usage(
145 "query.duplicate",
146 format!(
147 "`{}` is asked more than once; ask each reading once, every argument in \
148 its one call",
149 one.name
150 ),
151 ));
152 }
153 out.push(one);
154 }
155 Ok(out)
156}
157
158impl Asked {
159 pub fn path(&self) -> Option<&Path> {
160 match &self.dest {
161 Some(Destination::File(path)) => Some(path),
162 Some(Destination::Alsa { .. }) | None => None,
163 }
164 }
165}
166
167pub fn asked(call: &Call) -> Result<Asked, CliError> {
169 let name = call.name.as_str();
170 if let Some(write) = retired(name) {
171 return Err(CliError::usage(
172 "query.retired",
173 format!("`{name}` left the language; write `{write}`"),
174 ));
175 }
176 let Some(row) = READINGS.iter().find(|row| row.name == name) else {
177 let names: Vec<&str> = READINGS.iter().map(|row| row.name).collect();
178 return Err(CliError::usage(
179 "query.unknown_representation",
180 format!(
181 "unknown representation `{name}`; the representations are {}",
182 names.join(", ")
183 ),
184 ));
185 };
186 let (mut given, mut node, mut brief, mut skim) = (Given::default(), None, false, false);
187 let mut buffer = None;
188 for (key, raw) in &call.args {
189 let Some(arg) = row.takes.iter().find(|arg| arg.key() == key) else {
190 let keys: Vec<&str> = row.takes.iter().map(|arg| arg.key()).collect();
191 return Err(CliError::usage(
192 "query.unknown_argument",
193 match keys[..] {
194 [] => format!("`{name}` takes no arguments, not `{key}`"),
195 _ => format!("`{name}` takes {}, not `{key}`", keys.join(", ")),
196 },
197 ));
198 };
199 match arg {
200 Arg::Peaks => given.peaks = count(raw, key)?,
201 Arg::Depth => given.depth = count(raw, key)?,
202 Arg::Frame => given.frame = Some(seconds(raw, key)?),
203 Arg::Oversample => given.oversample = factor(raw)?,
204 Arg::Brief => brief = switch(raw, key)?,
205 Arg::Skim => skim = switch(raw, key)?,
206 Arg::Node => node = Some(self::node(raw)?),
207 Arg::Buffer => buffer = Some(microseconds(raw, key)?),
208 Arg::Floor => given.floor_db = decibels(raw, key)?,
209 Arg::Norm => given.norm = norm(raw)?,
210 Arg::Pairs => given.max_pairs = count(raw, key)?,
211 Arg::Window => given.window = Some(seconds(raw, key)?),
212 Arg::Curve => given.curve = switch(raw, key)?,
213 Arg::Bpm => given.bpm = Some(tempo(raw, key)?),
214 Arg::Cell => given.cell_secs = seconds(raw, key)?,
215 Arg::Smooth => given.smooth_secs = seconds(raw, key)?,
216 }
217 }
218 if row.takes.contains(&Arg::Node) && node.is_none() {
219 return Err(CliError::usage(
220 "query.missing_argument",
221 format!("`{name}` reads one instance's {name}; name it, as `{name}(node=@voice)`"),
222 ));
223 }
224 Ok(Asked {
225 name: call.name.clone(),
226 representation: row.representation(&given),
227 dest: destination(name, call.dest.as_deref(), buffer)?,
228 node,
229 brief,
230 skim,
231 })
232}
233
234fn destination(
236 name: &str,
237 dest: Option<&Path>,
238 buffer: Option<f64>,
239) -> Result<Option<Destination>, CliError> {
240 let device = dest.and_then(|d| d.to_str()?.strip_prefix("alsa:"));
241 match (dest, device, buffer) {
242 (_, Some(""), _) => Err(CliError::usage(
243 "destination.no_device",
244 format!(
245 "`{name}=alsa:` names no device; write an ALSA device after it, as \
246 `alsa:default` or `alsa:hw:1`"
247 ),
248 )),
249 (_, Some(device), _) => Ok(Some(Destination::Alsa {
250 device: device.to_string(),
251 buffer_secs: buffer.unwrap_or(DEFAULT_BUFFER_SECS),
252 })),
253 (_, None, Some(_)) => Err(CliError::usage(
254 "query.buffer_without_device",
255 format!(
256 "`buffer` sizes a live device's buffer, and `{name}` names none; write \
257 `{name}(buffer=...)=alsa:<device>`, or drop `buffer`"
258 ),
259 )),
260 (dest, None, None) => Ok(dest.map(|d| Destination::File(d.to_path_buf()))),
261 }
262}
263
264fn split_outside(raw: &str, sep: char) -> Result<Vec<&str>, CliError> {
266 let (mut depth, mut from, mut out) = (0i32, 0, Vec::new());
267 for (at, c) in raw.char_indices() {
268 match c {
269 '(' => depth += 1,
270 ')' => depth -= 1,
271 c if c == sep && depth == 0 => {
272 out.push(&raw[from..at]);
273 from = at + c.len_utf8();
274 }
275 _ => {}
276 }
277 if depth < 0 {
278 return Err(unwritten(raw, "a `)` closes nothing"));
279 }
280 }
281 out.push(&raw[from..]);
282 Ok(out)
283}
284
285fn closing(text: &str) -> Option<usize> {
287 let mut depth = 0i32;
288 for (at, c) in text.char_indices() {
289 match c {
290 '(' => depth += 1,
291 ')' => {
292 depth -= 1;
293 if depth == 0 {
294 return Some(at);
295 }
296 }
297 _ => {}
298 }
299 }
300 None
301}
302
303fn unwritten(text: &str, why: &str) -> CliError {
304 CliError::usage(
305 "query.malformed",
306 format!(
307 "`{text}` is no reading: {why}. write a call, as `spectrum(peaks=8, frame=50ms)`, \
308 `=path` after it to write it to a file"
309 ),
310 )
311}
312
313pub fn wav_path(raw: &str) -> Result<PathBuf, CliError> {
314 match is_wav(Path::new(raw)) {
315 true => Ok(PathBuf::from(raw)),
316 false => Err(CliError::usage(
317 "analyze.not_wav",
318 format!("`{raw}` is no `.wav` file; mp3 and every other format are out of scope"),
319 )),
320 }
321}
322
323fn count(raw: &str, key: &str) -> Result<usize, CliError> {
324 raw.parse::<usize>().map_err(|_| {
325 CliError::usage(
326 "query.bad_value",
327 format!("`{key}` needs a whole count, got `{raw}`"),
328 )
329 })
330}
331
332fn factor(raw: &str) -> Result<u32, CliError> {
333 match raw.parse::<u32>() {
334 Ok(k) if (2..=16).contains(&k) && k.is_power_of_two() => Ok(k),
335 _ => Err(CliError::usage(
336 "query.bad_value",
337 format!("`oversample` needs a power of two from 2 to 16, got `{raw}`"),
338 )),
339 }
340}
341
342pub fn seconds(raw: &str, key: &str) -> Result<f64, CliError> {
344 let held = match sva_ast::tokenize(raw).as_deref() {
345 Ok([one]) => match one.kind {
346 sva_ast::TokenKind::Num(v)
347 | sva_ast::TokenKind::Time(v, sva_ast::SpanUnit::Seconds) => Some(v),
348 _ => None,
349 },
350 _ => None,
351 };
352 match held {
353 Some(v) if v > 0.0 && v.is_finite() => Ok(v),
354 _ => Err(CliError::usage(
355 "query.bad_value",
356 format!("`{key}` needs a positive time, as `50ms` or `0.05`, got `{raw}`"),
357 )),
358 }
359}
360
361fn tempo(raw: &str, key: &str) -> Result<f64, CliError> {
363 let held = match sva_ast::tokenize(raw).as_deref() {
364 Ok([one]) => match one.kind {
365 sva_ast::TokenKind::Num(v) => Some(v),
366 _ => None,
367 },
368 _ => None,
369 };
370 match held {
371 Some(v) if v > 0.0 && v.is_finite() => Ok(v),
372 _ => Err(CliError::usage(
373 "query.bad_value",
374 format!("`{key}` needs a positive tempo in beats per minute, as `140`, got `{raw}`"),
375 )),
376 }
377}
378
379fn microseconds(raw: &str, key: &str) -> Result<f64, CliError> {
381 let secs = seconds(raw, key)?;
382 match (1.0..=f64::from(i32::MAX)).contains(&(secs * 1e6).round()) {
383 true => Ok(secs),
384 false => Err(CliError::usage(
385 "query.bad_value",
386 format!(
387 "`{key}` is a whole number of microseconds from 1us to {}s, got `{raw}`",
388 i32::MAX / 1_000_000
389 ),
390 )),
391 }
392}
393
394fn decibels(raw: &str, key: &str) -> Result<f64, CliError> {
396 use sva_ast::{LogUnit::Decibels, TokenKind};
397 let kinds: Option<Vec<TokenKind>> = sva_ast::tokenize(raw)
398 .ok()
399 .map(|tokens| tokens.into_iter().map(|t| t.kind).collect());
400 let held = match kinds.as_deref() {
401 Some([TokenKind::Log(v, Decibels)]) => Some(*v),
402 Some([TokenKind::Minus, TokenKind::Log(v, Decibels)]) => Some(-v),
403 _ => None,
404 };
405 match held {
406 Some(v) if v.is_finite() => Ok(v),
407 _ => Err(CliError::usage(
408 "query.bad_value",
409 format!("`{key}` needs a level in decibels, as `-40db`, got `{raw}`"),
410 )),
411 }
412}
413
414fn norm(raw: &str) -> Result<Norm, CliError> {
415 [Norm::L1, Norm::L2]
416 .into_iter()
417 .find(|n| n.key() == raw)
418 .ok_or_else(|| {
419 CliError::usage(
420 "query.bad_value",
421 format!("`norm` is `l1` or `l2`, got `{raw}`"),
422 )
423 })
424}
425
426fn switch(raw: &str, key: &str) -> Result<bool, CliError> {
427 match raw {
428 "1" => Ok(true),
429 "0" => Ok(false),
430 _ => Err(CliError::usage(
431 "query.bad_value",
432 format!("`{key}` is `1` or `0`, got `{raw}`"),
433 )),
434 }
435}
436
437fn node(raw: &str) -> Result<String, CliError> {
439 match raw.strip_prefix('@') {
440 Some(path) if !path.is_empty() => Ok(path.to_string()),
441 _ => Err(CliError::usage(
442 "query.bad_value",
443 format!("`node` names an instance as a ref, as `@voice`, not `{raw}`"),
444 )),
445 }
446}