1use std::path::Path;
4
5use sva_core::{
6 Answer, Asked, CliError, Destination, Diagnostic, Hear, Job, Output, Printed, Report,
7 SAMPLE_LIMIT, Session, Tier, 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::device::Hearing;
14use crate::directory::Directory;
15use crate::store::wait;
16use crate::wav::{SampleEncoding, read_channels};
17use sva_core::success_envelope;
18
19pub fn render(args: &RenderArgs) -> Result<String, CliError> {
21 let here = crate::composition::composition()?;
22 let (dir, target) = crate::composition::located(&here, &args.target)?;
23 for dest in args.asked.iter().filter_map(Asked::path) {
25 refuse_inside(&dir, dest)?;
26 refuse_replacing(dest, args.confirm)?;
27 }
28 let mut warnings = Vec::new();
29 let tier = crate::store::opened(&args.cache, &mut warnings)?;
30 let answered = answered(args, &dir, &target, tier, &mut warnings);
31 crate::store::persisted(tier, &mut warnings);
32 answered.map(|data| success_envelope(&data, &warnings))
33}
34
35fn answered(
37 args: &RenderArgs,
38 dir: &Path,
39 target: &str,
40 tier: &Tier<Directory>,
41 warnings: &mut Vec<Diagnostic>,
42) -> Result<String, CliError> {
43 let source = sva_ast::Dir::at(dir);
44 let logs = std::env::var("SVA_LOG").is_ok_and(|level| level == "debug");
45 let hearing = hearing(args)?;
46 let job = Job {
47 until: args.until.as_deref(),
48 rate: args.rate,
49 bits: args.bits,
50 asked: &args.asked,
51 threads: args.threads,
52 hear: hearing.as_ref().map(|hearing| hearing as &dyn Hear),
53 ..Job::over(&source, target)
54 };
55 let rendered = wait(execute_over(job, tier, &mut Session::default()));
56 let stopped = hearing.as_ref().and_then(Hearing::stopped);
57 let rendered = match (rendered, stopped) {
58 (_, Some(why)) => return Err(why),
59 (rendered, None) => rendered?,
60 };
61 let played = hearing.map(Hearing::finish).transpose()?;
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 writing 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 kept: Vec<&Asked> = args.asked.iter().filter(|a| !plays(a)).collect();
88 let mut taken = Vec::with_capacity(kept.len());
89 for asked in &kept {
90 let node = asked.node.as_deref().unwrap_or(&rendered.target);
91 let answer = rendered.answer(node, asked.representation)?;
92 taken.push(match asked.brief {
93 true => briefed(answer),
94 false => answer,
95 });
96 }
97 let (answers, written) = routed(&kept, taken, &framing)?;
98
99 Ok(query_data(&Report {
100 target: &args.target,
101 rate,
102 bits: Some(bits),
103 interval,
104 profile: rendered.config.profile.name,
105 label: rendered.label(),
106 written: &written,
107 played: played.as_ref(),
108 answers: &answers,
109 limit: Some(SAMPLE_LIMIT),
110 }))
111}
112
113fn plays(asked: &Asked) -> bool {
114 matches!(asked.dest, Some(Destination::Alsa { .. }))
115}
116
117fn hearing(args: &RenderArgs) -> Result<Option<Hearing>, CliError> {
120 let alsa = args.asked.iter().find_map(|a| match &a.dest {
121 Some(Destination::Alsa {
122 device,
123 buffer_secs,
124 }) => Some((a.name.as_str(), device, *buffer_secs)),
125 _ => None,
126 });
127 let Some((name, device, buffer_secs)) = alsa else {
128 return Ok(None);
129 };
130 let encoding = SampleEncoding::of(sva_core::precision_bits(args.bits)?);
131 let named = (name, args.target.as_str());
132 Hearing::open(device, buffer_secs, encoding, named).map(Some)
133}
134
135type Routed<'a> = (Vec<Printed>, Vec<(String, &'a Path)>);
137
138fn routed<'a>(
141 asked: &[&'a Asked],
142 taken: Vec<Answer>,
143 framing: &Framing,
144) -> Result<Routed<'a>, CliError> {
145 let mut answers = Vec::new();
146 let mut written = Vec::new();
147 for (one, answer) in asked.iter().zip(taken) {
148 let printed = Printed {
149 name: one.name.clone(),
150 answer,
151 skim: one.skim,
152 };
153 match &one.dest {
154 None => answers.push(printed),
155 Some(Destination::File(dest)) => {
156 write(&printed, dest, framing)?;
157 written.push((one.name.clone(), dest.as_path()));
158 }
159 Some(Destination::Alsa { .. }) => {
160 unreachable!("a device heard its reading as it rendered")
161 }
162 }
163 }
164 Ok((answers, written))
165}
166
167fn briefed(answer: Answer) -> Answer {
170 let Output::Ledger(entries) = answer.value else {
171 return answer;
172 };
173 Answer {
174 value: Output::Ledger(
175 entries
176 .into_iter()
177 .filter(|e| e.clipped == Some(true))
178 .collect(),
179 ),
180 ..answer
181 }
182}
183
184pub fn analyze(args: &AnalyzeArgs) -> Result<String, CliError> {
187 let dir = cwd()?;
188 let composition = is_composition(&dir);
189 for dest in args.asked.iter().filter_map(Asked::path) {
190 if composition {
191 refuse_inside(&dir, dest)?;
192 }
193 refuse_replacing(dest, args.confirm)?;
194 }
195 let (planes, rate) = read_channels(&args.path)?;
196 let target = args.path.display().to_string();
197 let held = Buffer::of_planes(
198 rate,
199 planes
200 .iter()
201 .map(|p| p.iter().map(|v| f64::from(*v)).collect())
202 .collect(),
203 );
204 crate::args::check_frame(&args.asked, rate)?;
205 let buffer = held;
206 let interval = Some((0.0, buffer.len() as f64 / f64::from(rate)));
207 let framing = Framing {
208 target: target.clone(),
209 rate,
210 bits: None,
211 interval,
212 profile: PSYCHOACOUSTIC_V1.name,
213 encoding: SampleEncoding::Float,
214 replace: args.confirm,
215 };
216
217 let mut taken = Vec::with_capacity(args.asked.len());
218 for asked in &args.asked {
219 taken.push(
220 answer_buffer(
221 &target,
222 &buffer,
223 asked.representation,
224 PSYCHOACOUSTIC_V1.name,
225 )
226 .map_err(CliError::Engine)?,
227 );
228 }
229 let (answers, written) = routed(&args.asked.iter().collect::<Vec<_>>(), taken, &framing)?;
230 Ok(success_envelope(
231 &query_data(&Report {
232 target: &target,
233 rate,
234 bits: None,
235 interval,
236 profile: PSYCHOACOUSTIC_V1.name,
237 label: None,
238 written: &written,
239 played: None,
240 answers: &answers,
241 limit: Some(SAMPLE_LIMIT),
242 }),
243 &[],
244 ))
245}
246
247fn is_composition(dir: &Path) -> bool {
249 dir.join(sva_core::ROOT).is_file() || dir.join(sva_ast::VARIABLES).is_dir()
250}