1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
use super::*;
use crate::filter::*;
use crate::filter::{Biquad, FilterMethods, PoleOrZero, SeriesBiquad, ZPKModel};
use crate::*;
use derive_builder::Builder;
use itertools::Itertools;
use ndarray::ArrayView1;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use rayon::prelude::*;
use std::collections::BTreeMap;
use std::sync::Arc;
#[derive(Debug, Clone)]
struct SLMChannel {
// Square of the reference level, i.e. (20 microPascal)^2
Lrefsq: Flt,
// Statistics to store
stat: SLMStat,
// The bandpass filter for this channel, if any
bp: SeriesBiquad,
// Time weighting filter
twfilter: TwFilter,
}
impl SLMChannel {
/// Run the SLM channel on the given prefiltered data, returning the output
/// if `provide_output` is true. If output is not requested, returns `None`,
/// and only statistics are updated.
///
/// # Arguments
///
/// * `prefiltered` - The prefiltered data to run this bandpass filter on.
/// * `provide_output` - Whether to return the output of the channel.
fn run(&mut self, prefiltered: &[Flt], provide_output: bool) -> Option<Vec<Flt>> {
// Do not continue below if there is no data to run on
if prefiltered.is_empty() {
if provide_output {
return Some(vec![]);
} else {
return None;
}
}
assert!(!prefiltered.is_empty());
// Simple level computer for power inputs
let level_fun = |a| 10. * Flt::log10(a / self.Lrefsq);
let mut tmp = vec![0.; prefiltered.len()];
self.bp.filter(prefiltered, &mut tmp);
let mut N = self.stat.N;
// Filtered squared
let mut filtered_squared = {
let mut tmp_view = ArrayViewMut1::from(&mut tmp);
tmp_view.mapv_inplace(|a| a * a);
tmp_view
};
// Compute and update Lpk and Leq
filtered_squared.for_each(|sample_pwr| {
let new_pk = sample_pwr.abs();
if new_pk > self.stat.Ppk {
self.stat.Ppk = new_pk;
}
// Update equivalent level
self.stat.Peq = (self.stat.Peq * N as Flt + sample_pwr) / (N as Flt + 1.);
N += 1;
});
// Apply time weighting to filtered squared signal
filtered_squared.mapv_inplace(|s| self.twfilter.filter(s));
// Update max signal power gotten so far
let time_weighted = &mut filtered_squared;
time_weighted.for_each(|val| {
if *val > self.stat.Pmax {
self.stat.Pmax = *val;
}
});
// Update last signal power coming from SLM
self.stat.Pt_last = *filtered_squared.last().unwrap();
// Convert output to levels
filtered_squared.mapv_inplace(level_fun);
self.stat.N += prefiltered.len();
if provide_output { Some(tmp) } else { None }
}
}
/// Sound Level Meter
#[cfg_attr(feature = "python-bindings", gen_stub_pyclass, pyclass(from_py_object))]
#[derive(Debug, Clone)]
pub struct SLM {
// Pre-filter (A-weighting, Z-weighting, etc.)
prefilter: SeriesBiquad,
/// SLM channels (bandpass filters) keyed by their filter descriptor. In a
/// BTreeMap, such that order is preserved on the keys.
channels: BTreeMap<StandardFilterDescriptor, SLMChannel>,
/// Current frame counter: frame index of the next sample to be
/// processed, relative to t = 0 of the measurement. Negative
/// during pre-capture. `frame == 0` is exactly t = 0.
frame_ctr: i64,
/// Sampling period [s].
dt: Flt,
/// Number of warm-up samples remaining before the time weighting
/// filter has settled and output can be provided.
warmup_remaining: usize,
/// Output every N-th sample; 1 = no decimation.
decimation_step: usize,
/// Index of the next decimated frame to be outputed. None, if no output is
/// yet ready (i.e. the warm-up period has not yet passed, or the
/// `frame_ctr` is not yet >= 0).
next_decimated_frame: Option<usize>,
}
impl SLM {
/// Create new Sound Level Meter from given settings.
///
/// # Arguments
///
/// * `settings` - SLM configuration (sampling rate, filters, weightings, Lref).
/// * `initial_frame` - Optional initial frame offset, relative to t = 0 of
/// the measurement. When `Some(negative)`, the SLM suppresses output
/// until the frame counter passes zero (t > 0). When `None`, the frame
/// counter starts at zero.
pub fn new(settings: SLMSettings, initial_frame: Option<i64>) -> Self {
let fs = settings.fs;
let dt = 1.0 / *fs;
// Warm-up: 3 times the rising time constant
let warmup_frames =
(settings.timeWeighting.warmupTime().as_secs_f64() * *fs).ceil() as usize;
// Generate rectifier filter(s)
let (pole_up, pole_down) = settings.timeWeighting.getLowpassPoles();
let alpha_up = Flt::exp(pole_up / *fs);
let alpha_down = pole_down.map(|p| Flt::exp(p / *fs));
// Decimation: one output sample per fixed sample distance
let decimation_step = if settings.decimate {
let tau = 1. / pole_up.abs();
((tau * SLM_LT_DECIMATION_FRACTION * *fs).ceil() as usize).max(1)
} else {
1
};
let twfilter = TwFilter::new(alpha_up, alpha_down);
let prefilter = ZPKModel::freqWeightingFilter(settings.freqWeighting).bilinear(fs);
let channels: BTreeMap<_, _> = settings
.filterDescriptors
.iter()
.copied()
.map(|descriptor| {
let bp = descriptor.genFilter().bilinear(fs);
let stat = SLMStat::default();
let ch = SLMChannel {
stat,
bp,
twfilter,
Lrefsq: settings.Lref.powi(2),
};
(descriptor, ch)
})
.collect();
let frame_ctr = initial_frame.unwrap_or(0);
SLM {
prefilter,
channels,
frame_ctr,
dt,
warmup_remaining: warmup_frames,
decimation_step,
next_decimated_frame: None,
}
}
/// Push new time data through the sound level meter. Returns `None`
/// while the time weighting filter is warming up, or while the frame
/// counter is at or below zero (if a negative initial frame was set).
/// Once **both** warm-up is complete **and** the frame counter has
/// passed zero (t > 0), returns [SLMResult] with updated statistics
/// and optional Lt output.
///
/// The levels-vs-time output is decimated to one sample per fixed
/// sample distance when `settings.decimate` is set (see
/// [SLMSettings]). The time axis for the output samples is included
/// in the returned [SLMResult] (as an [Arc], so results can be
/// cloned cheaply).
///
/// # Args
///
/// * `new_signal` - Newly provided time data.
/// * `provide_output` - If true, Lt (and its time axis) is included
/// in [SLMResult].
pub fn run(&mut self, new_signal: &[Flt], provide_output: bool) -> Option<SLMResult> {
if new_signal.is_empty() {
return None;
}
let N = new_signal.len();
// Offset in new_signal slice to read samples from.
let mut offset = 0;
// ---- Warm-up phase ----
// Process data without collecting output until the time weighting
// filter has settled. Statistics are reset once warm-up completes.
if self.warmup_remaining > 0 {
let warmup_frames = self.warmup_remaining.min(N);
self.process_block(&new_signal[..warmup_frames], false);
self.frame_ctr += warmup_frames as i64;
self.warmup_remaining -= warmup_frames;
offset = warmup_frames;
if self.warmup_remaining == 0 {
self.reset_stats();
// Warm-up just completed. If the frame counter is already
// positive, proceed to output. Otherwise, continue to the
// negative-time suppression below.
} else {
// Warm-up not yet complete — nothing to output.
return None;
}
}
// ---- Negative-time suppression ----
// Output only starts when t >= 0, i.e. when the frame counter is
// positive. Suppress samples with frame index < 0.
if self.frame_ctr < 0 {
let suppress = self.frame_ctr.unsigned_abs() as usize;
let suppress = suppress.min(N - offset);
if suppress > 0 {
self.process_block(&new_signal[offset..offset + suppress], false);
self.frame_ctr += suppress as i64;
offset += suppress;
}
}
if offset >= N {
// No samples left for output
return None;
}
// Once we are here, the frame counter is non-negative and the warmup
// period is over. We also have at least one sample left to output as
// level vs time.
debug_assert!(self.frame_ctr >= 0);
debug_assert!(self.warmup_remaining == 0);
debug_assert!(offset < N);
// Reset statistics when frame counter is at 0.
if self.frame_ctr == 0 {
self.reset_stats();
}
// ---- Valid output phase ----
let output_slice = &new_signal[offset..];
let lt = self.process_block(output_slice, provide_output);
let (Lt, t) = if let Some(lt) = lt {
// Decimate the output. Also advances the frame counter.
let (Lt, t) = self.decimate(lt);
(Some(Lt), Some(t))
} else {
// No decimation, just advance the frame counter.
self.frame_ctr += output_slice.len() as i64;
(None, None)
};
let Lmax = self.Lmax();
let Lpk = self.Lpk();
let Leq = self.Leq();
Some(SLMResult {
Lmax,
Lpk,
Leq,
Lt,
t,
})
}
/// Extract the levels-vs-time output at a fixed sample distance.
/// Returns the decimated levels per descriptor, together with the
/// time of each decimated sample. *DOES* update `frame_ctr` and
/// `next_decimated_frame`.
fn decimate(
&mut self,
lt: BTreeMap<StandardFilterDescriptor, Vec<Flt>>,
) -> (BTreeMap<StandardFilterDescriptor, Vec<Flt>>, Vec<Flt>) {
debug_assert!(self.frame_ctr >= 0);
let N = lt.values().next().map(|v| v.len()).unwrap_or(0);
// Shortcut for the case of no decimation
if self.decimation_step == 1 {
// No decimation: output level data for each sample.
let t: Vec<Flt> = (self.frame_ctr..self.frame_ctr + N as i64)
.map(|i| i as Flt * self.dt)
.collect();
self.frame_ctr += N as i64;
return (lt, t);
}
// Initialize next_decimated_frame
if self.next_decimated_frame.is_none() {
self.next_decimated_frame = Some(self.frame_ctr as usize);
}
let mut out: BTreeMap<StandardFilterDescriptor, Vec<Flt>> = BTreeMap::new();
// BTreeMap::with_capacity(lt.len());
let mut next_decimated_frame = self
.next_decimated_frame
.expect("next_decimated_frame is None");
// Allocation capacity for decimated output
let output_capacity = N / self.decimation_step + 1;
// Compute time stamps for decimated output
let t = {
let mut t = Vec::with_capacity(output_capacity);
for i in self.frame_ctr as usize..self.frame_ctr as usize + N {
if i == next_decimated_frame {
t.push(i as Flt * self.dt);
next_decimated_frame += self.decimation_step;
}
}
t
};
// Compute decimated output for each band
for (descriptor, band) in lt.into_iter() {
// Reset decimation counter
let mut next_decimated_frame = self
.next_decimated_frame
.expect("next_decimated_frame is None");
// Pre-allocate output buffer
let mut this_band_decimated_level = Vec::with_capacity(output_capacity);
for (i, sample) in band.iter().enumerate() {
let frame = i + self.frame_ctr as usize;
if frame == next_decimated_frame {
this_band_decimated_level.push(*sample);
next_decimated_frame += self.decimation_step;
}
}
// This should be the case
debug_assert!(this_band_decimated_level.len() == t.len());
out.insert(descriptor, this_band_decimated_level);
}
// Store next decimation output for next run
self.next_decimated_frame = Some(next_decimated_frame);
self.frame_ctr += N as i64;
(out, t)
}
/// Internal: process a contiguous block of samples through the SLM. Updates
/// statistics, and returns levels vs time if `provide_output` is true, and
/// `signal` is not empty. *DOES NOT* update `frame_ctr` or
/// `next_decimated_frame`.
fn process_block(
&mut self,
signal: &[Flt],
provide_output: bool,
) -> Option<BTreeMap<StandardFilterDescriptor, Vec<Flt>>> {
if signal.is_empty() {
return if provide_output {
let empty: BTreeMap<_, _> = self.channels.keys().map(|k| (*k, vec![])).collect();
Some(empty)
} else {
None
};
}
let mut prefiltered = vec![0.; signal.len()];
self.prefilter.filter(signal, &mut prefiltered);
let results: BTreeMap<StandardFilterDescriptor, Option<Vec<Flt>>> = self
.channels
.par_iter_mut()
.map(|(desc, ch)| (*desc, ch.run(&prefiltered, provide_output)))
.collect();
if provide_output {
Some(
results
.into_iter()
.map(|(k, v)| (k, v.expect("No output provided for ch")))
.collect(),
)
} else {
// Just consume for side effects
drop(results);
None
}
}
/// Number of channels / bands in the SLM.
pub fn nch(&self) -> usize {
self.channels.len()
}
fn levels_from<T>(&self, stat_returner: T) -> BTreeMap<StandardFilterDescriptor, Flt>
where
T: Fn(&SLMChannel) -> Flt,
{
self.channels
.iter()
.map(|(desc, ch)| (*desc, 10. * Flt::log10(stat_returner(ch) / ch.Lrefsq)))
.collect()
}
fn Lmax(&self) -> BTreeMap<StandardFilterDescriptor, Flt> {
self.levels_from(|ch| ch.stat.Pmax)
}
fn Lpk(&self) -> BTreeMap<StandardFilterDescriptor, Flt> {
self.levels_from(|ch| ch.stat.Ppk)
}
fn Leq(&self) -> BTreeMap<StandardFilterDescriptor, Flt> {
self.levels_from(|ch| ch.stat.Peq)
}
/// Reset all statistics (sample counter N, Pmax, Ppk, Peq, Pt_last) to
/// their initial values. The filter states (bandpass, time weighting) are
/// preserved. This is called automatically when warm-up completes or frame
/// counter reaches zero.
pub fn reset_stats(&mut self) {
for ch in self.channels.values_mut() {
ch.stat = SLMStat::default();
}
}
/// Current time [s] of the next sample to be processed.
pub fn current_time(&self) -> Flt {
self.frame_ctr as Flt * self.dt
}
/// Current frame counter (frame index of the next sample to be
/// processed, relative to t = 0 of the measurement).
pub fn current_frame(&self) -> i64 {
self.frame_ctr
}
/// Whether the SLM has completed its warm-up.
pub fn is_warm(&self) -> bool {
self.warmup_remaining == 0
}
}
#[cfg(feature = "python-bindings")]
#[cfg_attr(feature = "python-bindings", gen_stub_pymethods, pymethods)]
impl SLM {
#[new]
#[pyo3(signature = (settings, initial_frame = None))]
fn new_py(settings: SLMSettings, initial_frame: Option<i64>) -> SLM {
SLM::new(settings, initial_frame)
}
#[gen_stub(skip)]
#[pyo3(name = "run", signature = (dat, provide_output = true))]
fn run_py(&mut self, dat: PyReadonlyArray1<Flt>, provide_output: bool) -> Option<SLMResult> {
self.run(dat.as_array().as_slice()?, provide_output)
}
#[pyo3(name = "is_warm")]
fn is_warm_py(&self) -> bool {
self.is_warm()
}
#[pyo3(name = "current_time")]
fn current_time_py(&self) -> Flt {
self.current_time()
}
}
cfg_select! {
feature = "python-bindings" => {
pyo3_stub_gen::inventory::submit! {
gen_methods_from_python! {
r#"
import numpy
import numpy.typing
import typing
class SLM:
def run(self, dat: numpy.ndarray, provide_output: bool = True) -> typing.Optional[SLMResult]:
""" Run the SLM algorithm on the input data.
Returns None during warm-up and when time is negative. """
"#
}
}
},
_ => {}
}
#[derive(Debug, Clone, Default)]
/// Quantities defined as powers, i.e. square of amplitude
struct SLMStat {
// Number of samples processed since the last stats reset
N: usize,
// Max signal power
Pmax: Flt,
// Peak signal power
Ppk: Flt,
// Equivalent signal power
Peq: Flt,
// Last obtained signal power, after last time run() is called.
Pt_last: Flt,
}
#[derive(Clone, Copy, Debug)]
struct F {
alpha: Flt,
state: Flt,
}
// Time weighting filter
#[derive(Clone, Copy, Debug)]
struct TwFilter {
// Filter constant (see lasp_doc) for time weighting and state of previous sample.
filter_state_up: F,
// Filter constant (see lasp_doc) for time weighting (only there for
// asymmetric filters (i.e. impulse time weighting)
filter_state_down: Option<F>,
}
impl TwFilter {
fn new(alpha_up: Flt, alpha_down: Option<Flt>) -> TwFilter {
let filter_state_down = alpha_down.map(|alpha_down| F {
alpha: alpha_down,
state: 0.,
});
TwFilter {
filter_state_up: F {
alpha: alpha_up,
state: 0.,
},
filter_state_down,
}
}
#[inline]
fn filter(&mut self, input: Flt) -> Flt {
if self.filter_state_down.is_some() {
self.filter_asymmetric(input)
} else {
self.filter_symmetric(input)
}
}
#[inline]
fn filter_symmetric(&mut self, input: Flt) -> Flt {
assert!(
self.filter_state_down.is_none(),
"Call this only for symmetric filters"
);
let F { alpha, state } = &mut self.filter_state_up;
let alpha = *alpha;
let output = (1. - alpha) * input + alpha * *state;
*state = output;
output
}
#[inline]
fn filter_asymmetric(&mut self, input: Flt) -> Flt {
let F {
alpha: alpha_up,
state: state_up,
} = &mut self.filter_state_up;
let alpha_up = *alpha_up;
let filter_state_down = self
.filter_state_down
.as_mut()
.expect("Call this only for asymmetric filters");
let F {
alpha: alpha_down,
state: state_down,
} = filter_state_down;
let alpha_down = *alpha_down;
let pu = (1. - alpha_up) * input + alpha_up * *state_up;
*state_up = pu;
let pIn = alpha_down * *state_down;
if pu > pIn {
*state_down = pu;
pu
} else {
*state_down = pIn;
pIn
}
}
}
#[cfg(test)]
mod test {
use approx::assert_abs_diff_eq;
use super::*;
use crate::siggen::*;
#[test]
fn test_slm1() {
const fs: Flt = 48e3;
const N: usize = (15. * fs) as usize;
let fsp = StrictlyPositive::new(fs).unwrap();
let desc = StandardFilterDescriptor::Overall().unwrap();
let settings = SLMSettingsBuilder::default()
.fs(fsp)
.timeWeighting(TimeWeighting::Fast {})
.filterDescriptors([desc])
.decimate(false)
.build()
.unwrap();
let srcdesc = SourceDescriptor::Sine {
frequency: (1000.).try_into().unwrap(),
};
let mut siggen = Siggen::new(1, srcdesc);
siggen.setAllMute(false);
siggen.reset(fsp).unwrap();
// Amplitude of a sine wave, when it is square root of 2, the result of
// the SLM should go to 94 dB SPL
siggen.setAllGains(sq2.try_into().unwrap());
let mut data = vec![0.; N];
siggen.genSignal(&mut data);
// Use None initial_frame: output after warm-up only
let mut slm = SLM::new(settings, None);
// Feed exactly warm-up frames: this block completes warm-up,
// t >= 0, so we get Some with valid stats but empty Lt.
let warmup_frames = slm.warmup_remaining;
let res = slm.run(&data[..warmup_frames], true);
assert!(res.is_none(), "Should produce no output during warm-up");
assert!(slm.is_warm(), "Should be warm now");
let res = slm.run(&data[warmup_frames..], true).unwrap();
// Lt should contain valid level data for the rest of the block
assert_eq!(
res.Lt.as_ref().unwrap()[&desc].len(),
data.len() - warmup_frames
);
let band_lt = &res.Lt.as_ref().unwrap()[&desc];
println!("{:#?}", &band_lt[band_lt.len() - 100..]);
assert_abs_diff_eq!(band_lt[band_lt.len() - 1], 94., epsilon = 0.03);
}
#[test]
fn test_slm_warmup_suppression() {
// Verify that run() returns None during warm-up, then Some with
// valid stats (possibly empty Lt) once warm-up completes.
const fs: Flt = 48e3;
let fsp = StrictlyPositive::new(fs).unwrap();
let desc = StandardFilterDescriptor::Overall().unwrap();
let settings = SLMSettingsBuilder::default()
.fs(fsp)
.timeWeighting(TimeWeighting::Fast {})
.freqWeighting(FreqWeighting::Z)
.filterDescriptors([desc])
.Lref(StrictlyPositive::new(2e-5).unwrap())
.decimate(false)
.build()
.unwrap();
let mut slm = SLM::new(settings, None);
let warmup_frames = slm.warmup_remaining;
assert!(warmup_frames > 0, "Should need warm-up");
// Feed half of warm-up — should still be None
let half = warmup_frames / 2;
let data = vec![0.0; half];
let res = slm.run(&data, true);
assert!(res.is_none(), "Should be None during warm-up (half)");
// Feed remaining warm-up — still None (entire block consumed by warm-up).
let remaining_warmup = warmup_frames - half;
let data2 = vec![0.0; remaining_warmup];
let res = slm.run(&data2, true);
assert!(
res.is_none(),
"Should be None: block fully consumed by warm-up"
);
assert!(slm.is_warm());
// Now feed extra data — Lt should have samples
let data3 = vec![0.0; 100];
let res = slm.run(&data3, true).expect("Should produce output");
assert_eq!(res.Lt.as_ref().unwrap()[&desc].len(), 100);
}
#[test]
fn test_slm_first_output_after_warmup() {
// Verify that the first Lt sample after warm-up is already at the
// expected level (~94 dB SPL), proving the time weighting filter
// has settled during warm-up. Uses Slow weighting for a harder test.
const fs: Flt = 48e3;
let fsp = StrictlyPositive::new(fs).unwrap();
let desc = StandardFilterDescriptor::Overall().unwrap();
let settings = SLMSettingsBuilder::default()
.fs(fsp)
.timeWeighting(TimeWeighting::Slow {})
.freqWeighting(FreqWeighting::A)
.filterDescriptors([desc])
.decimate(false)
.build()
.unwrap();
// Generate a 1 kHz sine at sqrt(2) amplitude → 94 dB SPL
let total_seconds = 5.0;
let N = (total_seconds * fs) as usize;
let mut siggen = Siggen::new(
1,
SourceDescriptor::Sine {
frequency: (1000.).try_into().unwrap(),
},
);
siggen.setAllMute(false);
siggen.reset(fsp).unwrap();
siggen.setAllGains(sq2.try_into().unwrap());
let mut data = vec![0.; N];
siggen.genSignal(&mut data);
let mut slm = SLM::new(settings, None);
// Feed warm-up: block exactly fills warm-up, returns None
let warmup = slm.warmup_remaining;
let res = slm.run(&data[..warmup], true);
assert!(res.is_none(), "Warm-up block fully consumed");
assert!(slm.is_warm());
// Feed remaining data — first Lt sample should already be ~94 dB
let res = slm.run(&data[warmup..], true).unwrap();
let band = &res.Lt.as_ref().unwrap()[&desc];
assert!(!band.is_empty());
assert_abs_diff_eq!(band[0], 94., epsilon = 0.3);
}
#[test]
fn test_slm_negative_initial_time() {
// Verify that run() suppresses output when initial_time is negative.
const fs: Flt = 48e3;
let fsp = StrictlyPositive::new(fs).unwrap();
let desc = StandardFilterDescriptor::Overall().unwrap();
let settings = SLMSettingsBuilder::default()
.fs(fsp)
.timeWeighting(TimeWeighting::Fast {})
.filterDescriptors([desc])
.decimate(false)
.build()
.unwrap();
// Start 1 second (48000 frames) before t = 0
let mut slm = SLM::new(settings, Some(-48000));
// Feed enough data to complete warm-up but still before t = 0
let warmup_frames = slm.warmup_remaining;
let block = vec![0.0; warmup_frames];
let res = slm.run(&block, true);
assert!(
res.is_none(),
"Should suppress: warm-up not complete and/or t <= 0"
);
// Feed data up to the first sample with t >= 0
let frames_to_t0 = (-slm.frame_ctr) as usize;
let block = vec![0.0; frames_to_t0];
let res = slm.run(&block, true);
assert!(
res.is_none(),
"Should still suppress: entire block consumed by t < 0"
);
// Now feed extra data in positive time
let block = vec![0.0; 100];
let res = slm.run(&block, true);
assert!(res.is_some(), "Should produce output after t >= 0");
assert!(slm.is_warm());
assert!(slm.frame_ctr > 0);
}
}