1use std::path::{Path, PathBuf};
4
5use sva_ast::SpanUnit;
6use sva_ast::score::{Field, Kind, Reading as Folding, Refused, evaluate};
7use sva_engine::harmony::voice_leading::Norm;
8use sva_engine::{
9 Arg, DEFAULT_BUFFER_SECS, DEFAULT_PRUNE_DB, Given, LEAST_PRUNE_DB, MAX_PINNED_FRAME, READINGS,
10 Reading, Representation, pinned_frame,
11};
12
13use crate::cli_error::CliError;
14
15pub const RETIRED: [(&str, &str); 2] = [
16 ("exact-envelope", "envelope, symbolic on a closed form"),
17 ("exact-derivative", "derivative, symbolic on a closed form"),
18];
19
20#[derive(Clone, Debug, PartialEq)]
23pub struct Call {
24 pub name: String,
25 pub args: Vec<(String, String)>,
26 pub dest: Option<PathBuf>,
28}
29
30#[derive(Clone, Debug, PartialEq)]
33pub enum Destination {
34 File(PathBuf),
35 Alsa {
36 device: String,
38 buffer_secs: f64,
40 },
41}
42
43const MOST_SAMPLES: f64 = 9_007_199_254_740_992.0;
46
47#[derive(Clone, Debug)]
50pub struct Asked {
51 pub name: String,
52 pub written: String,
54 given: Given,
55 times: Vec<(Arg, Time)>,
56 pub dest: Option<Destination>,
57 pub node: Option<String>,
59 pub brief: bool,
61 pub skim: bool,
63}
64
65impl PartialEq for Asked {
67 fn eq(&self, other: &Asked) -> bool {
68 (&self.name, &self.given, &self.times, &self.dest, &self.node)
69 == (
70 &other.name,
71 &other.given,
72 &other.times,
73 &other.dest,
74 &other.node,
75 )
76 && (self.brief, self.skim) == (other.brief, other.skim)
77 }
78}
79
80pub fn is_wav(path: &Path) -> bool {
81 path.extension()
82 .is_some_and(|e| e.eq_ignore_ascii_case("wav"))
83}
84
85pub fn retired(name: &str) -> Option<&'static str> {
86 RETIRED
87 .iter()
88 .find(|(gone, _)| *gone == name)
89 .map(|(_, write)| *write)
90}
91
92pub fn calls(text: &str) -> Result<Vec<Call>, CliError> {
95 let mut joined: Vec<String> = Vec::new();
96 for one in split_outside(text, ',')? {
97 match joined.last_mut() {
98 Some(last) if names_a_device(last) && !names_a_reading(one) => {
99 last.push(',');
100 last.push_str(one);
101 }
102 _ => joined.push(one.to_string()),
103 }
104 }
105 joined.iter().map(|one| call(one.trim())).collect()
106}
107
108fn names_a_device(call: &str) -> bool {
109 let close = call.rfind(')').map_or(0, |at| at + 1);
110 call[close..]
111 .split_once('=')
112 .is_some_and(|(_, dest)| dest.trim_start().starts_with("alsa:"))
113}
114
115fn names_a_reading(text: &str) -> bool {
116 let head = text[..text.find(['(', '=']).unwrap_or(text.len())].trim();
117 READINGS.iter().any(|row| row.name == head) || retired(head).is_some()
118}
119
120pub fn call(text: &str) -> Result<Call, CliError> {
122 let head_end = text.find(['(', '=']).unwrap_or(text.len());
123 let name = text[..head_end].trim().to_string();
124 if name.is_empty() {
125 return Err(unwritten(text, "it names no reading"));
126 }
127 let mut rest = &text[head_end..];
128 let mut args = Vec::new();
129 if rest.starts_with('(') {
130 let close = closing(rest).ok_or_else(|| unwritten(text, "its `(` is never closed"))?;
131 let inner = rest[1..close].trim();
132 if !inner.is_empty() {
133 for arg in split_outside(inner, ',')? {
134 let Some((key, value)) = arg.split_once('=') else {
135 return Err(unwritten(
136 text,
137 &format!("`{}` is no named argument; write `key=value`", arg.trim()),
138 ));
139 };
140 args.push((key.trim().to_string(), value.trim().to_string()));
141 }
142 }
143 rest = &rest[close + 1..];
144 }
145 let dest = match rest.trim_start().strip_prefix('=') {
146 Some("") => {
147 return Err(unwritten(
148 text,
149 "`=` names no destination; drop it to read the reading under \
150 `data.representations`",
151 ));
152 }
153 Some(path) => Some(PathBuf::from(path)),
154 None if rest.trim().is_empty() => None,
155 None => {
156 return Err(unwritten(
157 text,
158 &format!("`{}` follows the call", rest.trim()),
159 ));
160 }
161 };
162 Ok(Call { name, args, dest })
163}
164
165pub fn asked_all(calls: &[Call]) -> Result<Vec<Asked>, CliError> {
168 let mut out: Vec<Asked> = Vec::with_capacity(calls.len());
169 for call in calls {
170 let one = asked(call)?;
171 if out.iter().any(|held| held.name == one.name) {
172 return Err(CliError::usage(
173 "query.duplicate",
174 format!(
175 "`{}` is asked more than once; ask each reading once, every argument in \
176 its one call",
177 one.name
178 ),
179 ));
180 }
181 out.push(one);
182 }
183 Ok(out)
184}
185
186#[derive(Clone, Debug, PartialEq)]
188struct Time {
189 amount: f64,
190 unit: SpanUnit,
191 written: String,
192}
193
194impl Asked {
195 fn reading(&self) -> &'static Reading {
196 READINGS
197 .iter()
198 .find(|row| row.name == self.name)
199 .expect("an asked reading names a row")
200 }
201
202 pub fn off_samples(&self, sampled: bool) -> bool {
204 self.reading().reads.off_samples(sampled)
205 }
206
207 pub fn representation(
210 &self,
211 rate: u32,
212 seconds_per_bar: Option<f64>,
213 ) -> Result<Representation, CliError> {
214 let mut given = self.given;
215 for (arg, time) in &self.times {
216 let secs = match (time.unit, seconds_per_bar) {
217 (SpanUnit::Seconds, _) => time.amount,
218 (SpanUnit::Bars, Some(per)) => time.amount * per,
219 (SpanUnit::Bars, None) => {
220 return Err(CliError::BadTempo(format!(
221 "`{}({}={})` is written in bars, and this reading has no tempo to \
222 count them in: a render reads it off `variables/bpm` and \
223 `variables/meter`, and a file holds none; write seconds",
224 self.name,
225 arg.key(),
226 time.written
227 )));
228 }
229 };
230 if *arg == Arg::Frame && (secs * f64::from(rate)).round() < 1.0 {
231 return Err(CliError::usage(
232 "query.bad_value",
233 format!(
234 "`{}(frame={})` is {secs}s, under half a sample at {rate} Hz, and a \
235 frame steps by whole samples, rounded; ask at least {}s, half a sample",
236 self.name,
237 time.written,
238 0.5 / f64::from(rate)
239 ),
240 ));
241 }
242 let most = MOST_SAMPLES / f64::from(rate);
243 if secs > most {
244 return Err(CliError::usage(
245 "query.bad_value",
246 format!(
247 "`{}({}={})` is {secs}s, past the 2^53 samples a count holds exactly at \
248 {rate} Hz; ask at most {most}s",
249 self.name,
250 arg.key(),
251 time.written
252 ),
253 ));
254 }
255 match arg {
256 Arg::Frame => given.frame = Some(secs),
257 Arg::Window => given.window = Some(secs),
258 Arg::Cell => given.cell_secs = secs,
259 Arg::Smooth => given.smooth_secs = secs,
260 _ => unreachable!("only a time argument is held as a time"),
261 }
262 }
263 let representation = self.reading().representation(&given);
264 pinned(representation, rate)?;
265 Ok(representation)
266 }
267
268 pub fn path(&self) -> Option<&Path> {
269 match &self.dest {
270 Some(Destination::File(path)) => Some(path),
271 Some(Destination::Alsa { .. }) | None => None,
272 }
273 }
274}
275
276pub fn asked(call: &Call) -> Result<Asked, CliError> {
278 let name = call.name.as_str();
279 if let Some(write) = retired(name) {
280 return Err(CliError::usage(
281 "query.retired",
282 format!("`{name}` left the language; write `{write}`"),
283 ));
284 }
285 let Some(row) = READINGS.iter().find(|row| row.name == name) else {
286 let names: Vec<&str> = READINGS.iter().map(|row| row.name).collect();
287 return Err(CliError::usage(
288 "query.unknown_representation",
289 format!(
290 "unknown representation `{name}`; the representations are {}",
291 names.join(", ")
292 ),
293 ));
294 };
295 let (mut given, mut node, mut brief, mut skim) = (Given::default(), None, false, false);
296 let (mut buffer, mut times) = (None, Vec::new());
297 for (key, raw) in &call.args {
298 let Some(arg) = row.takes.iter().find(|arg| arg.key() == key) else {
299 let keys: Vec<&str> = row.takes.iter().map(|arg| arg.key()).collect();
300 return Err(CliError::usage(
301 "query.unknown_argument",
302 match keys[..] {
303 [] => format!("`{name}` takes no arguments, not `{key}`"),
304 _ => format!("`{name}` takes {}, not `{key}`", keys.join(", ")),
305 },
306 ));
307 };
308 match arg {
309 Arg::Peaks => given.peaks = count(raw, key)?,
310 Arg::Depth => given.depth = count(raw, key)?,
311 Arg::Frame | Arg::Window | Arg::Cell | Arg::Smooth => {
312 times.push((*arg, time(raw, key)?));
313 }
314 Arg::Oversample => given.oversample = factor(raw)?,
315 Arg::Brief => brief = switch(raw, key)?,
316 Arg::Skim => skim = switch(raw, key)?,
317 Arg::Node => node = Some(self::node(raw)?),
318 Arg::Buffer => buffer = Some(microseconds(raw, key)?),
319 Arg::Floor => given.floor_db = Some(decibels(raw, key)?),
320 Arg::Prune => given.prune_db = prune(raw)?,
321 Arg::Norm => given.norm = norm(raw)?,
322 Arg::Pairs => given.max_pairs = count(raw, key)?,
323 Arg::Curve => given.curve = switch(raw, key)?,
324 Arg::Bpm => given.bpm = Some(tempo(raw, key)?),
325 }
326 }
327 if row.takes.contains(&Arg::Prune) {
328 cut(name, given)?;
329 }
330 if row.takes.contains(&Arg::Node) && node.is_none() {
331 return Err(CliError::usage(
332 "query.missing_argument",
333 format!("`{name}` reads one instance's {name}; name it, as `{name}(node=@voice)`"),
334 ));
335 }
336 let written = match call.args.is_empty() {
337 true => name.to_string(),
338 false => {
339 let args: Vec<String> = call.args.iter().map(|(k, v)| format!("{k}={v}")).collect();
340 format!("{name}({})", args.join(", "))
341 }
342 };
343 Ok(Asked {
344 name: call.name.clone(),
345 written,
346 given,
347 times,
348 dest: destination(name, call.dest.as_deref(), buffer)?,
349 node,
350 brief,
351 skim,
352 })
353}
354
355fn destination(
357 name: &str,
358 dest: Option<&Path>,
359 buffer: Option<f64>,
360) -> Result<Option<Destination>, CliError> {
361 let device = dest.and_then(|d| d.to_str()?.strip_prefix("alsa:"));
362 match (dest, device, buffer) {
363 (_, Some(""), _) => Err(CliError::usage(
364 "destination.no_device",
365 format!(
366 "`{name}=alsa:` names no device; write an ALSA device after it, as \
367 `alsa:default` or `alsa:hw:1`"
368 ),
369 )),
370 (_, Some(device), _) => Ok(Some(Destination::Alsa {
371 device: device.to_string(),
372 buffer_secs: buffer.unwrap_or(DEFAULT_BUFFER_SECS),
373 })),
374 (_, None, Some(_)) => Err(CliError::usage(
375 "query.buffer_without_device",
376 format!(
377 "`buffer` sizes a live device's buffer, and `{name}` names none; write \
378 `{name}(buffer=...)=alsa:<device>`, or drop `buffer`"
379 ),
380 )),
381 (dest, None, None) => Ok(dest.map(|d| Destination::File(d.to_path_buf()))),
382 }
383}
384
385fn split_outside(raw: &str, sep: char) -> Result<Vec<&str>, CliError> {
387 let (mut depth, mut from, mut out) = (0i32, 0, Vec::new());
388 for (at, c) in raw.char_indices() {
389 match c {
390 '(' => depth += 1,
391 ')' => depth -= 1,
392 c if c == sep && depth == 0 => {
393 out.push(&raw[from..at]);
394 from = at + c.len_utf8();
395 }
396 _ => {}
397 }
398 if depth < 0 {
399 return Err(unwritten(raw, "a `)` closes nothing"));
400 }
401 }
402 out.push(&raw[from..]);
403 Ok(out)
404}
405
406fn closing(text: &str) -> Option<usize> {
408 let mut depth = 0i32;
409 for (at, c) in text.char_indices() {
410 match c {
411 '(' => depth += 1,
412 ')' => {
413 depth -= 1;
414 if depth == 0 {
415 return Some(at);
416 }
417 }
418 _ => {}
419 }
420 }
421 None
422}
423
424fn unwritten(text: &str, why: &str) -> CliError {
425 CliError::usage(
426 "query.malformed",
427 format!(
428 "`{text}` is no reading: {why}. write a call, as `spectrum(peaks=8, frame=50ms)`, \
429 `=path` after it to write it to a file"
430 ),
431 )
432}
433
434pub fn wav_path(raw: &str) -> Result<PathBuf, CliError> {
435 match is_wav(Path::new(raw)) {
436 true => Ok(PathBuf::from(raw)),
437 false => Err(CliError::usage(
438 "analyze.not_wav",
439 format!("`{raw}` is no `.wav` file; mp3 and every other format are out of scope"),
440 )),
441 }
442}
443
444fn count(raw: &str, key: &str) -> Result<usize, CliError> {
445 raw.parse::<usize>().map_err(|_| {
446 CliError::usage(
447 "query.bad_value",
448 format!("`{key}` needs a whole count, got `{raw}`"),
449 )
450 })
451}
452
453fn factor(raw: &str) -> Result<u32, CliError> {
454 match raw.parse::<u32>() {
455 Ok(k) if (2..=16).contains(&k) && k.is_power_of_two() => Ok(k),
456 _ => Err(CliError::usage(
457 "query.bad_value",
458 format!("`oversample` needs a power of two from 2 to 16, got `{raw}`"),
459 )),
460 }
461}
462
463fn time(raw: &str, key: &str) -> Result<Time, CliError> {
466 let bad = |why: String| {
467 CliError::usage(
468 "query.bad_value",
469 format!(
470 "`{key}` needs a positive time, in seconds as `50ms` or `0.05`, or in bars as \
471 `1b/8`: {why}"
472 ),
473 )
474 };
475 let folding = Folding::<f64> {
476 field: Field::Time,
477 mark: &|_| None,
478 degree: &|_| Err("a reading's time counts no degrees".to_string()),
479 own: None,
480 };
481 let value = evaluate(raw, &folding).map_err(|refused| match refused {
482 Refused::At(_, why) => bad(why),
483 Refused::Mark(name, _) => bad(format!(
484 "`mark({name})` is a score's, and a reading has none"
485 )),
486 })?;
487 let unit = match value.kind {
488 Kind::Bars => SpanUnit::Bars,
489 Kind::Seconds | Kind::Number => SpanUnit::Seconds,
490 other => return Err(bad(format!("`{raw}` is a {}", other.name()))),
491 };
492 match value.amount > 0.0 && value.amount.is_finite() {
493 true => Ok(Time {
494 amount: value.amount,
495 unit,
496 written: raw.to_string(),
497 }),
498 false => Err(bad(format!("`{raw}` is not over zero"))),
499 }
500}
501
502fn pinned(representation: Representation, rate: u32) -> Result<(), CliError> {
504 let Representation::Spectrum {
505 frame_secs: Some(secs),
506 ..
507 } = representation
508 else {
509 return Ok(());
510 };
511 let sr = f64::from(rate);
512 let frame = pinned_frame(secs, sr);
513 match frame > MAX_PINNED_FRAME {
514 true => Err(CliError::usage(
515 "query.bad_value",
516 format!(
517 "`spectrum(frame={secs})` sizes its transform to {frame} samples at {sr} Hz, \
518 past the {MAX_PINNED_FRAME}-sample bound"
519 ),
520 )),
521 false => Ok(()),
522 }
523}
524
525pub fn seconds(raw: &str, key: &str) -> Result<f64, CliError> {
527 let held = match sva_ast::tokenize(raw).as_deref() {
528 Ok([one]) => match one.kind {
529 sva_ast::TokenKind::Num(v)
530 | sva_ast::TokenKind::Time(v, sva_ast::SpanUnit::Seconds) => Some(v),
531 _ => None,
532 },
533 _ => None,
534 };
535 match held {
536 Some(v) if v > 0.0 && v.is_finite() => Ok(v),
537 _ => Err(CliError::usage(
538 "query.bad_value",
539 format!("`{key}` needs a positive time, as `50ms` or `0.05`, got `{raw}`"),
540 )),
541 }
542}
543
544fn tempo(raw: &str, key: &str) -> Result<f64, CliError> {
546 let held = match sva_ast::tokenize(raw).as_deref() {
547 Ok([one]) => match one.kind {
548 sva_ast::TokenKind::Num(v) => Some(v),
549 _ => None,
550 },
551 _ => None,
552 };
553 match held {
554 Some(v) if v > 0.0 && v.is_finite() => Ok(v),
555 _ => Err(CliError::usage(
556 "query.bad_value",
557 format!("`{key}` needs a positive tempo in beats per minute, as `140`, got `{raw}`"),
558 )),
559 }
560}
561
562fn microseconds(raw: &str, key: &str) -> Result<f64, CliError> {
564 let secs = seconds(raw, key)?;
565 match (1.0..=f64::from(i32::MAX)).contains(&(secs * 1e6).round()) {
566 true => Ok(secs),
567 false => Err(CliError::usage(
568 "query.bad_value",
569 format!(
570 "`{key}` is a whole number of microseconds from 1us to {}s, got `{raw}`",
571 i32::MAX / 1_000_000
572 ),
573 )),
574 }
575}
576
577fn decibels(raw: &str, key: &str) -> Result<f64, CliError> {
579 use sva_ast::{LogUnit::Decibels, TokenKind};
580 let kinds: Option<Vec<TokenKind>> = sva_ast::tokenize(raw)
581 .ok()
582 .map(|tokens| tokens.into_iter().map(|t| t.kind).collect());
583 let held = match kinds.as_deref() {
584 Some([TokenKind::Log(v, Decibels)]) => Some(*v),
585 Some([TokenKind::Minus, TokenKind::Log(v, Decibels)]) => Some(-v),
586 _ => None,
587 };
588 match held {
589 Some(v) if v.is_finite() => Ok(v),
590 _ => Err(CliError::usage(
591 "query.bad_value",
592 format!("`{key}` needs a level in decibels, as `-40db`, got `{raw}`"),
593 )),
594 }
595}
596
597fn prune(raw: &str) -> Result<Option<f64>, CliError> {
599 match raw {
600 "1" => return Ok(Some(DEFAULT_PRUNE_DB)),
601 "0" => return Ok(None),
602 _ => {}
603 }
604 let malformed = || {
605 CliError::usage(
606 "query.bad_value",
607 format!("`prune` is `1`, `0` or a level over 0db, as `0.5db`, got `{raw}`"),
608 )
609 };
610 match decibels(raw, "prune").map_err(|_| malformed())? {
611 db if db >= LEAST_PRUNE_DB => Ok(Some(db)),
612 db if db <= 0.0 => Err(malformed()),
613 _ => Err(CliError::usage(
614 "query.bad_value",
615 format!(
616 "`prune={raw}` is under {LEAST_PRUNE_DB}db, finer than an f64 holds a level \
617 of tens of dB, so no error evaluated in one could certify it; ask at least \
618 {LEAST_PRUNE_DB}db"
619 ),
620 )),
621 }
622}
623
624fn cut(name: &str, given: Given) -> Result<(), CliError> {
626 match (given.prune_db, given.floor_db) {
627 (None, Some(_)) => Err(CliError::usage(
628 "query.missing_argument",
629 format!(
630 "`floor` cuts a pruned track, and this `{name}` prunes none; write \
631 `{name}(prune=1, floor=...)`, or drop `floor`"
632 ),
633 )),
634 (Some(_), Some(db)) if db >= 0.0 => Err(CliError::usage(
635 "query.bad_value",
636 format!(
637 "`{name}(floor=...)` counts down from each band's peak, so it is under \
638 0db, as `-80db`, got {db}db"
639 ),
640 )),
641 _ => Ok(()),
642 }
643}
644
645fn norm(raw: &str) -> Result<Norm, CliError> {
646 [Norm::L1, Norm::L2]
647 .into_iter()
648 .find(|n| n.key() == raw)
649 .ok_or_else(|| {
650 CliError::usage(
651 "query.bad_value",
652 format!("`norm` is `l1` or `l2`, got `{raw}`"),
653 )
654 })
655}
656
657fn switch(raw: &str, key: &str) -> Result<bool, CliError> {
658 match raw {
659 "1" => Ok(true),
660 "0" => Ok(false),
661 _ => Err(CliError::usage(
662 "query.bad_value",
663 format!("`{key}` is `1` or `0`, got `{raw}`"),
664 )),
665 }
666}
667
668fn node(raw: &str) -> Result<String, CliError> {
670 match raw.strip_prefix('@') {
671 Some(path) if !path.is_empty() => Ok(path.to_string()),
672 _ => Err(CliError::usage(
673 "query.bad_value",
674 format!("`node` names an instance as a ref, as `@voice`, not `{raw}`"),
675 )),
676 }
677}