1use crate::grid::{BINS, N_FFT, SR, SR_F, design_freqs};
5use crate::setup::{Setup, SubFilter};
6use crate::{C, EngineConfig, EngineError, MeasurementSet, Samples};
7use secs_dsp::{fft, response};
8use std::{f64::consts::PI, sync::Arc};
9
10pub const WINDOW_MS: f64 = 500.0;
11
12pub(crate) const SPECTRUM_BYTES: u64 = (BINS * std::mem::size_of::<C>()) as u64;
14
15#[cfg(test)]
16thread_local! {
17 pub(crate) static SPECTRA_BUILT: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
20}
21
22pub(crate) fn cells(set: &MeasurementSet, roles: Option<&[String]>) -> u64 {
24 set.positions
25 .iter()
26 .flat_map(|(_, measured)| measured)
27 .filter(|(role, _)| roles.is_none_or(|roles| roles.contains(role)))
28 .count() as u64
29}
30
31pub(crate) fn spectra_bytes(set: &MeasurementSet) -> u64 {
33 cells(set, None).saturating_mul(SPECTRUM_BYTES)
34}
35
36pub(crate) fn build_scratch_bytes(filters: usize) -> u64 {
41 (4 * N_FFT * std::mem::size_of::<f64>()) as u64
42 + (2 + filters as u64).saturating_mul(SPECTRUM_BYTES)
43}
44
45pub(crate) fn preparation_bytes(set: &MeasurementSet, setup: &Setup) -> u64 {
47 spectra_bytes(set).saturating_add(build_scratch_bytes(setup.sub_filters.len()))
48}
49
50pub fn onset_index(ir: &Samples) -> usize {
51 if ir.is_empty() {
52 return 0;
53 }
54 let mut peak = 0;
55 for i in 1..ir.len() {
56 if ir.at(i).abs() > ir.at(peak).abs() {
57 peak = i;
58 }
59 }
60 let threshold = 0.1 * ir.at(peak).abs();
61 (0..=peak)
62 .find(|&i| ir.at(i).abs() >= threshold)
63 .unwrap_or(peak)
64}
65
66fn window(ir: &Samples, start: usize, length: usize, taper: f64) -> (Vec<f64>, usize) {
67 let a = start.saturating_sub(96).min(ir.len());
68 let end = a.saturating_add(length).min(ir.len());
69 let mut w: Vec<_> = (a..end).map(|i| ir.at(i)).collect();
70 let k = (w.len() as f64 * taper) as usize;
71 let from = w.len() - k;
72 for (i, v) in w[from..].iter_mut().enumerate() {
73 *v *= 0.5 * (1.0 + (PI * i as f64 / k as f64).cos());
74 }
75 (w, a)
76}
77
78pub fn windowed(ir: &Samples, start: usize) -> (Vec<f64>, usize) {
79 window(ir, start, (WINDOW_MS / 1000.0 * SR_F) as usize, 0.2)
80}
81
82pub fn spectrum(x: &[f64]) -> Vec<C> {
83 let mut padded = vec![0.0; N_FFT];
84 let n = x.len().min(N_FFT);
85 padded[..n].copy_from_slice(&x[..n]);
86 fft::rfft(&padded)
87}
88
89pub(crate) fn ramp_phase(f: f64, t: f64) -> f64 {
93 0.0 + (-2.0 * PI * f) * t
94}
95
96pub fn delay_response(ms: f64) -> Vec<C> {
101 design_freqs()
102 .iter()
103 .map(|&f| C::from_polar(1.0, ramp_phase(f, ms) * 0.001))
104 .collect()
105}
106
107pub fn sub_filter_response(filter: &SubFilter) -> Vec<C> {
108 let mut out = vec![C::new(1.0, 0.0); BINS];
109 for (hz, kind, typ) in [
110 (filter.hp_hz, "HP", filter.hp_type),
111 (filter.lp_hz, "LP", filter.lp_type),
112 ] {
113 if hz > 0.0 {
114 if let Ok(h) = response::band_filter(design_freqs(), hz, kind, typ.name()) {
116 for (v, h) in out.iter_mut().zip(h) {
117 *v *= h;
118 }
119 }
120 }
121 }
122 if !filter.peq.is_empty() {
123 for (v, h) in out
124 .iter_mut()
125 .zip(response::peq_response(design_freqs(), &filter.peq))
126 {
127 *v *= h;
128 }
129 }
130 out
131}
132
133#[derive(Debug, Clone, Default)]
135pub struct Spectra {
136 positions: Vec<u32>,
137 cells: Vec<Vec<(String, Arc<[C]>)>>,
138}
139
140impl Spectra {
141 pub fn positions(&self) -> &[u32] {
142 &self.positions
143 }
144 pub fn get(&self, position: u32, role: &str) -> Option<&Arc<[C]>> {
145 let p = self.positions.binary_search(&position).ok()?;
146 self.cells[p]
147 .iter()
148 .find(|(r, _)| r == role)
149 .map(|(_, h)| h)
150 }
151 pub fn roles_at(&self, position: u32) -> impl Iterator<Item = &str> {
152 self.positions
153 .binary_search(&position)
154 .ok()
155 .into_iter()
156 .flat_map(|p| self.cells[p].iter().map(|(r, _)| r.as_str()))
157 }
158 pub fn set(&mut self, position: u32, role: &str, value: Arc<[C]>) {
159 let p = self.ensure_position(position);
160 if let Some((_, h)) = self.cells[p].iter_mut().find(|(r, _)| r == role) {
161 *h = value;
162 } else {
163 self.cells[p].push((role.to_owned(), value));
164 }
165 }
166 fn ensure_position(&mut self, position: u32) -> usize {
167 match self.positions.binary_search(&position) {
168 Ok(p) => p,
169 Err(p) => {
170 self.positions.insert(p, position);
171 self.cells.insert(p, Vec::new());
172 p
173 }
174 }
175 }
176}
177
178#[derive(Debug, Clone)]
179pub struct Prepared {
180 pub arrival_s: Vec<(String, f64)>,
181 pub align_delay_ms: Vec<(String, f64)>,
182 pub base_s: f64,
183 pub h: Spectra,
184 pub h0: Spectra,
185}
186
187fn check(cancelled: &dyn Fn() -> bool) -> Result<(), EngineError> {
191 if cancelled() {
192 Err(EngineError::Cancelled)
193 } else {
194 Ok(())
195 }
196}
197
198fn validate(set: &MeasurementSet, cancelled: &dyn Fn() -> bool) -> Result<(), EngineError> {
199 let mut previous = None;
200 for (pos, roles) in &set.positions {
201 if previous.is_some_and(|p| p >= *pos) {
202 return Err(EngineError::Input(
203 "positions must be strictly ascending".into(),
204 ));
205 }
206 previous = Some(*pos);
207 for (i, (role, m)) in roles.iter().enumerate() {
208 check(cancelled)?;
209 if m.sample_rate != SR {
210 return Err(EngineError::MeasurementSampleRate {
211 position: *pos,
212 role: role.clone(),
213 actual_hz: m.sample_rate,
214 expected_hz: SR,
215 });
216 }
217 if m.samples.is_empty()
218 || !m.t0.is_finite()
219 || (0..m.samples.len()).any(|i| !m.samples.at(i).is_finite())
220 {
221 return Err(EngineError::InvalidMeasurement {
222 position: *pos,
223 role: role.clone(),
224 });
225 }
226 if roles[..i].iter().any(|(r, _)| r == role) {
227 return Err(EngineError::DuplicateMeasurement {
228 position: *pos,
229 role: role.clone(),
230 });
231 }
232 }
233 }
234 Ok(())
235}
236
237fn build_spectra(
238 set: &MeasurementSet,
239 base: f64,
240 filters: &[(String, SubFilter)],
241 length: usize,
242 taper: f64,
243 cancelled: &dyn Fn() -> bool,
244) -> Result<Spectra, EngineError> {
245 #[cfg(test)]
246 SPECTRA_BUILT.with(|built| built.set(built.get() + 1));
247 let filters: Vec<_> = filters
248 .iter()
249 .map(|(r, f)| (r, sub_filter_response(f)))
250 .collect();
251 let mut out = Spectra::default();
252 for (pos, roles) in &set.positions {
253 out.ensure_position(*pos);
254 for (role, m) in roles {
255 check(cancelled)?;
256 let (w, a) = window(&m.samples, onset_index(&m.samples), length, taper);
257 let start_t = m.t0 + a as f64 / SR_F - base;
258 let mut h = spectrum(&w);
259 for (h, f) in h.iter_mut().zip(design_freqs()) {
260 *h *= C::from_polar(1.0, ramp_phase(*f, start_t));
261 }
262 if let Some((_, filter)) = filters.iter().find(|(r, _)| *r == role) {
263 for (h, f) in h.iter_mut().zip(filter) {
264 *h *= f;
265 }
266 }
267 out.set(*pos, role, h.into());
268 }
269 }
270 Ok(out)
271}
272
273pub fn prepare(
275 set: &MeasurementSet,
276 config: &EngineConfig,
277 setup: &Setup,
278) -> Result<Prepared, EngineError> {
279 prepare_until(set, config, setup, &|| false)
280}
281
282pub fn prepare_until(
286 set: &MeasurementSet,
287 config: &EngineConfig,
288 setup: &Setup,
289 cancelled: &dyn Fn() -> bool,
290) -> Result<Prepared, EngineError> {
291 validate(set, cancelled)?;
292 let main = set
293 .positions
294 .iter()
295 .find(|(p, _)| *p == config.main_position)
296 .ok_or(EngineError::MissingReferencePosition {
297 position: config.main_position,
298 })?;
299 if !main.1.iter().any(|(r, _)| config.mains.contains(r)) {
300 return Err(EngineError::MissingMainMeasurement {
301 position: config.main_position,
302 });
303 }
304 let arrival_s = main
305 .1
306 .iter()
307 .map(|(r, m)| {
308 check(cancelled)?;
309 Ok((r.clone(), m.t0 + onset_index(&m.samples) as f64 / SR_F))
310 })
311 .collect::<Result<Vec<_>, EngineError>>()?;
312 let latest = arrival_s
313 .iter()
314 .filter(|(r, _)| config.mains.contains(r) || config.subs.contains(r))
315 .map(|(_, a)| *a)
316 .fold(f64::NEG_INFINITY, f64::max);
317 let base_s = arrival_s
318 .iter()
319 .map(|(_, a)| *a)
320 .fold(f64::INFINITY, f64::min);
321 let align_delay_ms = arrival_s
322 .iter()
323 .map(|(r, a)| (r.clone(), 1000.0 * (latest - a)))
324 .collect();
325 let h = build_spectra(set, base_s, &setup.sub_filters, 24000, 0.2, cancelled)?;
326 let h0 = h.clone();
328 Ok(Prepared {
329 arrival_s,
330 align_delay_ms,
331 base_s,
332 h,
333 h0,
334 })
335}
336
337pub(crate) fn filtered_spectra(raw: &Spectra, filters: &[(String, SubFilter)]) -> Spectra {
340 let filters: Vec<_> = filters
341 .iter()
342 .map(|(r, f)| (r, sub_filter_response(f)))
343 .collect();
344 let mut out = raw.clone();
345 for &position in raw.positions() {
346 for role in raw.roles_at(position) {
347 if let Some((_, filter)) = filters.iter().find(|(r, _)| r.as_str() == role) {
348 let h = raw.get(position, role).expect("existing cell");
349 out.set(
350 position,
351 role,
352 h.iter().zip(filter).map(|(h, f)| h * f).collect(),
353 );
354 }
355 }
356 }
357 out
358}
359
360pub fn long_spectra(
362 set: &MeasurementSet,
363 prepared: &Prepared,
364 sub_filters: &[(String, SubFilter)],
365 window_ms: f64,
366) -> Result<Spectra, EngineError> {
367 long_spectra_until(set, prepared, sub_filters, window_ms, &|| false)
368}
369
370pub fn long_spectra_until(
373 set: &MeasurementSet,
374 prepared: &Prepared,
375 sub_filters: &[(String, SubFilter)],
376 window_ms: f64,
377 cancelled: &dyn Fn() -> bool,
378) -> Result<Spectra, EngineError> {
379 validate(set, cancelled)?;
380 if !window_ms.is_finite() || window_ms <= 0.0 || !prepared.base_s.is_finite() {
381 return Err(EngineError::Input(
382 "invalid long window or spectral origin".into(),
383 ));
384 }
385 let length = (window_ms / 1000.0 * SR_F) as usize;
386 for (_, m) in set.positions.iter().flat_map(|(_, rs)| rs) {
388 check(cancelled)?;
389 let a = onset_index(&m.samples).saturating_sub(96);
390 if m.samples.len().saturating_sub(a).min(length) < 10 {
391 return Err(EngineError::Input(
392 "long window needs at least ten samples".into(),
393 ));
394 }
395 }
396 build_spectra(set, prepared.base_s, sub_filters, length, 0.1, cancelled)
397}