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, Session, Tier,
7    cwd, execute_over, query_data,
8};
9use sva_engine::{Buffer, PSYCHOACOUSTIC_V1, answer_buffer, cache_log};
10
11use crate::args::{AnalyzeArgs, RenderArgs};
12use crate::destination::{Framing, refuse_inside, refuse_replacing, write};
13use crate::directory::Directory;
14use crate::store::wait;
15use crate::wav::{SampleEncoding, read_channels};
16use sva_core::success_envelope;
17
18/// Only the nodes the target reaches are read; its interval and `--until` decide its range.
19pub fn render(args: &RenderArgs) -> Result<String, CliError> {
20    let here = crate::composition::composition()?;
21    let (dir, target) = crate::composition::located(&here, &args.target)?;
22    // Judged before the first is opened: a refusal on the last leaves none of them on disk.
23    for asked in &args.asked {
24        if let Some(dest) = asked.dest.as_deref() {
25            refuse_inside(&dir, dest)?;
26            refuse_replacing(dest, args.confirm)?;
27        }
28    }
29    let mut warnings = Vec::new();
30    let tier = crate::store::opened(&args.cache, &mut warnings)?;
31    let answered = answered(args, &dir, &target, tier, &mut warnings);
32    crate::store::persisted(tier, &mut warnings);
33    answered.map(|data| success_envelope(&data, &warnings))
34}
35
36/// The render's `data`.
37fn answered(
38    args: &RenderArgs,
39    dir: &Path,
40    target: &str,
41    tier: &Tier<Directory>,
42    warnings: &mut Vec<Diagnostic>,
43) -> Result<String, CliError> {
44    let source = sva_ast::Dir::at(dir);
45    let logs = std::env::var("SVA_LOG").is_ok_and(|level| level == "debug");
46    let job = Job {
47        until: args.until.as_deref(),
48        rate: args.rate,
49        bits: args.bits,
50        asked: &args.asked,
51        flop_budget: args.flop_budget,
52        ..Job::over(&source, target)
53    };
54    let rendered = wait(execute_over(job, tier, &mut Session::default()))?;
55
56    let rate = rendered.config.rate;
57    let stats = rendered.render.cache_stats.as_ref();
58    if let Some(why) = stats.and_then(|stats| stats.unstaged.as_deref()) {
59        let message = format!("this render stopped writing what it computed: {why}");
60        warnings.push(crate::store::warning("store.unstaged", message));
61    }
62    if let (true, Some(stats)) = (logs, stats) {
63        eprint!("{}", cache_log(stats, rate));
64    }
65    let bits = rendered.config.profile.precision_bits;
66    let interval = rendered
67        .render
68        .range
69        .map(|r| (r.start_secs(rate), r.end as f64 / f64::from(rate)));
70    let framing = Framing {
71        target: args.target.clone(),
72        rate,
73        bits: Some(bits),
74        interval,
75        profile: rendered.config.profile.name,
76        encoding: SampleEncoding::of(bits),
77        replace: args.confirm,
78    };
79
80    let mut taken = Vec::with_capacity(args.asked.len());
81    for asked in &args.asked {
82        let node = asked.node.as_deref().unwrap_or(&rendered.target);
83        let answer = rendered.answer(node, asked.representation)?;
84        taken.push(match asked.brief {
85            true => briefed(answer),
86            false => answer,
87        });
88    }
89    let (answers, written) = routed(&args.asked, taken, &framing)?;
90
91    Ok(query_data(&Report {
92        target: &args.target,
93        rate,
94        bits: Some(bits),
95        interval,
96        profile: rendered.config.profile.name,
97        label: rendered.label(),
98        written: &written,
99        answers: &answers,
100        limit: Some(SAMPLE_LIMIT),
101    }))
102}
103
104/// What stays in the envelope, and what left it for a file.
105type Routed<'a> = (Vec<Printed>, Vec<(String, &'a Path)>);
106
107/// Every reading is taken before any file is opened, so a later refusal writes nothing.
108fn routed<'a>(
109    asked: &'a [Asked],
110    taken: Vec<Answer>,
111    framing: &Framing,
112) -> Result<Routed<'a>, CliError> {
113    let mut answers = Vec::new();
114    let mut written = Vec::new();
115    for (one, answer) in asked.iter().zip(taken) {
116        let printed = Printed {
117            name: one.name.clone(),
118            answer,
119            skim: one.skim,
120        };
121        match one.dest.as_deref() {
122            None => answers.push(printed),
123            Some(dest) => {
124                write(&printed, dest, framing)?;
125                written.push((one.name.clone(), dest));
126            }
127        }
128    }
129    Ok((answers, written))
130}
131
132/// `ledger(brief=1)`: a node that did not clip is not news, so a ledger keeps only the ones
133/// that did.
134fn briefed(answer: Answer) -> Answer {
135    let Output::Ledger(entries) = answer.value else {
136        return answer;
137    };
138    Answer {
139        value: Output::Ledger(
140            entries
141                .into_iter()
142                .filter(|e| e.clipped == Some(true))
143                .collect(),
144        ),
145        ..answer
146    }
147}
148
149/// The same routing over a decoded external WAV instead of a rendered graph: the file's own
150/// rate stands, and it is read whole.
151pub fn analyze(args: &AnalyzeArgs) -> Result<String, CliError> {
152    let dir = cwd()?;
153    let composition = is_composition(&dir);
154    for dest in args.asked.iter().filter_map(|a| a.dest.as_deref()) {
155        if composition {
156            refuse_inside(&dir, dest)?;
157        }
158        refuse_replacing(dest, args.confirm)?;
159    }
160    let (planes, rate) = read_channels(&args.path)?;
161    let target = args.path.display().to_string();
162    let held = Buffer::of_planes(
163        rate,
164        planes
165            .iter()
166            .map(|p| p.iter().map(|v| f64::from(*v)).collect())
167            .collect(),
168    );
169    crate::args::check_frame(&args.asked, rate)?;
170    let buffer = held;
171    let interval = Some((0.0, buffer.len() as f64 / f64::from(rate)));
172    let framing = Framing {
173        target: target.clone(),
174        rate,
175        bits: None,
176        interval,
177        profile: PSYCHOACOUSTIC_V1.name,
178        encoding: SampleEncoding::Float,
179        replace: args.confirm,
180    };
181
182    let mut taken = Vec::with_capacity(args.asked.len());
183    for asked in &args.asked {
184        taken.push(
185            answer_buffer(
186                &target,
187                &buffer,
188                asked.representation,
189                PSYCHOACOUSTIC_V1.name,
190            )
191            .map_err(CliError::Engine)?,
192        );
193    }
194    let (answers, written) = routed(&args.asked, taken, &framing)?;
195    Ok(success_envelope(
196        &query_data(&Report {
197            target: &target,
198            rate,
199            bits: None,
200            interval,
201            profile: PSYCHOACOUSTIC_V1.name,
202            label: None,
203            written: &written,
204            answers: &answers,
205            limit: Some(SAMPLE_LIMIT),
206        }),
207        &[],
208    ))
209}
210
211/// Whether the next parse would walk this directory, not whether it parses cleanly today.
212fn is_composition(dir: &Path) -> bool {
213    dir.join(sva_core::ROOT).is_file() || dir.join(sva_ast::VARIABLES).is_dir()
214}