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