Skip to main content

sva_cli/args/
solve.rs

1// Concern: parses a solve's cost, its variables, stops and flags | Non-concern: running it (solve.rs), the cost's grammar (sva-core) | IO: (argv tail) -> Command or CliError
2
3use std::path::PathBuf;
4
5use super::reading::{count, hertz};
6use super::{CacheAt, Command, value};
7use sva_core::CliError;
8use sva_core::solve::{
9    Cost, DEFAULT_COST_TOL, DEFAULT_MAX_RENDERS, DEFAULT_STEP_TOL, Stops, Vary, cost, vary,
10};
11
12#[derive(Debug, PartialEq)]
13pub struct SolveArgs {
14    pub cost: Cost,
15    pub vary: Vec<Vary>,
16    pub stops: Stops,
17    pub sets: Vec<(String, String)>,
18    pub rate: Option<u32>,
19    pub threads: Option<std::num::NonZeroUsize>,
20    pub cache: CacheAt,
21    pub write: bool,
22    pub confirm: bool,
23}
24
25fn number(raw: &str, flag: &str, fits: impl Fn(f64) -> bool, says: &str) -> Result<f64, CliError> {
26    match raw.parse::<f64>() {
27        Ok(v) if v.is_finite() && fits(v) => Ok(v),
28        _ => Err(CliError::usage(
29            "cli.bad_value",
30            format!("{flag} takes {says}, not `{raw}`"),
31        )),
32    }
33}
34
35pub(super) fn solve_args(rest: &[String]) -> Result<Command, CliError> {
36    let mut it = rest.iter();
37    let written = it.next().filter(|a| !a.starts_with("--")).ok_or_else(|| {
38        CliError::usage(
39            "cli.missing_argument",
40            "solve needs a cost, as `sva-cli solve 'norm(repr.bands(@master) - \
41             repr.bands(sample(\"../ref.wav\")))' --vary gain=0..2`",
42        )
43    })?;
44    let cost = cost(written)?;
45    let mut stops = Stops {
46        max_renders: DEFAULT_MAX_RENDERS,
47        step_tol: DEFAULT_STEP_TOL,
48        cost_tol: DEFAULT_COST_TOL,
49    };
50    let (mut vary_all, mut sets) = (Vec::new(), Vec::new());
51    let (mut rate, mut threads, mut cache) = (None, None, CacheAt::Platform);
52    let (mut write, mut confirm) = (false, false);
53    while let Some(flag) = it.next() {
54        match flag.as_str() {
55            "--vary" => vary_all.push(vary(&value(&mut it, "--vary")?)?),
56            "--set" => sets.push(sva_core::set(&value(&mut it, "--set")?)?),
57            "--max-renders" => {
58                let raw = value(&mut it, "--max-renders")?;
59                stops.max_renders = count(&raw, "--max-renders", "renders")?.get();
60            }
61            "--step-tol" => {
62                let raw = value(&mut it, "--step-tol")?;
63                let says = "a fraction of each variable's first step above 0 and at most 1";
64                stops.step_tol = number(&raw, "--step-tol", |v| v > 0.0 && v <= 1.0, says)?;
65            }
66            "--cost-tol" => {
67                let raw = value(&mut it, "--cost-tol")?;
68                let says = "a cost at or above 0";
69                stops.cost_tol = number(&raw, "--cost-tol", |v| v >= 0.0, says)?;
70            }
71            "--rate" => rate = Some(hertz(&value(&mut it, "--rate")?)?),
72            "--threads" => {
73                threads = Some(count(
74                    &value(&mut it, "--threads")?,
75                    "--threads",
76                    "threads",
77                )?);
78            }
79            "--cache" => {
80                cache = match value(&mut it, "--cache")?.as_str() {
81                    "none" => CacheAt::Off,
82                    path => CacheAt::Path(PathBuf::from(path)),
83                };
84            }
85            "--write" => write = true,
86            "--confirm" => confirm = true,
87            other => {
88                return Err(CliError::usage(
89                    "cli.unknown_argument",
90                    format!("unknown argument `{other}`"),
91                ));
92            }
93        }
94    }
95    if vary_all.is_empty() {
96        return Err(CliError::usage(
97            "solve.no_vary",
98            "solve needs at least one `--vary <name>=<lo>..<hi>`, a variable under \
99             `variables/` it fits",
100        ));
101    }
102    if confirm && !write {
103        return Err(CliError::usage(
104            "cli.confirm_without_write",
105            "`--confirm` lets `--write` replace a variable's file; write both, or neither",
106        ));
107    }
108    Ok(Command::Solve(Box::new(SolveArgs {
109        cost,
110        vary: vary_all,
111        stops,
112        sets,
113        rate,
114        threads,
115        cache,
116        write,
117        confirm,
118    })))
119}