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