1mod device;
27pub mod kernels;
28
29use crate::error::{OpsError, OpsResult};
30
31pub use device::detect_crossings_on_device;
32
33fn invalid(msg: String) -> OpsError {
34 OpsError::Core(dspk_core::DspError::InvalidConfig(msg))
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
39pub enum Direction {
40 #[default]
42 Rising,
43 Falling,
45 Both,
48}
49
50#[derive(Debug, Clone, PartialEq)]
52pub struct CrossingOptions {
53 pub levels: Vec<f32>,
56 pub releases: Vec<f32>,
60 pub direction: Direction,
61 pub min_duration_samples: u64,
63 pub refractory_samples: u64,
65}
66
67impl CrossingOptions {
68 pub fn new(level: f32) -> Self {
70 Self { levels: vec![level], releases: Vec::new(), direction: Direction::Rising, min_duration_samples: 0, refractory_samples: 0 }
71 }
72
73 fn sides(&self, channels: usize) -> OpsResult<Vec<Side>> {
76 let spread = |v: &[f32], what: &str| -> OpsResult<Vec<f32>> {
77 match v.len() {
78 1 => Ok(vec![v[0]; channels]),
79 n if n == channels => Ok(v.to_vec()),
80 n => Err(invalid(format!("crossings: {n} {what} for {channels} channels (give one, or one per channel)"))),
81 }
82 };
83 let levels = spread(&self.levels, "levels")?;
84 let releases = if self.releases.is_empty() { levels.clone() } else { spread(&self.releases, "releases")? };
85 for (ch, (&l, &r)) in levels.iter().zip(&releases).enumerate() {
86 if !l.is_finite() || !r.is_finite() {
87 return Err(invalid(format!("crossings: channel {ch}: level and release must be finite")));
88 }
89 let (bad, rule) = match self.direction {
90 Direction::Rising => (r > l, "at most"),
91 Direction::Falling => (r < l, "at least"),
92 Direction::Both => (r.abs() > l.abs(), "in magnitude at most"),
93 };
94 if bad {
95 return Err(invalid(format!("crossings: channel {ch}: the release ({r}) must be {rule} the level ({l})")));
96 }
97 }
98 let side = |sign: f32, magnitude: bool| {
99 let to = |v: &f32| if magnitude { v.abs() } else { sign * v };
100 Side::new(sign, levels.iter().map(to).collect(), releases.iter().map(to).collect())
101 };
102 Ok(match self.direction {
103 Direction::Rising => vec![side(1.0, false)],
104 Direction::Falling => vec![side(-1.0, false)],
105 Direction::Both => vec![side(1.0, true), side(-1.0, true)],
106 })
107 }
108}
109
110#[derive(Debug, Clone, Copy, PartialEq)]
112pub struct CrossingEvent {
113 pub channel: usize,
114 pub onset: u64,
116 pub offset: Option<u64>,
118 pub peak_sample: u64,
121 pub peak_value: f32,
122}
123
124#[derive(Debug, Clone, Copy)]
126struct ChannelState {
127 on: bool,
128 onset: u64,
129 peak: f32,
131 peak_at: u64,
132 ready_at: u64,
134}
135
136#[derive(Debug, Clone)]
138struct Side {
139 sign: f32,
140 levels: Vec<f32>,
141 releases: Vec<f32>,
142 state: Vec<ChannelState>,
143}
144
145impl Side {
146 fn new(sign: f32, levels: Vec<f32>, releases: Vec<f32>) -> Self {
147 let idle = ChannelState { on: false, onset: 0, peak: f32::NEG_INFINITY, peak_at: 0, ready_at: 0 };
148 let n = levels.len();
149 Self { sign, levels, releases, state: vec![idle; n] }
150 }
151}
152
153#[derive(Debug, Clone)]
155pub struct CrossingTracker {
156 sides: Vec<Side>,
158 min_duration: u64,
159 refractory: u64,
160 prev: Vec<f32>,
162 events: Vec<CrossingEvent>,
163}
164
165type Crossings<'a> = &'a [(u32, u32)];
168type Stretches<'a> = &'a [(f32, u32)];
171
172impl CrossingTracker {
173 pub fn new(channels: usize, options: &CrossingOptions) -> OpsResult<Self> {
174 Ok(Self {
175 sides: options.sides(channels)?,
176 min_duration: options.min_duration_samples,
177 refractory: options.refractory_samples,
178 prev: vec![f32::NAN; channels],
179 events: Vec::new(),
180 })
181 }
182
183 pub fn channels(&self) -> usize {
184 self.prev.len()
185 }
186
187 fn take(&mut self, side: usize, ch: usize, origin: u64, crossings: Crossings, stretches: Stretches) {
190 debug_assert_eq!(stretches.len(), crossings.len() + 1);
191 let sign = self.sides[side].sign;
192 let st = &mut self.sides[side].state[ch];
193 let grow = |st: &mut ChannelState, (v, t): (f32, u32)| {
194 if v > st.peak {
195 (st.peak, st.peak_at) = (v, origin + t as u64);
196 }
197 };
198 if st.on {
199 grow(st, stretches[0]);
200 }
201 for (k, &(t, kind)) in crossings.iter().enumerate() {
202 let at = origin + t as u64;
203 let after = stretches[k + 1];
204 if kind == kernels::CROSSING_UP {
205 if st.on {
206 grow(st, after);
207 } else if at >= st.ready_at {
208 (st.on, st.onset, st.peak, st.peak_at) = (true, at, f32::NEG_INFINITY, at);
209 grow(st, after);
210 }
211 } else if st.on {
212 st.on = false;
213 if at - st.onset >= self.min_duration {
214 self.events.push(CrossingEvent { channel: ch, onset: st.onset, offset: Some(at), peak_sample: st.peak_at, peak_value: sign * st.peak });
215 st.ready_at = at + self.refractory;
216 }
217 }
218 }
219 }
220
221 pub fn push_host_window(&mut self, rows: &[f32], samples: usize, scan: std::ops::Range<usize>, origin: u64) {
225 let mut crossings = Vec::new();
226 let mut stretches = Vec::new();
227 for side in 0..self.sides.len() {
228 for ch in 0..self.channels() {
229 let row = &rows[ch * samples..(ch + 1) * samples];
230 let Side { sign: s, ref levels, ref releases, .. } = self.sides[side];
231 let (l, r) = (levels[ch], releases[ch]);
232 crossings.clear();
233 for t in scan.clone() {
234 let p = if t > 0 { row[t - 1] } else { self.prev[ch] };
235 let (sp, sv) = (s * p, s * row[t]);
236 if sp < l && sv >= l {
237 crossings.push((t as u32, kernels::CROSSING_UP));
238 } else if sp >= r && sv < r {
239 crossings.push((t as u32, kernels::CROSSING_DOWN));
240 }
241 }
242 stretches.clear();
243 let bounds: Vec<u32> = std::iter::once(scan.start as u32).chain(crossings.iter().map(|c| c.0)).chain(std::iter::once(scan.end as u32)).collect();
244 for w in bounds.windows(2) {
245 let mut best = (-3.0e38f32, w[0]);
246 for t in w[0]..w[1] {
247 let v = s * row[t as usize];
248 if v > best.0 {
249 best = (v, t);
250 }
251 }
252 stretches.push(best);
253 }
254 self.take(side, ch, origin, &crossings, &stretches);
255 }
256 }
257 if scan.end > 0 {
258 for ch in 0..self.channels() {
259 self.prev[ch] = rows[ch * samples + scan.end - 1];
260 }
261 }
262 }
263
264 pub fn finish(mut self) -> Vec<CrossingEvent> {
266 for side in &self.sides {
267 for (ch, st) in side.state.iter().enumerate() {
268 if st.on {
269 self.events.push(CrossingEvent { channel: ch, onset: st.onset, offset: None, peak_sample: st.peak_at, peak_value: side.sign * st.peak });
270 }
271 }
272 }
273 self.events.sort_by(|a, b| (a.onset, a.channel).cmp(&(b.onset, b.channel)).then(a.peak_value.total_cmp(&b.peak_value)));
274 self.events
275 }
276}
277
278pub fn detect_crossings(rows: &[f32], channels: usize, samples: usize, options: &CrossingOptions) -> OpsResult<Vec<CrossingEvent>> {
280 if rows.len() != channels * samples {
281 return Err(invalid(format!("crossings: {} values for {channels} × {samples}", rows.len())));
282 }
283 let mut tracker = CrossingTracker::new(channels, options)?;
284 tracker.push_host_window(rows, samples, 0..samples, 0);
285 Ok(tracker.finish())
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291
292 fn signal() -> (Vec<f32>, usize) {
294 let n = 40;
295 let mut x = vec![0.0f32; 2 * n];
296 for (t, v) in [(5, 6.0), (6, 9.0), (7, 7.0), (8, 1.0), (20, 8.0), (21, 4.0), (22, 10.0), (23, 6.0)] {
297 x[t] = v;
298 }
299 for t in 30..35 {
300 x[n + t] = 3.0 + t as f32 - 30.0;
301 }
302 (x, n)
303 }
304
305 #[test]
306 fn events_with_hysteresis_and_peaks() {
307 let (x, n) = signal();
308 let mut o = CrossingOptions::new(5.0);
309 o.releases = vec![2.0];
310 let ev = detect_crossings(&x, 2, n, &o).unwrap();
311 let got: Vec<_> = ev.iter().map(|e| (e.channel, e.onset, e.offset, e.peak_sample, e.peak_value)).collect();
312 assert_eq!(got, vec![(0, 5, Some(8), 6, 9.0), (0, 20, Some(24), 22, 10.0), (1, 32, Some(35), 34, 7.0)]);
314 let ev = detect_crossings(&x, 2, n, &CrossingOptions::new(5.0)).unwrap();
316 assert_eq!(ev.iter().filter(|e| e.channel == 0).count(), 3);
317 }
318
319 #[test]
320 fn min_duration_refractory_and_open_events() {
321 let (x, n) = signal();
322 let mut o = CrossingOptions::new(5.0);
323 o.min_duration_samples = 4;
324 let ev = detect_crossings(&x, 2, n, &o).unwrap();
325 assert!(ev.iter().all(|e| e.offset.unwrap() - e.onset >= 4), "{ev:?}");
326 let mut o = CrossingOptions::new(5.0);
327 o.releases = vec![2.0];
328 o.refractory_samples = 20;
329 let ev = detect_crossings(&x[..n], 1, n, &o).unwrap();
330 assert_eq!(ev.len(), 1, "the second bump starts within 20 samples of the first's end");
331 let ev = detect_crossings(&x[..21], 1, 21, &CrossingOptions::new(5.0)).unwrap();
333 assert_eq!(ev.last().map(|e| (e.onset, e.offset)), Some((20, None)));
334 }
335
336 #[test]
337 fn falling_and_chunks_give_the_same_events() {
338 let (x, n) = signal();
339 let neg: Vec<f32> = x.iter().map(|v| -v).collect();
340 let mut o = CrossingOptions::new(-5.0);
341 o.releases = vec![-2.0];
342 o.direction = Direction::Falling;
343 let whole = detect_crossings(&neg, 2, n, &o).unwrap();
344 assert_eq!(whole.iter().map(|e| e.peak_value).collect::<Vec<_>>(), vec![-9.0, -10.0, -7.0]);
345 let mut t = CrossingTracker::new(2, &o).unwrap();
347 for start in (0..n).step_by(7) {
348 let end = (start + 7).min(n);
349 let w = end - start;
350 let rows: Vec<f32> = (0..2).flat_map(|c| neg[c * n + start..c * n + end].to_vec()).collect();
351 t.push_host_window(&rows, w, 0..w, start as u64);
352 }
353 assert_eq!(t.finish(), whole);
354 }
355
356 #[test]
357 fn both_directions_at_once() {
358 let (x, n) = signal();
359 let mut y = x[..n].to_vec();
361 for t in 30..33 {
362 y[t] = -8.0;
363 }
364 let mut o = CrossingOptions::new(5.0);
365 o.releases = vec![2.0];
366 o.direction = Direction::Both;
367 let ev = detect_crossings(&y, 1, n, &o).unwrap();
368 let got: Vec<_> = ev.iter().map(|e| (e.onset, e.offset, e.peak_value)).collect();
369 assert_eq!(got, vec![(5, Some(8), 9.0), (20, Some(24), 10.0), (30, Some(33), -8.0)]);
370 o.levels = vec![-5.0];
372 assert_eq!(detect_crossings(&y, 1, n, &o).unwrap(), ev);
373 }
374
375 #[test]
376 fn bad_options_are_refused() {
377 assert!(detect_crossings(&[0.0; 4], 2, 2, &CrossingOptions { levels: vec![1.0, 2.0, 3.0], ..CrossingOptions::new(0.0) }).is_err());
378 let mut o = CrossingOptions::new(5.0);
379 o.releases = vec![6.0];
380 assert!(detect_crossings(&[0.0; 4], 2, 2, &o).is_err(), "a rising release above the level");
381 }
382}