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