Skip to main content

sva_cli/
render.rs

1// Concern: runs one render or one file analysis and routes every reading it answered | Non-concern: argv (args/), the JSON shape (sva-core) | IO: (RenderArgs or AnalyzeArgs) -> JSON or CliError
2
3use std::path::Path;
4
5use sva_core::{
6    Answer, Asked, CliError, Diagnostic, Job, Output, Printed, Report, SAMPLE_LIMIT, Store, cwd,
7    execute, execute_through, query_data,
8};
9use sva_engine::{
10    Buffer, DEFAULT_FRAME_SECS, PSYCHOACOUSTIC_V1, Representation, answer_buffer, cache_log,
11};
12
13use crate::args::{AnalyzeArgs, RenderArgs};
14use crate::destination::{Framing, refuse_inside, refuse_replacing, write, write_analysis};
15use crate::directory::Directory;
16use crate::store::wait;
17use crate::wav::{SampleEncoding, read_channels};
18use sva_core::success_envelope;
19
20/// Only the nodes the target reaches are read; its interval and `--until` decide its range.
21pub fn render(args: &RenderArgs) -> Result<String, CliError> {
22    let here = crate::composition::composition()?;
23    let (dir, target) = crate::composition::located(&here, &args.target)?;
24    // Judged before the first is opened: a refusal on the last leaves none of them on disk.
25    for asked in &args.asked {
26        if let Some(dest) = asked.dest.as_deref() {
27            refuse_inside(&dir, dest)?;
28            refuse_replacing(dest, args.confirm)?;
29        }
30    }
31    let mut warnings = Vec::new();
32    let store = crate::store::opened(&args.cache, &mut warnings)?;
33    let answered = answered(args, &dir, &target, store, &mut warnings);
34    if let Some(store) = store {
35        crate::store::persisted(store, &mut warnings);
36    }
37    answered.map(|data| success_envelope(&data, &warnings))
38}
39
40/// The render's `data`.
41fn answered(
42    args: &RenderArgs,
43    dir: &Path,
44    target: &str,
45    store: Option<&Store<Directory>>,
46    warnings: &mut Vec<Diagnostic>,
47) -> Result<String, CliError> {
48    let source = sva_ast::Dir::at(dir);
49    let logs = std::env::var("SVA_LOG").is_ok_and(|level| level == "debug");
50    let job = Job {
51        until: args.until.as_deref(),
52        rate: args.rate,
53        bits: args.bits,
54        asked: &args.asked,
55        flop_budget: args.flop_budget,
56        ..Job::over(&source, target)
57    };
58    let rendered = match store {
59        Some(store) => wait(execute_through(job, store))?,
60        None => execute(job)?,
61    };
62
63    let rate = rendered.config.rate;
64    let stats = rendered.render.cache_stats.as_ref();
65    if let Some(why) = stats.and_then(|stats| stats.unstaged.as_deref()) {
66        let message = format!("this render stopped staging what it computed: {why}");
67        warnings.push(crate::store::warning("store.unstaged", message));
68    }
69    if let (true, Some(stats)) = (logs, stats) {
70        eprint!("{}", cache_log(stats, rate));
71    }
72    let bits = rendered.config.profile.precision_bits;
73    let interval = rendered
74        .render
75        .range
76        .map(|r| (r.start_secs(rate), r.end as f64 / f64::from(rate)));
77    let framing = Framing {
78        target: args.target.clone(),
79        rate,
80        bits: Some(bits),
81        interval,
82        profile: rendered.config.profile.name,
83        encoding: SampleEncoding::of(bits),
84        replace: args.confirm,
85    };
86
87    let mut taken = Vec::with_capacity(args.asked.len());
88    for asked in &args.asked {
89        let node = asked.node.as_deref().unwrap_or(&rendered.target);
90        let answer = rendered.answer(node, asked.representation)?;
91        taken.push(match asked.brief {
92            true => briefed(answer),
93            false => answer,
94        });
95    }
96    let (answers, written) = routed(&args.asked, taken, &framing)?;
97
98    Ok(query_data(&Report {
99        target: &args.target,
100        rate,
101        bits: Some(bits),
102        interval,
103        profile: rendered.config.profile.name,
104        label: rendered.label(),
105        written: &written,
106        answers: &answers,
107        analyses: &[],
108        limit: Some(SAMPLE_LIMIT),
109    }))
110}
111
112/// What stays in the envelope, and what left it for a file.
113type Routed<'a> = (Vec<Printed>, Vec<(String, &'a Path)>);
114
115/// Every reading is taken before any file is opened, so a later refusal writes nothing.
116fn routed<'a>(
117    asked: &'a [Asked],
118    taken: Vec<Answer>,
119    framing: &Framing,
120) -> Result<Routed<'a>, CliError> {
121    let mut answers = Vec::new();
122    let mut written = Vec::new();
123    for (one, answer) in asked.iter().zip(taken) {
124        let printed = Printed {
125            name: one.name.clone(),
126            answer,
127            skim: one.skim,
128        };
129        match one.dest.as_deref() {
130            None => answers.push(printed),
131            Some(dest) => {
132                write(&printed, dest, framing)?;
133                written.push((one.name.clone(), dest));
134            }
135        }
136    }
137    Ok((answers, written))
138}
139
140/// `ledger(brief=1)`: a node that did not clip is not news, so a ledger keeps only the ones
141/// that did.
142fn briefed(answer: Answer) -> Answer {
143    let Output::Ledger(entries) = answer.value else {
144        return answer;
145    };
146    Answer {
147        value: Output::Ledger(
148            entries
149                .into_iter()
150                .filter(|e| e.clipped == Some(true))
151                .collect(),
152        ),
153        ..answer
154    }
155}
156
157/// The same routing over a decoded external WAV instead of a rendered graph: the file's own
158/// rate stands, and it is read whole.
159pub fn analyze(args: &AnalyzeArgs) -> Result<String, CliError> {
160    let dir = cwd()?;
161    let composition = is_composition(&dir);
162    let destinations = args
163        .asked
164        .iter()
165        .map(|a| a.dest.as_deref())
166        .chain(args.analyses.iter().map(|a| a.dest.as_deref()));
167    for dest in destinations {
168        let Some(dest) = dest else {
169            continue;
170        };
171        if composition {
172            refuse_inside(&dir, dest)?;
173        }
174        refuse_replacing(dest, args.confirm)?;
175    }
176    let (planes, rate) = read_channels(&args.path)?;
177    let target = args.path.display().to_string();
178    let held = Buffer::of_planes(
179        rate,
180        planes
181            .iter()
182            .map(|p| p.iter().map(|v| f64::from(*v)).collect())
183            .collect(),
184    );
185    crate::args::check_frame(&args.asked, rate)?;
186    let buffer = held;
187    let interval = Some((0.0, buffer.len() as f64 / f64::from(rate)));
188    let framing = Framing {
189        target: target.clone(),
190        rate,
191        bits: None,
192        interval,
193        profile: PSYCHOACOUSTIC_V1.name,
194        encoding: SampleEncoding::Float,
195        replace: args.confirm,
196    };
197
198    let mut taken = Vec::with_capacity(args.asked.len());
199    for asked in &args.asked {
200        taken.push(
201            answer_buffer(
202                &target,
203                &buffer,
204                asked.representation,
205                PSYCHOACOUSTIC_V1.name,
206            )
207            .map_err(CliError::Engine)?,
208        );
209    }
210    let heard = analysed(args, &buffer)?;
211    let (answers, mut written) = routed(&args.asked, taken, &framing)?;
212    let mut analyses = Vec::new();
213    for (analysis, value) in args.analyses.iter().zip(heard) {
214        match analysis.dest.as_deref() {
215            None => analyses.push((analysis.name.clone(), value)),
216            Some(dest) => {
217                write_analysis(&analysis.name, &value, dest, &framing)?;
218                written.push((analysis.name.clone(), dest));
219            }
220        }
221    }
222    Ok(success_envelope(
223        &query_data(&Report {
224            target: &target,
225            rate,
226            bits: None,
227            interval,
228            profile: PSYCHOACOUSTIC_V1.name,
229            label: None,
230            written: &written,
231            answers: &answers,
232            analyses: &analyses,
233            limit: Some(SAMPLE_LIMIT),
234        }),
235        &[],
236    ))
237}
238
239/// The readings `sva-analysis` answers off a buffer alone. Every field its `Request` names is
240/// taken from this one file: a `.wav` carries no tempo and no node behind it.
241fn analysed(args: &AnalyzeArgs, buffer: &Buffer) -> Result<Vec<String>, CliError> {
242    if args.analyses.is_empty() {
243        return Ok(Vec::new());
244    }
245    let rate = f64::from(buffer.rate);
246    let mono: Vec<f32> = buffer.plane(0).iter().map(|s| *s as f32).collect();
247    let against = match args.analyses.iter().find_map(|a| a.against.as_deref()) {
248        Some(path) => {
249            let (planes, _) = read_channels(path)?;
250            Some(planes.first().cloned().unwrap_or_default())
251        }
252        None => None,
253    };
254    let name = args.path.display().to_string();
255    let read = |representation| {
256        answer_buffer(&name, buffer, representation, PSYCHOACOUSTIC_V1.name).map(|a| a.value)
257    };
258    let envelope = match read(Representation::Envelope { frame_secs: None }) {
259        Ok(Output::Envelope(frames)) => frames,
260        _ => Vec::new(),
261    };
262    let image = match read(Representation::Stereo {
263        frame_secs: DEFAULT_FRAME_SECS,
264    }) {
265        Ok(Output::Stereo(found)) => Some(*found),
266        _ => None,
267    };
268    let request = sva_analysis::Request {
269        samples: &mono,
270        sample_rate: rate,
271        start_secs: 0.0,
272        frame_secs: DEFAULT_FRAME_SECS,
273        tempo: None,
274        envelope: &envelope,
275        stereo: image.as_ref(),
276        against: against.as_deref(),
277        gated: false,
278        input_envelope: None,
279    };
280    let mut out = Vec::with_capacity(args.analyses.len());
281    for analysis in &args.analyses {
282        out.push(
283            sva_analysis::run(&analysis.name, &request)
284                .map_err(|sva_analysis::AnalysisError(why)| CliError::Usage(why))?,
285        );
286    }
287    Ok(out)
288}
289
290/// Whether the next parse would walk this directory, not whether it parses cleanly today.
291fn is_composition(dir: &Path) -> bool {
292    dir.join(sva_core::ROOT).is_file() || dir.join(sva_ast::VARIABLES).is_dir()
293}