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_FRAME_SECS, Given, Harmonic, MAX_PINNED_FRAME, READINGS,
10 Reading, Representation, pinned_frame,
11};
12
13use crate::cli_error::CliError;
14
15pub const RETIRED: [(&str, &str); 4] = [
16 ("exact-envelope", "envelope, symbolic on a closed form"),
17 ("exact-derivative", "derivative, symbolic on a closed form"),
18 ("onsets", "score, each event's `at_secs`"),
19 (
20 "panel",
21 "sharpness,beating,envelope(frame=10ms),pitch(frame=100ms, peaks=1)",
22 ),
23];
24
25#[derive(Clone, Debug, PartialEq)]
28pub struct Call {
29 pub name: String,
30 pub args: Vec<(String, String)>,
31 pub dest: Option<PathBuf>,
33}
34
35#[derive(Clone, Debug, PartialEq)]
38pub enum Destination {
39 File(PathBuf),
40 Alsa {
41 device: String,
43 buffer_secs: f64,
45 },
46}
47
48const MOST_SAMPLES: f64 = 9_007_199_254_740_992.0;
51
52#[derive(Clone, Debug, PartialEq)]
55pub struct Asked {
56 pub name: String,
57 given: Given,
58 times: Vec<(Arg, Time)>,
59 pub dest: Option<Destination>,
60 pub node: Option<String>,
62 pub brief: bool,
64 pub skim: bool,
66}
67
68pub fn is_wav(path: &Path) -> bool {
69 path.extension()
70 .is_some_and(|e| e.eq_ignore_ascii_case("wav"))
71}
72
73pub fn retired(name: &str) -> Option<&'static str> {
74 RETIRED
75 .iter()
76 .find(|(gone, _)| *gone == name)
77 .map(|(_, write)| *write)
78}
79
80pub fn calls(text: &str) -> Result<Vec<Call>, CliError> {
83 let mut joined: Vec<String> = Vec::new();
84 for one in split_outside(text, ',')? {
85 match joined.last_mut() {
86 Some(last) if names_a_device(last) && !names_a_reading(one) => {
87 last.push(',');
88 last.push_str(one);
89 }
90 _ => joined.push(one.to_string()),
91 }
92 }
93 joined.iter().map(|one| call(one.trim())).collect()
94}
95
96fn names_a_device(call: &str) -> bool {
97 let close = call.rfind(')').map_or(0, |at| at + 1);
98 call[close..]
99 .split_once('=')
100 .is_some_and(|(_, dest)| dest.trim_start().starts_with("alsa:"))
101}
102
103fn names_a_reading(text: &str) -> bool {
104 let head = text[..text.find(['(', '=']).unwrap_or(text.len())].trim();
105 READINGS.iter().any(|row| row.name == head) || retired(head).is_some()
106}
107
108pub fn call(text: &str) -> Result<Call, CliError> {
110 let head_end = text.find(['(', '=']).unwrap_or(text.len());
111 let name = text[..head_end].trim().to_string();
112 if name.is_empty() {
113 return Err(unwritten(text, "it names no reading"));
114 }
115 let mut rest = &text[head_end..];
116 let mut args = Vec::new();
117 if rest.starts_with('(') {
118 let close = closing(rest).ok_or_else(|| unwritten(text, "its `(` is never closed"))?;
119 let inner = rest[1..close].trim();
120 if !inner.is_empty() {
121 for arg in split_outside(inner, ',')? {
122 let Some((key, value)) = arg.split_once('=') else {
123 return Err(unwritten(
124 text,
125 &format!("`{}` is no named argument; write `key=value`", arg.trim()),
126 ));
127 };
128 args.push((key.trim().to_string(), value.trim().to_string()));
129 }
130 }
131 rest = &rest[close + 1..];
132 }
133 let dest = match rest.trim_start().strip_prefix('=') {
134 Some("") => {
135 return Err(unwritten(
136 text,
137 "`=` names no destination; drop it to read the reading under \
138 `data.representations`",
139 ));
140 }
141 Some(path) => Some(PathBuf::from(path)),
142 None if rest.trim().is_empty() => None,
143 None => {
144 return Err(unwritten(
145 text,
146 &format!("`{}` follows the call", rest.trim()),
147 ));
148 }
149 };
150 Ok(Call { name, args, dest })
151}
152
153pub fn asked_all(calls: &[Call]) -> Result<Vec<Asked>, CliError> {
156 let mut out: Vec<Asked> = Vec::with_capacity(calls.len());
157 for call in calls {
158 let one = asked(call)?;
159 if out.iter().any(|held| held.name == one.name) {
160 return Err(CliError::usage(
161 "query.duplicate",
162 format!(
163 "`{}` is asked more than once; ask each reading once, every argument in \
164 its one call",
165 one.name
166 ),
167 ));
168 }
169 out.push(one);
170 }
171 Ok(out)
172}
173
174#[derive(Clone, Debug, PartialEq)]
176struct Time {
177 amount: f64,
178 unit: SpanUnit,
179 written: String,
180}
181
182impl Asked {
183 fn reading(&self) -> &'static Reading {
184 READINGS
185 .iter()
186 .find(|row| row.name == self.name)
187 .expect("an asked reading names a row")
188 }
189
190 pub fn off_samples(&self, sampled: bool) -> bool {
192 self.reading().reads.off_samples(sampled)
193 }
194
195 pub fn representation(
198 &self,
199 rate: u32,
200 seconds_per_bar: Option<f64>,
201 ) -> Result<Representation, CliError> {
202 let given = self.resolved(rate, seconds_per_bar)?;
203 let representation = self.reading().representation(&given);
204 pinned(representation, rate)?;
205 Ok(representation)
206 }
207
208 pub fn args(
213 &self,
214 rate: u32,
215 seconds_per_bar: Option<f64>,
216 taken: Taken,
217 ) -> Result<Vec<(&'static str, Option<Spelled>)>, CliError> {
218 use Representation as R;
219 let count = |n: usize| Some(Spelled::Number(n.to_string()));
220 let secs = |s: f64| Some(Spelled::Text(spelled_secs(s)));
221 let db = |d: f64| Some(Spelled::Text(format!("{d}db")));
222 let switch = |on: bool| Some(Spelled::Number(u8::from(on).to_string()));
223 let text = |t: &str| Some(Spelled::Text(t.to_string()));
224 let frame = |written: Option<f64>, unwritten: Option<f64>| match taken {
225 Taken::Exact => written.and_then(secs),
226 Taken::Measured { .. } => written.or(unwritten).and_then(secs),
227 };
228 let mut held = match self.representation(rate, seconds_per_bar)? {
229 R::Spectrum {
230 max_peaks,
231 frame_secs,
232 } => {
233 let fitted = match taken {
234 Taken::Measured { fitted_frame } => fitted_frame,
235 Taken::Exact => None,
236 };
237 let fitted = fitted.map(|samples| samples as f64 / f64::from(rate));
238 vec![
239 (Arg::Peaks, count(max_peaks)),
240 (Arg::Frame, frame(frame_secs, fitted)),
241 ]
242 }
243 R::Envelope { frame_secs } => {
244 vec![(Arg::Frame, frame(frame_secs, Some(DEFAULT_FRAME_SECS)))]
245 }
246 R::Ledger { depth } => vec![(Arg::Depth, count(depth))],
247 R::Pitch {
248 max_notes: peaks,
249 frame_secs,
250 }
251 | R::Formants {
252 max_formants: peaks,
253 frame_secs,
254 } => vec![(Arg::Peaks, count(peaks)), (Arg::Frame, secs(frame_secs))],
255 R::Stereo { frame_secs } => vec![(Arg::Frame, secs(frame_secs))],
256 R::Loudness { window_secs } => vec![(Arg::Window, secs(window_secs))],
257 R::Bands => vec![],
258 R::Alias { oversample } => vec![(Arg::Oversample, count(oversample as usize))],
259 R::Harmony {
260 reading,
261 frame_secs,
262 floor_db,
263 } => {
264 let own = match reading {
265 Harmonic::Dissonance { max_pairs } => Some((Arg::Pairs, count(max_pairs))),
266 Harmonic::VoiceLeading { norm } => Some((Arg::Norm, text(norm.key()))),
267 Harmonic::TonalDistance | Harmonic::Inharmonicity => None,
268 };
269 let shared = [(Arg::Frame, secs(frame_secs)), (Arg::Floor, db(floor_db))];
270 shared.into_iter().chain(own).collect()
271 }
272 R::Periodicity {
273 frame_secs,
274 window_secs,
275 curve,
276 } => vec![
277 (Arg::Frame, secs(frame_secs)),
278 (Arg::Window, secs(window_secs)),
279 (Arg::Curve, switch(curve)),
280 ],
281 R::Modulation {
282 frame_secs,
283 window_secs,
284 bpm,
285 } => vec![
286 (Arg::Frame, secs(frame_secs)),
287 (Arg::Window, secs(window_secs)),
288 (Arg::Bpm, bpm.map(|b| Spelled::Number(b.to_string()))),
289 ],
290 R::SelfSimilarity {
291 frame_secs,
292 cell_secs,
293 smooth_secs,
294 window_secs,
295 floor_db,
296 listed,
297 curve,
298 } => vec![
299 (Arg::Frame, secs(frame_secs)),
300 (Arg::Cell, secs(cell_secs)),
301 (Arg::Smooth, secs(smooth_secs)),
302 (Arg::Window, secs(window_secs)),
303 (Arg::Floor, db(floor_db)),
304 (Arg::Peaks, count(listed)),
305 (Arg::Curve, switch(curve)),
306 ],
307 R::Lines
308 | R::Atoms
309 | R::Derivative
310 | R::Samples
311 | R::Crest
312 | R::Score
313 | R::Bindings
314 | R::Arguments => Vec::new(),
315 R::Partials {
316 frame_secs,
317 window_secs,
318 hop_secs,
319 floor_db,
320 tracks,
321 } => vec![
322 (Arg::Frame, secs(frame_secs)),
323 (Arg::Window, secs(window_secs)),
324 (Arg::Hop, secs(hop_secs)),
325 (Arg::Floor, db(floor_db)),
326 (Arg::Peaks, count(tracks)),
327 ],
328 R::LoudnessPattern { frame_secs, spl_db } | R::Sharpness { frame_secs, spl_db } => {
329 vec![(Arg::Frame, secs(frame_secs)), (Arg::Spl, db(spl_db))]
330 }
331 R::Beating {
332 frame_secs,
333 window_secs,
334 spl_db,
335 } => vec![
336 (Arg::Frame, secs(frame_secs)),
337 (Arg::Window, secs(window_secs)),
338 (Arg::Spl, db(spl_db)),
339 ],
340 };
341 let buffer = match &self.dest {
342 Some(Destination::Alsa { buffer_secs, .. }) => secs(*buffer_secs),
343 _ => None,
344 };
345 held.extend([
346 (Arg::Brief, switch(self.brief)),
347 (Arg::Skim, switch(self.skim)),
348 (
349 Arg::Node,
350 self.node.as_ref().and_then(|n| text(&format!("@{n}"))),
351 ),
352 (Arg::Buffer, buffer),
353 ]);
354 Ok(self
355 .reading()
356 .takes
357 .iter()
358 .map(|arg| {
359 let (_, value) = held
360 .iter()
361 .find(|(one, _)| one == arg)
362 .expect("a reading holds every argument it takes");
363 (arg.key(), value.clone())
364 })
365 .collect())
366 }
367
368 fn resolved(&self, rate: u32, seconds_per_bar: Option<f64>) -> Result<Given, CliError> {
370 let mut given = self.given;
371 for (arg, time) in &self.times {
372 let secs = match (time.unit, seconds_per_bar) {
373 (SpanUnit::Seconds, _) => time.amount,
374 (SpanUnit::Bars, Some(per)) => time.amount * per,
375 (SpanUnit::Bars, None) => {
376 return Err(CliError::BadTempo(format!(
377 "`{}({}={})` is written in bars, and this reading has no tempo to \
378 count them in: a render reads it off `variables/bpm` and \
379 `variables/meter`, and a file holds none; write seconds",
380 self.name,
381 arg.key(),
382 time.written
383 )));
384 }
385 };
386 if matches!(arg, Arg::Frame | Arg::Hop) && (secs * f64::from(rate)).round() < 1.0 {
387 return Err(CliError::usage(
388 "query.bad_value",
389 format!(
390 "`{}({}={})` is {secs}s, under half a sample at {rate} Hz, and a \
391 {} steps by whole samples, rounded; ask at least {}s, half a sample",
392 self.name,
393 arg.key(),
394 time.written,
395 arg.key(),
396 0.5 / f64::from(rate)
397 ),
398 ));
399 }
400 let most = MOST_SAMPLES / f64::from(rate);
401 if secs > most {
402 return Err(CliError::usage(
403 "query.bad_value",
404 format!(
405 "`{}({}={})` is {secs}s, past the 2^53 samples a count holds exactly at \
406 {rate} Hz; ask at most {most}s",
407 self.name,
408 arg.key(),
409 time.written
410 ),
411 ));
412 }
413 match arg {
414 Arg::Frame => given.frame = Some(secs),
415 Arg::Window => given.window = Some(secs),
416 Arg::Cell => given.cell_secs = secs,
417 Arg::Smooth => given.smooth_secs = secs,
418 Arg::Hop => given.hop = Some(secs),
419 _ => unreachable!("only a time argument is held as a time"),
420 }
421 }
422 Ok(given)
423 }
424
425 pub fn path(&self) -> Option<&Path> {
426 match &self.dest {
427 Some(Destination::File(path)) => Some(path),
428 Some(Destination::Alsa { .. }) | None => None,
429 }
430 }
431}
432
433pub fn asked(call: &Call) -> Result<Asked, CliError> {
435 let name = call.name.as_str();
436 if let Some(write) = retired(name) {
437 return Err(CliError::usage(
438 "query.retired",
439 format!("`{name}` left the language; write `{write}`"),
440 ));
441 }
442 let Some(row) = READINGS.iter().find(|row| row.name == name) else {
443 let names: Vec<&str> = READINGS.iter().map(|row| row.name).collect();
444 return Err(CliError::usage(
445 "query.unknown_representation",
446 format!(
447 "unknown representation `{name}`; the representations are {}",
448 names.join(", ")
449 ),
450 ));
451 };
452 let (mut given, mut node, mut brief, mut skim) = (Given::default(), None, false, false);
453 let (mut buffer, mut times) = (None, Vec::new());
454 for (key, raw) in &call.args {
455 let Some(arg) = row.takes.iter().find(|arg| arg.key() == key) else {
456 let keys: Vec<&str> = row.takes.iter().map(|arg| arg.key()).collect();
457 return Err(CliError::usage(
458 "query.unknown_argument",
459 match keys[..] {
460 [] => format!("`{name}` takes no arguments, not `{key}`"),
461 _ => format!("`{name}` takes {}, not `{key}`", keys.join(", ")),
462 },
463 ));
464 };
465 match arg {
466 Arg::Peaks => given.peaks = Some(count(raw, key)?),
467 Arg::Depth => given.depth = count(raw, key)?,
468 Arg::Frame | Arg::Window | Arg::Cell | Arg::Smooth | Arg::Hop => {
469 times.push((*arg, time(raw, key)?));
470 }
471 Arg::Oversample => given.oversample = factor(raw)?,
472 Arg::Brief => brief = switch(raw, key)?,
473 Arg::Skim => skim = switch(raw, key)?,
474 Arg::Node => node = Some(self::node(raw)?),
475 Arg::Buffer => buffer = Some(microseconds(raw, key)?),
476 Arg::Floor => given.floor_db = Some(decibels(raw, key)?),
477 Arg::Spl => given.spl_db = Some(decibels(raw, key)?),
478 Arg::Norm => given.norm = norm(raw)?,
479 Arg::Pairs => given.max_pairs = count(raw, key)?,
480 Arg::Curve => given.curve = switch(raw, key)?,
481 Arg::Bpm => given.bpm = Some(tempo(raw, key)?),
482 }
483 }
484 if row.takes.contains(&Arg::Node) && node.is_none() {
485 return Err(CliError::usage(
486 "query.missing_argument",
487 format!("`{name}` reads one instance's {name}; name it, as `{name}(node=@voice)`"),
488 ));
489 }
490 Ok(Asked {
491 name: call.name.clone(),
492 given,
493 times,
494 dest: destination(name, call.dest.as_deref(), buffer)?,
495 node,
496 brief,
497 skim,
498 })
499}
500
501fn destination(
503 name: &str,
504 dest: Option<&Path>,
505 buffer: Option<f64>,
506) -> Result<Option<Destination>, CliError> {
507 let device = dest.and_then(|d| d.to_str()?.strip_prefix("alsa:"));
508 match (dest, device, buffer) {
509 (_, Some(""), _) => Err(CliError::usage(
510 "destination.no_device",
511 format!(
512 "`{name}=alsa:` names no device; write an ALSA device after it, as \
513 `alsa:default` or `alsa:hw:1`"
514 ),
515 )),
516 (_, Some(device), _) => Ok(Some(Destination::Alsa {
517 device: device.to_string(),
518 buffer_secs: buffer.unwrap_or(DEFAULT_BUFFER_SECS),
519 })),
520 (_, None, Some(_)) => Err(CliError::usage(
521 "query.buffer_without_device",
522 format!(
523 "`buffer` sizes a live device's buffer, and `{name}` names none; write \
524 `{name}(buffer=...)=alsa:<device>`, or drop `buffer`"
525 ),
526 )),
527 (dest, None, None) => Ok(dest.map(|d| Destination::File(d.to_path_buf()))),
528 }
529}
530
531fn split_outside(raw: &str, sep: char) -> Result<Vec<&str>, CliError> {
533 let (mut depth, mut from, mut out) = (0i32, 0, Vec::new());
534 for (at, c) in raw.char_indices() {
535 match c {
536 '(' => depth += 1,
537 ')' => depth -= 1,
538 c if c == sep && depth == 0 => {
539 out.push(&raw[from..at]);
540 from = at + c.len_utf8();
541 }
542 _ => {}
543 }
544 if depth < 0 {
545 return Err(unwritten(raw, "a `)` closes nothing"));
546 }
547 }
548 out.push(&raw[from..]);
549 Ok(out)
550}
551
552fn closing(text: &str) -> Option<usize> {
554 let mut depth = 0i32;
555 for (at, c) in text.char_indices() {
556 match c {
557 '(' => depth += 1,
558 ')' => {
559 depth -= 1;
560 if depth == 0 {
561 return Some(at);
562 }
563 }
564 _ => {}
565 }
566 }
567 None
568}
569
570fn unwritten(text: &str, why: &str) -> CliError {
571 CliError::usage(
572 "query.malformed",
573 format!(
574 "`{text}` is no reading: {why}. write a call, as `spectrum(peaks=8, frame=50ms)`, \
575 `=path` after it to write it to a file"
576 ),
577 )
578}
579
580fn count(raw: &str, key: &str) -> Result<usize, CliError> {
581 raw.parse::<usize>().map_err(|_| {
582 CliError::usage(
583 "query.bad_value",
584 format!("`{key}` needs a whole count, got `{raw}`"),
585 )
586 })
587}
588
589fn factor(raw: &str) -> Result<u32, CliError> {
590 match raw.parse::<u32>() {
591 Ok(k) if (2..=16).contains(&k) && k.is_power_of_two() => Ok(k),
592 _ => Err(CliError::usage(
593 "query.bad_value",
594 format!("`oversample` needs a power of two from 2 to 16, got `{raw}`"),
595 )),
596 }
597}
598
599fn time(raw: &str, key: &str) -> Result<Time, CliError> {
602 let bad = |why: String| {
603 CliError::usage(
604 "query.bad_value",
605 format!(
606 "`{key}` needs a positive time, in seconds as `50ms` or `0.05`, or in bars as \
607 `1b/8`: {why}"
608 ),
609 )
610 };
611 let folding = Folding::<f64> {
612 field: Field::Time,
613 mark: &|_| None,
614 degree: &|_| Err("a reading's time counts no degrees".to_string()),
615 own: None,
616 };
617 let value = evaluate(raw, &folding).map_err(|refused| match refused {
618 Refused::At(_, why) => bad(why),
619 Refused::Mark(name, _) => bad(format!(
620 "`mark({name})` is a score's, and a reading has none"
621 )),
622 })?;
623 let unit = match value.kind {
624 Kind::Bars => SpanUnit::Bars,
625 Kind::Seconds | Kind::Number => SpanUnit::Seconds,
626 other => return Err(bad(format!("`{raw}` is a {}", other.name()))),
627 };
628 match value.amount > 0.0 && value.amount.is_finite() {
629 true => Ok(Time {
630 amount: value.amount,
631 unit,
632 written: raw.to_string(),
633 }),
634 false => Err(bad(format!("`{raw}` is not over zero"))),
635 }
636}
637
638#[derive(Clone, Copy, Debug, PartialEq)]
640pub enum Taken {
641 Exact,
643 Measured { fitted_frame: Option<usize> },
645}
646
647#[derive(Clone, Debug, PartialEq)]
649pub enum Spelled {
650 Number(String),
651 Text(String),
652}
653
654impl Spelled {
655 pub fn text(&self) -> &str {
656 match self {
657 Spelled::Number(t) | Spelled::Text(t) => t,
658 }
659 }
660}
661
662fn spelled_secs(secs: f64) -> String {
665 let ms = secs * 1000.0;
666 match secs < 1.0 && ms / 1000.0 == secs {
667 true => format!("{ms}ms"),
668 false => format!("{secs}s"),
669 }
670}
671
672fn pinned(representation: Representation, rate: u32) -> Result<(), CliError> {
674 let Representation::Spectrum {
675 frame_secs: Some(secs),
676 ..
677 } = representation
678 else {
679 return Ok(());
680 };
681 let sr = f64::from(rate);
682 let frame = pinned_frame(secs, sr);
683 match frame > MAX_PINNED_FRAME {
684 true => Err(CliError::usage(
685 "query.bad_value",
686 format!(
687 "`spectrum(frame={secs})` sizes its transform to {frame} samples at {sr} Hz, \
688 past the {MAX_PINNED_FRAME}-sample bound"
689 ),
690 )),
691 false => Ok(()),
692 }
693}
694
695pub fn seconds(raw: &str, key: &str) -> Result<f64, CliError> {
697 let held = match sva_ast::tokenize(raw).as_deref() {
698 Ok([one]) => match one.kind {
699 sva_ast::TokenKind::Num(v)
700 | sva_ast::TokenKind::Time(v, sva_ast::SpanUnit::Seconds) => Some(v),
701 _ => None,
702 },
703 _ => None,
704 };
705 match held {
706 Some(v) if v > 0.0 && v.is_finite() => Ok(v),
707 _ => Err(CliError::usage(
708 "query.bad_value",
709 format!("`{key}` needs a positive time, as `50ms` or `0.05`, got `{raw}`"),
710 )),
711 }
712}
713
714fn tempo(raw: &str, key: &str) -> Result<f64, CliError> {
716 let held = match sva_ast::tokenize(raw).as_deref() {
717 Ok([one]) => match one.kind {
718 sva_ast::TokenKind::Num(v) => Some(v),
719 _ => None,
720 },
721 _ => None,
722 };
723 match held {
724 Some(v) if v > 0.0 && v.is_finite() => Ok(v),
725 _ => Err(CliError::usage(
726 "query.bad_value",
727 format!("`{key}` needs a positive tempo in beats per minute, as `140`, got `{raw}`"),
728 )),
729 }
730}
731
732fn microseconds(raw: &str, key: &str) -> Result<f64, CliError> {
734 let secs = seconds(raw, key)?;
735 match (1.0..=f64::from(i32::MAX)).contains(&(secs * 1e6).round()) {
736 true => Ok(secs),
737 false => Err(CliError::usage(
738 "query.bad_value",
739 format!(
740 "`{key}` is a whole number of microseconds from 1us to {}s, got `{raw}`",
741 i32::MAX / 1_000_000
742 ),
743 )),
744 }
745}
746
747fn decibels(raw: &str, key: &str) -> Result<f64, CliError> {
749 use sva_ast::{LogUnit::Decibels, TokenKind};
750 let kinds: Option<Vec<TokenKind>> = sva_ast::tokenize(raw)
751 .ok()
752 .map(|tokens| tokens.into_iter().map(|t| t.kind).collect());
753 let held = match kinds.as_deref() {
754 Some([TokenKind::Log(v, Decibels)]) => Some(*v),
755 Some([TokenKind::Minus, TokenKind::Log(v, Decibels)]) => Some(-v),
756 _ => None,
757 };
758 match held {
759 Some(v) if v.is_finite() => Ok(v),
760 _ => Err(CliError::usage(
761 "query.bad_value",
762 format!("`{key}` needs a level in decibels, as `-40db`, got `{raw}`"),
763 )),
764 }
765}
766
767fn norm(raw: &str) -> Result<Norm, CliError> {
768 [Norm::L1, Norm::L2]
769 .into_iter()
770 .find(|n| n.key() == raw)
771 .ok_or_else(|| {
772 CliError::usage(
773 "query.bad_value",
774 format!("`norm` is `l1` or `l2`, got `{raw}`"),
775 )
776 })
777}
778
779fn switch(raw: &str, key: &str) -> Result<bool, CliError> {
780 match raw {
781 "1" => Ok(true),
782 "0" => Ok(false),
783 _ => Err(CliError::usage(
784 "query.bad_value",
785 format!("`{key}` is `1` or `0`, got `{raw}`"),
786 )),
787 }
788}
789
790fn node(raw: &str) -> Result<String, CliError> {
792 match raw.strip_prefix('@') {
793 Some(path) if !path.is_empty() => Ok(path.to_string()),
794 _ => Err(CliError::usage(
795 "query.bad_value",
796 format!("`node` names an instance as a ref, as `@voice`, not `{raw}`"),
797 )),
798 }
799}