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