Skip to main content

sva_core/
query.rs

1// Concern: the observation both front ends ask for, and the window it is asked over | Non-concern: argv (sva-cli's args.rs), taking the reading (sva-engine) | IO: (names, window) -> Query or CliError
2
3use std::path::{Path, PathBuf};
4
5use sva_engine::{DEFAULT_FRAME_SECS, Horizon, Representation};
6
7use crate::cli_error::CliError;
8
9pub const REPRESENTATIONS: [&str; 15] = [
10    "lines",
11    "atoms",
12    "spectrum",
13    "envelope",
14    "derivative",
15    "samples",
16    "ledger",
17    "pitch",
18    "formants",
19    "stereo",
20    "bands",
21    "crest",
22    "loudness",
23    "alias",
24    "bindings",
25];
26
27pub const RETIRED: [(&str, &str); 2] = [
28    ("exact-envelope", "envelope, symbolic on a closed form"),
29    ("exact-derivative", "derivative, symbolic on a closed form"),
30];
31
32/// No destination is stdout.
33#[derive(Clone, Debug, PartialEq)]
34pub struct Asked {
35    pub name: String,
36    pub representation: Representation,
37    pub dest: Option<PathBuf>,
38}
39
40pub fn is_wav(path: &Path) -> bool {
41    path.extension()
42        .is_some_and(|e| e.eq_ignore_ascii_case("wav"))
43}
44
45pub const DEFAULT_OVERSAMPLE: u32 = 4;
46pub const DEFAULT_MAX_PEAKS: usize = 16;
47pub const DEFAULT_LEDGER_DEPTH: usize = 3;
48
49#[derive(Clone, Copy, Debug, PartialEq)]
50pub struct Shaping {
51    pub frame_secs: Option<f64>,
52    pub depth: usize,
53    pub peaks: usize,
54    pub oversample: u32,
55}
56
57impl Default for Shaping {
58    fn default() -> Shaping {
59        Shaping {
60            frame_secs: None,
61            depth: DEFAULT_LEDGER_DEPTH,
62            peaks: DEFAULT_MAX_PEAKS,
63            oversample: DEFAULT_OVERSAMPLE,
64        }
65    }
66}
67
68pub fn representation_for(name: &str, shape: Shaping) -> Option<Representation> {
69    let frame = shape.frame_secs.unwrap_or(DEFAULT_FRAME_SECS);
70    Some(match name {
71        "spectrum" => Representation::Spectrum {
72            max_peaks: shape.peaks,
73            frame_secs: shape.frame_secs,
74        },
75        "envelope" => Representation::Envelope {
76            frame_secs: shape.frame_secs,
77        },
78        "pitch" => Representation::Pitch {
79            max_notes: shape.peaks,
80            frame_secs: frame,
81        },
82        "formants" => Representation::Formants {
83            max_formants: shape.peaks,
84            frame_secs: frame,
85        },
86        "stereo" => Representation::Stereo { frame_secs: frame },
87        "ledger" => Representation::Ledger { depth: shape.depth },
88        "alias" => Representation::Alias {
89            oversample: shape.oversample,
90        },
91        other => Representation::from_name(other)?,
92    })
93}
94
95pub fn retired(name: &str) -> Option<&'static str> {
96    RETIRED
97        .iter()
98        .find(|(gone, _)| *gone == name)
99        .map(|(_, write)| *write)
100}
101
102/// What a window flag names, in the literals FORMAT 5.1 already spells: a bare number and
103/// `Ns` are seconds, `Nb` is bars, and `end` is the node's own extent.
104#[derive(Clone, Copy, Debug, PartialEq)]
105pub enum WindowEdge {
106    Secs(f64),
107    Bars(f64),
108    End,
109}
110
111pub fn window_edge(raw: &str, flag: &str) -> Result<WindowEdge, CliError> {
112    let refuse = || {
113        CliError::Usage(format!(
114            "{flag} needs a time — a number of seconds, `<n>s`, `<n>b`, or `end`, got `{raw}`"
115        ))
116    };
117    if raw == "end" {
118        return Ok(WindowEdge::End);
119    }
120    let (number, wrap): (&str, fn(f64) -> WindowEdge) = match raw.strip_suffix('b') {
121        Some(bars) => (bars, WindowEdge::Bars),
122        None => (raw.strip_suffix('s').unwrap_or(raw), WindowEdge::Secs),
123    };
124    match number.parse::<f64>() {
125        Ok(v) if v.is_finite() && v >= 0.0 => Ok(wrap(v)),
126        _ => Err(refuse()),
127    }
128}
129
130fn edge(
131    given: Option<WindowEdge>,
132    flag: &str,
133    unset: f64,
134    seconds_per_bar: Option<f64>,
135) -> Result<f64, CliError> {
136    let secs = match given {
137        None | Some(WindowEdge::End) => return Ok(unset),
138        Some(WindowEdge::Secs(v)) => v,
139        Some(WindowEdge::Bars(v)) => match seconds_per_bar {
140            Some(secs) => v * secs,
141            None => {
142                return Err(CliError::BadTempo(format!(
143                    "{flag} is written in bars and nothing here declares a tempo; \
144                     state `variables/bpm` and `variables/meter`, or write seconds"
145                )));
146            }
147        },
148    };
149    match secs.is_finite() && secs >= 0.0 {
150        true => Ok(secs),
151        false => Err(CliError::Usage(format!(
152            "{flag} needs a time at or after zero, got `{secs}`"
153        ))),
154    }
155}
156
157/// An unstated end, and `end` itself, are infinite here: `config_for` settles both against
158/// the node's own extent.
159pub fn window_for(
160    from: Option<WindowEdge>,
161    to: Option<WindowEdge>,
162    seconds_per_bar: Option<f64>,
163) -> Result<Horizon, CliError> {
164    Ok(Horizon::secs(
165        edge(from, "--from", 0.0, seconds_per_bar)?,
166        edge(to, "--to", f64::INFINITY, seconds_per_bar)?,
167    ))
168}