use std::path::Path;
use sva_core::{CliError, Output, Printed, Report, encode, is_wav, query_data};
use crate::wav::{SampleEncoding, write_channels};
use sva_core::success_envelope;
pub fn refuse_inside(dir: &Path, dest: &Path) -> Result<(), CliError> {
if !under(dir, dest) {
return Ok(());
}
Err(CliError::Usage(format!(
"{} is inside the composition at {}, where it would be read as a node on the next \
parse; write it outside the composition",
dest.display(),
dir.display()
)))
}
fn under(dir: &Path, dest: &Path) -> bool {
let Ok(root) = dir.canonicalize() else {
return true;
};
if let Ok(real) = dest.canonicalize() {
return real.starts_with(&root);
}
let parent = match dest.parent() {
Some(p) if !p.as_os_str().is_empty() => p.to_path_buf(),
_ => Path::new(".").to_path_buf(),
};
let Ok(mut at) = std::path::absolute(parent) else {
return true;
};
loop {
if let Ok(real) = at.canonicalize() {
return real.starts_with(&root);
}
if !at.pop() {
return true;
}
}
}
pub struct Framing {
pub target: String,
pub rate: u32,
pub bits: Option<i32>,
pub interval: Option<(f64, f64)>,
pub profile: &'static str,
pub encoding: SampleEncoding,
pub replace: bool,
}
pub(crate) fn refuse_replacing(dest: &Path, replace: bool) -> Result<(), CliError> {
match !replace && dest.exists() {
true => Err(CliError::Conflict {
by: "destination",
message: format!(
"{} already holds a file; pass `--confirm` to replace it",
dest.display()
),
}),
false => Ok(()),
}
}
pub fn write_analysis(
name: &str,
value: &str,
dest: &Path,
framing: &Framing,
) -> Result<(), CliError> {
if is_wav(dest) {
return Err(not_audio(name));
}
refuse_replacing(dest, framing.replace)?;
let analyses = [(name.to_string(), value.to_string())];
let json = success_envelope(
&query_data(&Report {
target: &framing.target,
rate: framing.rate,
bits: None,
interval: framing.interval,
profile: framing.profile,
label: None,
written: &[],
answers: &[],
analyses: &analyses,
limit: None,
}),
&[],
);
std::fs::write(dest, json + "\n")
.map_err(|e| CliError::Io(format!("could not write {}: {e}", dest.display())))
}
fn not_audio(name: &str) -> CliError {
CliError::Usage(format!(
"`{name}` is not audio, so it has no samples to write to a `.wav` path"
))
}
pub fn write(printed: &Printed, dest: &Path, framing: &Framing) -> Result<(), CliError> {
refuse_replacing(dest, framing.replace)?;
if is_wav(dest) {
let Output::Samples(buffer) = &printed.answer.value else {
return Err(not_audio(&printed.name));
};
let node = &framing.target;
let held: Vec<Vec<f32>> = (0..buffer.width)
.map(|c| encode::float32(node, buffer.plane(c)))
.collect::<Result<_, _>>()?;
if let SampleEncoding::Pcm(_) = framing.encoding {
held.iter()
.try_for_each(|plane| encode::full_scale(node, plane))?;
}
let planes: Vec<&[f32]> = held.iter().map(Vec::as_slice).collect();
return write_channels(&planes, buffer.rate, dest, framing.encoding);
}
let json = success_envelope(
&query_data(&Report {
target: &framing.target,
rate: framing.rate,
bits: framing.bits,
interval: framing.interval,
profile: framing.profile,
label: None,
written: &[],
answers: std::slice::from_ref(printed),
analyses: &[],
limit: None,
}),
&[],
);
std::fs::write(dest, json + "\n")
.map_err(|e| CliError::Io(format!("could not write {}: {e}", dest.display())))
}