1use 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
22fn 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
58pub(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
72pub 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
103fn 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
110pub 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}