use std::num::NonZeroUsize;
use std::path::{Component, Path, PathBuf};
use sva_core::{Asked, Call, CliError, Destination, asked_all, calls, is_wav, wav_path};
use sva_engine::{DEFAULT_SAMPLE_RATE, MAX_PINNED_FRAME, READINGS, Representation, pinned_frame};
use super::{AnalyzeArgs, CacheAt, Command, RenderArgs, value};
#[derive(Default)]
struct Flags {
calls: Vec<Call>,
confirm: bool,
}
impl Flags {
fn read<'a>(
&mut self,
flag: &str,
it: &mut impl Iterator<Item = &'a String>,
) -> Result<bool, CliError> {
match flag {
"--representation" => {
for call in calls(&value(it, "--representation")?)? {
refuse_audio_dest(&call.name, call.dest.as_deref())?;
self.calls.push(call);
}
}
"--confirm" => self.confirm = true,
_ => return Ok(false),
}
Ok(true)
}
fn resolved(&self, off_file: bool) -> Result<Vec<Asked>, CliError> {
let out = asked_all(&self.calls)?;
for one in &out {
if let Some(Destination::Alsa { device, .. }) = &one.dest {
refuse_device(&one.name, device, off_file)?;
}
}
if let Some(one) = out
.iter()
.find(|one| off_file && !one.representation.off_samples(true))
{
let answered: Vec<&str> = READINGS
.iter()
.filter(|row| row.reads.off_samples(true))
.map(|row| row.name)
.collect();
return Err(CliError::usage(
"query.not_analyzable",
format!(
"`analyze` does not answer `{}` — only {} need no rendered graph",
one.name,
answered.join(", ")
),
));
}
Ok(out)
}
}
fn positional(arg: Option<&String>) -> Option<String> {
arg.filter(|a| !a.starts_with("--")).cloned()
}
pub(super) fn render_args(rest: &[String]) -> Result<Command, CliError> {
let mut it = rest.iter();
let target = positional(it.next()).ok_or_else(|| {
CliError::usage(
"cli.missing_argument",
"render needs a target expression, as `sva-cli render '@song([0, inf))'`",
)
})?;
let mut flags = Flags::default();
let (mut until, mut rate, mut bits, mut threads) = (None, None, None, None);
let mut cache = CacheAt::Platform;
while let Some(flag) = it.next() {
if flags.read(flag, &mut it)? {
continue;
}
match flag.as_str() {
"--until" => until = Some(value(&mut it, "--until")?),
"--rate" => rate = Some(hertz(&value(&mut it, "--rate")?)?),
"--bits" => bits = Some(whole(&value(&mut it, "--bits")?, "--bits")?),
"--threads" => threads = Some(count(&value(&mut it, "--threads")?)?),
"--cache" => {
cache = match value(&mut it, "--cache")?.as_str() {
"none" => CacheAt::Off,
path => CacheAt::Path(PathBuf::from(path)),
};
}
other => {
return Err(CliError::usage(
"cli.unknown_argument",
format!("unknown argument `{other}`"),
));
}
}
}
let asked = flags.resolved(false)?;
refuse_shared_destination(asked.iter().filter_map(Asked::path))?;
if asked.is_empty() {
return Err(CliError::usage(
"cli.missing_argument",
"render needs at least one `--representation <r>`; `--representation \
samples=<path>.wav` writes audio",
));
}
check_frame(&asked, rate.unwrap_or(DEFAULT_SAMPLE_RATE))?;
Ok(Command::Render(Box::new(RenderArgs {
target,
until,
rate,
bits,
threads,
asked,
confirm: flags.confirm,
cache,
})))
}
pub(super) fn analyze_args(rest: &[String]) -> Result<Command, CliError> {
let mut it = rest.iter();
let path = positional(it.next())
.ok_or_else(|| CliError::usage("cli.missing_argument", "missing <file.wav>"))?;
let path = wav_path(&path)?;
let mut flags = Flags::default();
while let Some(flag) = it.next() {
if flags.read(flag, &mut it)? {
continue;
}
return Err(CliError::usage(
"cli.unknown_argument",
format!("unknown argument `{flag}`"),
));
}
let asked = flags.resolved(true)?;
refuse_shared_destination(asked.iter().filter_map(Asked::path))?;
if asked.is_empty() {
return Err(CliError::usage(
"cli.missing_argument",
"`analyze` needs at least one `--representation <r>`",
));
}
Ok(Command::Analyze(Box::new(AnalyzeArgs {
path,
asked,
confirm: flags.confirm,
})))
}
fn refuse_shared_destination<'a>(dests: impl Iterator<Item = &'a Path>) -> Result<(), CliError> {
let mut taken: Vec<PathBuf> = Vec::new();
for dest in dests {
let held = lexical(dest);
if taken.contains(&held) {
return Err(CliError::usage(
"query.shared_destination",
format!(
"more than one reading names `{}`; each would overwrite the last, so give \
every one its own path",
dest.display()
),
));
}
taken.push(held);
}
Ok(())
}
fn lexical(path: &Path) -> PathBuf {
let mut out = PathBuf::new();
for part in path.components() {
match part {
Component::CurDir => {}
Component::ParentDir
if matches!(out.components().next_back(), Some(Component::Normal(_))) =>
{
out.pop();
}
other => out.push(other),
}
}
out
}
fn hertz(raw: &str) -> Result<u32, CliError> {
match raw.parse::<u32>() {
Ok(hz) if hz > 0 => Ok(hz),
_ => Err(CliError::usage(
"cli.bad_value",
format!("--rate needs a whole number of hertz above zero, got `{raw}`"),
)),
}
}
fn count(raw: &str) -> Result<NonZeroUsize, CliError> {
raw.parse::<NonZeroUsize>().map_err(|_| {
CliError::usage(
"cli.bad_value",
format!("--threads needs a whole number of threads above zero, got `{raw}`"),
)
})
}
pub(super) fn whole(raw: &str, flag: &str) -> Result<i32, CliError> {
raw.parse::<i32>().map_err(|_| {
CliError::usage(
"cli.bad_value",
format!("{flag} needs a whole number, got `{raw}`"),
)
})
}
fn refuse_audio_dest(name: &str, dest: Option<&Path>) -> Result<(), CliError> {
if dest.is_some_and(is_wav) && name != "samples" {
return Err(CliError::usage(
"destination.not_audio",
format!(
"a `.wav` destination carries audio, and `{name}` is not audio; write it to a path \
with any other extension for JSON"
),
));
}
Ok(())
}
fn refuse_device(name: &str, device: &str, off_file: bool) -> Result<(), CliError> {
if name != "samples" {
return Err(CliError::usage(
"destination.not_audio",
format!(
"`alsa:{device}` plays audio, and `{name}` is not audio; write \
`samples=alsa:{device}`, and `{name}` to stdout or a path"
),
));
}
if off_file {
return Err(CliError::usage(
"destination.needs_render",
format!(
"`alsa:{device}` plays a render as it is pulled, and `analyze` renders \
nothing; play a composition with `sva-cli render`"
),
));
}
crate::device::refuse_unsupported(device)
}
pub(crate) fn check_frame(asked: &[Asked], rate: u32) -> Result<(), CliError> {
let sr = f64::from(rate);
for one in asked {
let Representation::Spectrum {
frame_secs: Some(secs),
..
} = one.representation
else {
continue;
};
let frame = pinned_frame(secs, sr);
if frame > MAX_PINNED_FRAME {
return Err(CliError::usage(
"query.bad_value",
format!(
"`spectrum(frame={secs})` sizes its transform to {frame} samples at {sr} Hz, \
past the {MAX_PINNED_FRAME}-sample bound"
),
));
}
}
Ok(())
}