1use std::path::Path;
4
5use sva_core::{
6 Answer, Asked, CliError, Destination, Diagnostic, Hear, Job, Output, Printed, Report,
7 SAMPLE_LIMIT, Session, Tier, analyzed, cwd, execute_over, query_data,
8};
9use sva_engine::{Buffer, PSYCHOACOUSTIC_V1, 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::recording::read_recording;
16use crate::store::wait;
17use crate::wav::SampleEncoding;
18use sva_core::success_envelope;
19
20pub fn render(args: &RenderArgs) -> Result<String, CliError> {
22 let here = crate::composition::composition()?;
23 let (dir, target) = crate::composition::located(&here, &args.target)?;
24 for dest in args.asked.iter().filter_map(Asked::path) {
26 refuse_inside(&dir, dest)?;
27 refuse_replacing(dest, args.confirm)?;
28 }
29 let mut warnings = Vec::new();
30 let tier = crate::store::opened(&args.cache, &mut warnings, crate::store::On::Persist)?;
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
36fn 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 files = crate::heard::Heard::at(dir)?;
45 let source = sva_core::overlaid(&files, &args.sets)?;
46 let logs = std::env::var("SVA_LOG").is_ok_and(|level| level == "debug");
47 let hearing = hearing(args)?;
48 let job = Job {
49 until: args.until.as_deref(),
50 rate: args.rate,
51 bits: args.bits,
52 asked: &args.asked,
53 threads: args.threads,
54 hear: hearing.as_ref().map(|hearing| hearing as &dyn Hear),
55 ..Job::over(&source, target)
56 };
57 let rendered = wait(execute_over(job, tier, &mut Session::default()));
58 let stopped = hearing.as_ref().and_then(Hearing::stopped);
59 let rendered = match (rendered, stopped) {
60 (_, Some(why)) => return Err(why),
61 (rendered, None) => rendered?,
62 };
63 let played = hearing.map(Hearing::finish).transpose()?;
64
65 warnings.extend(rendered.warnings.iter().map(sva_core::warning_diagnostic));
66 let rate = rendered.config.rate;
67 let stats = rendered.render.cache_stats.as_ref();
68 if let Some(why) = stats.and_then(|stats| stats.unstaged.as_deref()) {
69 let message = format!("this render stopped writing what it computed: {why}");
70 warnings.push(crate::store::warning("store.unstaged", message));
71 }
72 if let (true, Some(stats)) = (logs, stats) {
73 eprint!("{}", cache_log(stats, rate));
74 }
75 let bits = rendered.config.profile.precision_bits;
76 let interval = rendered
77 .render
78 .range
79 .map(|r| (r.start_secs(rate), r.end as f64 / f64::from(rate)));
80 let framing = Framing {
81 target: args.target.clone(),
82 rate,
83 bits: Some(bits),
84 interval,
85 profile: rendered.config.profile.name,
86 encoding: SampleEncoding::of(bits),
87 replace: args.confirm,
88 };
89
90 let kept: Vec<&Asked> = args.asked.iter().filter(|a| !plays(a)).collect();
91 let mut taken = Vec::with_capacity(kept.len());
92 for asked in &kept {
93 let answer = rendered.answered(asked)?;
94 taken.push(match asked.brief {
95 true => briefed(answer),
96 false => answer,
97 });
98 }
99 let tempo = (rate, rendered.seconds_per_bar());
100 let (answers, written) = routed(&kept, taken, tempo, &framing)?;
101
102 Ok(query_data(&Report {
103 target: &args.target,
104 rate,
105 bits: Some(bits),
106 interval,
107 profile: rendered.config.profile.name,
108 label: rendered.label(),
109 written: &written,
110 played: played.as_ref(),
111 answers: &answers,
112 limit: Some(SAMPLE_LIMIT),
113 }))
114}
115
116fn plays(asked: &Asked) -> bool {
117 matches!(asked.dest, Some(Destination::Alsa { .. }))
118}
119
120fn hearing(args: &RenderArgs) -> Result<Option<Hearing>, CliError> {
123 let alsa = args.asked.iter().find_map(|a| match &a.dest {
124 Some(Destination::Alsa {
125 device,
126 buffer_secs,
127 }) => Some((a.name.as_str(), device, *buffer_secs)),
128 _ => None,
129 });
130 let Some((name, device, buffer_secs)) = alsa else {
131 return Ok(None);
132 };
133 let encoding = SampleEncoding::of(sva_core::precision_bits(args.bits)?);
134 let named = (name, args.target.as_str());
135 Hearing::open(device, buffer_secs, encoding, named).map(Some)
136}
137
138type Routed<'a> = (Vec<Printed>, Vec<(String, &'a Path)>);
140
141fn routed<'a>(
144 asked: &[&'a Asked],
145 taken: Vec<Answer>,
146 (rate, seconds_per_bar): (u32, Option<f64>),
147 framing: &Framing,
148) -> Result<Routed<'a>, CliError> {
149 let mut answers = Vec::new();
150 let mut written = Vec::new();
151 for (one, answer) in asked.iter().zip(taken) {
152 let printed = Printed::of(one, answer, rate, seconds_per_bar)?;
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_recording(&args.path)?;
196 let target = args.path.display().to_string();
197 let buffer = Buffer::of_planes(
198 rate,
199 planes
200 .iter()
201 .map(|p| p.iter().map(|v| f64::from(*v)).collect())
202 .collect(),
203 );
204 let interval = Some((0.0, buffer.len() as f64 / f64::from(rate)));
205 let framing = Framing {
206 target: target.clone(),
207 rate,
208 bits: None,
209 interval,
210 profile: PSYCHOACOUSTIC_V1.name,
211 encoding: SampleEncoding::Float,
212 replace: args.confirm,
213 };
214
215 let taken = analyzed(&target, &buffer, &args.asked)?;
216 let asked: Vec<&Asked> = args.asked.iter().collect();
217 let (answers, written) = routed(&asked, taken, (rate, None), &framing)?;
218 Ok(success_envelope(
219 &query_data(&Report {
220 target: &target,
221 rate,
222 bits: None,
223 interval,
224 profile: PSYCHOACOUSTIC_V1.name,
225 label: None,
226 written: &written,
227 played: None,
228 answers: &answers,
229 limit: Some(SAMPLE_LIMIT),
230 }),
231 &[],
232 ))
233}
234
235fn is_composition(dir: &Path) -> bool {
237 dir.join(sva_core::ROOT).is_file() || dir.join(sva_ast::VARIABLES).is_dir()
238}