Skip to main content

sva_core/
query.rs

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