1use std::path::Path;
4
5use sva_core::{
6 Answer, Asked, CacheReport, CliError, Horizon, Job, Output, Report, SAMPLE_LIMIT, Slots, cwd,
7 execute, query_data, window_for,
8};
9use sva_engine::{
10 Buffer, Cache, DEFAULT_FRAME_SECS, DiskCache, PSYCHOACOUSTIC_V1, Representation, answer_buffer,
11};
12
13use crate::args::{AnalyzeArgs, RenderArgs};
14use crate::destination::{Framing, refuse_inside, refuse_replacing, write, write_analysis};
15use crate::wav::{SampleEncoding, read_channels};
16use sva_core::success_envelope;
17
18pub fn render(args: &RenderArgs) -> Result<String, CliError> {
21 let dir = crate::composition::composition(args.dir.as_deref())?;
22 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 store = args.cache.then(DiskCache::discover).flatten();
30 let cache = store.as_ref().map(|c| c as &dyn Cache);
31 let slots = Slots::default();
32 let source = sva_ast::Dir::at(&dir);
33 let rendered = execute(Job {
34 target: args.target.as_deref(),
35 from: args.from,
36 until: args.to,
37 silent: args.silent,
38 sample_rate: args.sample_rate,
39 cache,
40 reading: args.node.as_deref(),
41 representations: args.asked.iter().map(|a| a.representation).collect(),
42 reaching: args.target.is_some(),
44 flop_budget: args.flop_budget,
45 volatile: &args.volatile,
46 slots: Some(&slots),
47 ..Job::over(&source)
48 })?;
49
50 let node = args.node.clone().unwrap_or_else(|| rendered.target.clone());
51 let framing = Framing {
52 target: rendered.target.clone(),
53 rate: rendered.config.rate,
54 horizon: rendered.config.horizon,
55 profile: rendered.config.profile.name,
56 encoding: match args.pcm16 {
57 true => SampleEncoding::Pcm16,
58 false => SampleEncoding::Float,
59 },
60 skim: args.skim,
61 replace: args.confirm,
62 };
63
64 let mut taken = Vec::with_capacity(args.asked.len());
65 for asked in &args.asked {
66 let answer = rendered.answer(&node, asked.representation)?;
67 taken.push(match args.brief {
68 true => briefed(answer),
69 false => answer,
70 });
71 }
72 let (answers, written) = routed(&args.asked, taken, &framing)?;
73
74 let report = cache.map(|c| CacheReport {
75 dir: c
76 .dir()
77 .map_or_else(|| "memory".to_string(), |d| d.display().to_string()),
78 stats: rendered
79 .render
80 .cache_stats
81 .clone()
82 .expect("a render handed a store records every lookup it made"),
83 held_bytes: c.held_bytes(),
84 max_bytes: c.max_bytes(),
85 evicted_bytes: c.evicted_bytes(),
86 faults: c.faults(),
87 });
88 Ok(success_envelope(
89 &query_data(&Report {
90 target: &rendered.target,
91 rate: rendered.config.rate,
92 horizon: rendered.config.horizon,
93 profile: rendered.config.profile.name,
94 label: rendered.label(),
95 written: &written,
96 cache: report.as_ref(),
97 answers: &answers,
98 analyses: &[],
99 limit: Some(SAMPLE_LIMIT),
100 skim: args.skim,
101 }),
102 &[],
103 ))
104}
105
106type Routed<'a> = (Vec<(String, Answer)>, Vec<(String, &'a Path)>);
108
109fn routed<'a>(
111 asked: &'a [Asked],
112 taken: Vec<Answer>,
113 framing: &Framing,
114) -> Result<Routed<'a>, CliError> {
115 let mut answers = Vec::new();
116 let mut written = Vec::new();
117 for (one, answer) in asked.iter().zip(taken) {
118 match one.dest.as_deref() {
119 None => answers.push((one.name.clone(), answer)),
120 Some(dest) => {
121 write(&one.name, &answer, dest, framing)?;
122 written.push((one.name.clone(), dest));
123 }
124 }
125 }
126 Ok((answers, written))
127}
128
129fn briefed(answer: Answer) -> Answer {
131 let Output::Ledger(entries) = answer.value else {
132 return answer;
133 };
134 Answer {
135 value: Output::Ledger(
136 entries
137 .into_iter()
138 .filter(|e| e.clipped == Some(true))
139 .collect(),
140 ),
141 ..answer
142 }
143}
144
145pub fn analyze(args: &AnalyzeArgs) -> Result<String, CliError> {
148 let dir = cwd()?;
149 let composition = is_composition(&dir);
150 let destinations = args
151 .asked
152 .iter()
153 .map(|a| a.dest.as_deref())
154 .chain(args.analyses.iter().map(|(_, dest)| dest.as_deref()));
155 for dest in destinations {
156 let Some(dest) = dest else {
157 continue;
158 };
159 if composition {
160 refuse_inside(&dir, dest)?;
161 }
162 refuse_replacing(dest, args.confirm)?;
163 }
164 let (planes, rate) = read_channels(&args.path)?;
165 let target = args.path.display().to_string();
166 let held = Buffer::of_planes(
167 rate,
168 planes
169 .iter()
170 .map(|p| p.iter().map(|v| f64::from(*v)).collect())
171 .collect(),
172 );
173 let asked_window = window_for(args.from, args.to, None)?;
174 let duration = held.len() as f64 / f64::from(rate);
175 let horizon = Horizon::secs(
176 asked_window.start_secs,
177 match asked_window.end_secs.is_finite() {
178 true => asked_window.end_secs,
179 false => duration,
180 },
181 );
182 if horizon.span() <= 0.0 {
183 return Err(CliError::Usage(format!(
184 "a reading runs from {}s to {}s, which is no window; give --to past --from",
185 horizon.start_secs, horizon.end_secs
186 )));
187 }
188 if horizon.start_secs >= duration || horizon.end_secs > duration {
190 return Err(CliError::Usage(format!(
191 "{} is {duration}s long, and the window asked for runs to {}s",
192 args.path.display(),
193 horizon.end_secs
194 )));
195 }
196 crate::args::check_frame(&args.asked, rate)?;
197 let buffer = sliced(&held, horizon);
198 let framing = Framing {
199 target: target.clone(),
200 rate,
201 horizon,
202 profile: PSYCHOACOUSTIC_V1.name,
203 encoding: SampleEncoding::Float,
204 skim: false,
205 replace: args.confirm,
206 };
207
208 let mut taken = Vec::with_capacity(args.asked.len());
209 for asked in &args.asked {
210 taken.push(
211 answer_buffer(
212 &target,
213 &buffer,
214 asked.representation,
215 PSYCHOACOUSTIC_V1.name,
216 )
217 .map_err(CliError::Engine)?,
218 );
219 }
220 let heard = analysed(args, &buffer, horizon)?;
221 let (answers, mut written) = routed(&args.asked, taken, &framing)?;
222 let mut analyses = Vec::new();
223 for ((name, dest), value) in args.analyses.iter().zip(heard) {
224 match dest.as_deref() {
225 None => analyses.push((name.clone(), value)),
226 Some(dest) => {
227 write_analysis(name, &value, dest, &framing)?;
228 written.push((name.clone(), dest));
229 }
230 }
231 }
232 Ok(success_envelope(
233 &query_data(&Report {
234 target: &target,
235 rate,
236 horizon,
237 profile: PSYCHOACOUSTIC_V1.name,
238 label: None,
239 written: &written,
240 cache: None,
241 answers: &answers,
242 analyses: &analyses,
243 limit: Some(SAMPLE_LIMIT),
244 skim: false,
245 }),
246 &[],
247 ))
248}
249
250fn analysed(
253 args: &AnalyzeArgs,
254 buffer: &Buffer,
255 horizon: Horizon,
256) -> Result<Vec<String>, CliError> {
257 if args.analyses.is_empty() {
258 return Ok(Vec::new());
259 }
260 let rate = f64::from(buffer.rate);
261 let mono: Vec<f32> = buffer.plane(0).iter().map(|s| *s as f32).collect();
262 let against = match &args.against {
263 Some(path) => {
264 let (planes, _) = read_channels(path)?;
265 Some(planes.first().cloned().unwrap_or_default())
266 }
267 None => None,
268 };
269 let name = args.path.display().to_string();
270 let read = |representation| {
271 answer_buffer(&name, buffer, representation, PSYCHOACOUSTIC_V1.name).map(|a| a.value)
272 };
273 let envelope = match read(Representation::Envelope { frame_secs: None }) {
274 Ok(Output::Envelope(frames)) => frames,
275 _ => Vec::new(),
276 };
277 let image = match read(Representation::Stereo {
278 frame_secs: DEFAULT_FRAME_SECS,
279 }) {
280 Ok(Output::Stereo(found)) => Some(*found),
281 _ => None,
282 };
283 let request = sva_analysis::Request {
284 samples: &mono,
285 sample_rate: rate,
286 start_secs: horizon.start_secs,
287 frame_secs: DEFAULT_FRAME_SECS,
288 tempo: None,
289 envelope: &envelope,
290 stereo: image.as_ref(),
291 against: against.as_deref(),
292 gated: false,
293 input_envelope: None,
294 };
295 let mut out = Vec::with_capacity(args.analyses.len());
296 for (name, _) in &args.analyses {
297 out.push(
298 sva_analysis::run(name, &request)
299 .map_err(|sva_analysis::AnalysisError(why)| CliError::Usage(why))?,
300 );
301 }
302 Ok(out)
303}
304
305fn is_composition(dir: &Path) -> bool {
307 dir.join(sva_core::ROOT).is_file() || dir.join(sva_ast::VARIABLES).is_dir()
308}
309
310fn sliced(buffer: &Buffer, horizon: Horizon) -> Buffer {
312 let rate = f64::from(buffer.rate);
313 let start = ((horizon.start_secs * rate).round().max(0.0) as usize).min(buffer.len());
314 let end = ((horizon.end_secs * rate).round().max(0.0) as usize).clamp(start, buffer.len());
315 let mut out = Buffer::of_planes(
316 buffer.rate,
317 (0..buffer.width)
318 .map(|c| buffer.plane(c)[start..end].to_vec())
319 .collect(),
320 );
321 out.origin_secs = horizon.start_secs;
322 out
323}