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, Silent};
6
7use crate::cli_error::CliError;
8
9pub const REPRESENTATIONS: [&str; 16] = [
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    "arguments",
26];
27
28pub const RETIRED: [(&str, &str); 2] = [
29    ("exact-envelope", "envelope, symbolic on a closed form"),
30    ("exact-derivative", "derivative, symbolic on a closed form"),
31];
32
33/// No destination is stdout.
34#[derive(Clone, Debug, PartialEq)]
35pub struct Asked {
36    pub name: String,
37    pub representation: Representation,
38    pub dest: Option<PathBuf>,
39}
40
41pub fn is_wav(path: &Path) -> bool {
42    path.extension()
43        .is_some_and(|e| e.eq_ignore_ascii_case("wav"))
44}
45
46pub const DEFAULT_OVERSAMPLE: u32 = 4;
47pub const DEFAULT_MAX_PEAKS: usize = 16;
48pub const DEFAULT_LEDGER_DEPTH: usize = 3;
49
50#[derive(Clone, Copy, Debug, PartialEq)]
51pub struct Shaping {
52    pub frame_secs: Option<f64>,
53    pub depth: usize,
54    pub peaks: usize,
55    pub oversample: u32,
56}
57
58impl Default for Shaping {
59    fn default() -> Shaping {
60        Shaping {
61            frame_secs: None,
62            depth: DEFAULT_LEDGER_DEPTH,
63            peaks: DEFAULT_MAX_PEAKS,
64            oversample: DEFAULT_OVERSAMPLE,
65        }
66    }
67}
68
69pub fn representation_for(name: &str, shape: Shaping) -> Option<Representation> {
70    let frame = shape.frame_secs.unwrap_or(DEFAULT_FRAME_SECS);
71    Some(match name {
72        "spectrum" => Representation::Spectrum {
73            max_peaks: shape.peaks,
74            frame_secs: shape.frame_secs,
75        },
76        "envelope" => Representation::Envelope {
77            frame_secs: shape.frame_secs,
78        },
79        "pitch" => Representation::Pitch {
80            max_notes: shape.peaks,
81            frame_secs: frame,
82        },
83        "formants" => Representation::Formants {
84            max_formants: shape.peaks,
85            frame_secs: frame,
86        },
87        "stereo" => Representation::Stereo { frame_secs: frame },
88        "ledger" => Representation::Ledger { depth: shape.depth },
89        "alias" => Representation::Alias {
90            oversample: shape.oversample,
91        },
92        other => Representation::from_name(other)?,
93    })
94}
95
96pub fn retired(name: &str) -> Option<&'static str> {
97    RETIRED
98        .iter()
99        .find(|(gone, _)| *gone == name)
100        .map(|(_, write)| *write)
101}
102
103/// What a window flag names, in the literals FORMAT 5.1 already spells: a bare number and
104/// `Ns` are seconds, `Nb` is bars, and `end` is the node's own extent.
105#[derive(Clone, Copy, Debug, PartialEq)]
106pub enum WindowEdge {
107    Secs(f64),
108    Bars(f64),
109    End,
110}
111
112/// The bits `--to silent` measures silence at where none are written: the profile's own.
113pub const DEFAULT_SILENT_BITS: u32 = sva_engine::PSYCHOACOUSTIC_V1.precision_bits as u32;
114
115/// The latest instant `--to silent` looks for silence by, where `--max` names none.
116pub const DEFAULT_SILENT_MAX_SECS: f64 = 60.0;
117
118/// `silent` or `silent:<bits>`; `None` for any other end.
119pub fn silent_edge(raw: &str) -> Option<Result<Option<u32>, CliError>> {
120    if raw == "silent" {
121        return Some(Ok(None));
122    }
123    let bits = raw.strip_prefix("silent:")?;
124    Some(bits.parse::<u32>().map(Some).map_err(|_| {
125        CliError::Usage(format!(
126            "--to silent takes whole bits, as `silent:16`, got `{raw}`"
127        ))
128    }))
129}
130
131/// A double holds 53 bits, so no finer silence is one it could show.
132pub fn silence(bits: Option<u32>, max_secs: Option<f64>) -> Result<Silent, CliError> {
133    let bits = bits.unwrap_or(DEFAULT_SILENT_BITS);
134    if !(1..=53).contains(&bits) {
135        return Err(CliError::Usage(format!(
136            "silence takes whole bits from 1 to 53, got {bits}"
137        )));
138    }
139    let max_secs = max_secs.unwrap_or(DEFAULT_SILENT_MAX_SECS);
140    if !(max_secs.is_finite() && max_secs > 0.0) {
141        return Err(CliError::Usage(format!(
142            "silence is looked for until a time past zero, got {max_secs}"
143        )));
144    }
145    Ok(Silent { bits, max_secs })
146}
147
148pub fn window_edge(raw: &str, flag: &str) -> Result<WindowEdge, CliError> {
149    let refuse = || {
150        CliError::Usage(format!(
151            "{flag} needs a time — a number of seconds, `<n>s`, `<n>b`, or `end`, got `{raw}`"
152        ))
153    };
154    if raw == "end" {
155        return Ok(WindowEdge::End);
156    }
157    let (number, wrap): (&str, fn(f64) -> WindowEdge) = match raw.strip_suffix('b') {
158        Some(bars) => (bars, WindowEdge::Bars),
159        None => (raw.strip_suffix('s').unwrap_or(raw), WindowEdge::Secs),
160    };
161    match number.parse::<f64>() {
162        Ok(v) if v.is_finite() && v >= 0.0 => Ok(wrap(v)),
163        _ => Err(refuse()),
164    }
165}
166
167fn edge(
168    given: Option<WindowEdge>,
169    flag: &str,
170    unset: f64,
171    seconds_per_bar: Option<f64>,
172) -> Result<f64, CliError> {
173    let secs = match given {
174        None | Some(WindowEdge::End) => return Ok(unset),
175        Some(WindowEdge::Secs(v)) => v,
176        Some(WindowEdge::Bars(v)) => match seconds_per_bar {
177            Some(secs) => v * secs,
178            None => {
179                return Err(CliError::BadTempo(format!(
180                    "{flag} is written in bars and nothing here declares a tempo; \
181                     state `variables/bpm` and `variables/meter`, or write seconds"
182                )));
183            }
184        },
185    };
186    match secs.is_finite() && secs >= 0.0 {
187        true => Ok(secs),
188        false => Err(CliError::Usage(format!(
189            "{flag} needs a time at or after zero, got `{secs}`"
190        ))),
191    }
192}
193
194/// An unstated end, and `end` itself, are infinite here: `config_for` settles both against
195/// the node's own extent.
196pub fn window_for(
197    from: Option<WindowEdge>,
198    to: Option<WindowEdge>,
199    seconds_per_bar: Option<f64>,
200) -> Result<Horizon, CliError> {
201    Ok(Horizon::secs(
202        edge(from, "--from", 0.0, seconds_per_bar)?,
203        edge(to, "--to", f64::INFINITY, seconds_per_bar)?,
204    ))
205}