Skip to main content

sva_cli/
destination.rs

1// Concern: writes one reading to the path it named | Non-concern: picking that path (args/), the WAV byte layout (wav.rs) | IO: (Answer, path) -> a written file or CliError
2
3use std::path::Path;
4
5use sva_core::{CliError, Output, Printed, Report, encode, is_wav, query_data};
6
7use crate::wav::{SampleEncoding, write_channels};
8use sva_core::success_envelope;
9
10pub fn refuse_inside(dir: &Path, dest: &Path) -> Result<(), CliError> {
11    if !under(dir, dest) {
12        return Ok(());
13    }
14    Err(CliError::usage(
15        "destination.inside_composition",
16        format!(
17            "{} is inside the composition at {}, where it would be read as a node on the next \
18         parse; write it outside the composition",
19            dest.display(),
20            dir.display()
21        ),
22    ))
23}
24
25/// A link at the destination is where the write lands; a path not there yet takes the
26/// nearest ancestor that is, and a root that will not resolve proves nothing either way.
27fn under(dir: &Path, dest: &Path) -> bool {
28    let Ok(root) = dir.canonicalize() else {
29        return true;
30    };
31    if let Ok(real) = dest.canonicalize() {
32        return real.starts_with(&root);
33    }
34    let parent = match dest.parent() {
35        Some(p) if !p.as_os_str().is_empty() => p.to_path_buf(),
36        _ => Path::new(".").to_path_buf(),
37    };
38    let Ok(mut at) = std::path::absolute(parent) else {
39        return true;
40    };
41    loop {
42        if let Ok(real) = at.canonicalize() {
43            return real.starts_with(&root);
44        }
45        if !at.pop() {
46            return true;
47        }
48    }
49}
50
51pub struct Framing {
52    pub target: String,
53    pub rate: u32,
54    pub bits: Option<i32>,
55    pub interval: Option<(f64, f64)>,
56    pub profile: &'static str,
57    pub encoding: SampleEncoding,
58    pub replace: bool,
59}
60
61/// A written file does not come back, so `--confirm` is what replaces one.
62pub(crate) fn refuse_replacing(dest: &Path, replace: bool) -> Result<(), CliError> {
63    match !replace && dest.exists() {
64        true => Err(CliError::Conflict {
65            by: "destination",
66            message: format!(
67                "{} already holds a file; pass `--confirm` to replace it",
68                dest.display()
69            ),
70        }),
71        false => Ok(()),
72    }
73}
74
75/// Argv refuses this pair at parse time; a library caller passed none, so it is refused here.
76fn not_audio(name: &str) -> CliError {
77    CliError::usage(
78        "destination.not_audio",
79        format!("`{name}` is not audio, so it has no samples to write to a `.wav` path"),
80    )
81}
82
83/// A `.wav` path takes the samples; any other takes the JSON stdout caps.
84pub fn write(printed: &Printed, dest: &Path, framing: &Framing) -> Result<(), CliError> {
85    refuse_replacing(dest, framing.replace)?;
86    if is_wav(dest) {
87        let Output::Samples(buffer) = &printed.answer.value else {
88            return Err(not_audio(&printed.name));
89        };
90        let node = &framing.target;
91        let held: Vec<Vec<f32>> = (0..buffer.width())
92            .map(|c| encode::float32(node, buffer.plane(c)))
93            .collect::<Result<_, _>>()?;
94        if let SampleEncoding::Pcm(_) = framing.encoding {
95            held.iter()
96                .try_for_each(|plane| encode::full_scale(node, plane))?;
97        }
98        let planes: Vec<&[f32]> = held.iter().map(Vec::as_slice).collect();
99        return write_channels(&planes, buffer.rate, dest, framing.encoding);
100    }
101    let json = success_envelope(
102        &query_data(&Report {
103            target: &framing.target,
104            rate: framing.rate,
105            bits: framing.bits,
106            interval: framing.interval,
107            profile: framing.profile,
108            label: None,
109            written: &[],
110            played: None,
111            answers: std::slice::from_ref(printed),
112            limit: None,
113        }),
114        &[],
115    );
116    std::fs::write(dest, json + "\n")
117        .map_err(|e| CliError::Io(format!("could not write {}: {e}", dest.display())))
118}