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