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(
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
25fn 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
61pub(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
75fn 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
83pub 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}