use super::*;
use crate::filter::StandardFilterDescriptor;
use crate::slm::{SLM, SLMResult, SLMSettings};
use anyhow::{Result, anyhow};
use std::collections::BTreeMap;
impl SharedMeasurement {
pub fn SLM(
&self,
settings: SLMSettings,
channel: usize,
istart: Option<usize>,
istop: Option<usize>,
only_stats: bool,
) -> Result<SLMResult> {
let meas = self.read();
let nframes = meas.nframes();
let istart = istart.unwrap_or(0);
let istop = istop.unwrap_or(nframes);
let total_precap_frames = meas.precapture_nframes() as i64;
let warmup_time = settings.timeWeighting.warmupTime();
let ideal_warmpup_frames = (warmup_time.as_secs_f64() * *meas.samplerate()) as i64;
let ideal_initial_frame = istart as i64 - ideal_warmpup_frames;
let initial_frame = (ideal_initial_frame).max(-total_precap_frames);
let mut slm = SLM::new(settings, Some(initial_frame));
let mut lt_accum: Option<BTreeMap<_, _>> = None;
let mut t_accum: Vec<Flt> = Vec::new();
let mut last: Option<SLMResult> = None;
let live_iter = meas.data_iter(
Some(&[channel]),
Some(initial_frame.max(0) as usize),
Some(istop),
)?;
let precap_iter = if initial_frame < 0 {
meas.precapture_iter(
Some(&[channel]),
Some((total_precap_frames + initial_frame) as usize),
None,
)?
} else {
None
};
for block in precap_iter.into_iter().flatten().chain(live_iter) {
debug_assert!(block.ncols() == 1, "Invalid number of columns");
let td = block.column(0);
let td = td.as_slice().expect("converted block must be contiguous");
if let Some(result) = slm.run(td, !only_stats) {
last = Some(result.clone());
if let Some(lt) = &result.Lt {
let acc = lt_accum
.get_or_insert_with(|| lt.keys().map(|k| (*k, Vec::new())).collect());
for (desc, band) in lt {
acc.get_mut(desc)
.expect("accumulator exists for descriptor")
.extend_from_slice(band);
}
}
if let Some(t) = &result.t {
t_accum.extend(t.iter());
}
}
}
let mut result = last.ok_or_else(|| {
anyhow!(
"Not enough data for the SLM to produce valid output with the \
chosen settings (warm-up never completed)."
)
})?;
result.Lt = if only_stats { None } else { lt_accum };
result.t = if t_accum.is_empty() {
None
} else {
Some(t_accum)
};
Ok(result)
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::daq::RecordStatus;
use crate::daq::{DaqChannel, DaqConfig, RecordSettings, Recording, StreamMgr, StreamType};
use crate::filter::StandardFilterDescriptor;
use crate::measurement::MeasurementType;
use crate::siggen::{SiggenCommand, SourceDescriptor};
use crate::slm::SLMSettingsBuilder;
use crate::{FreqWeighting, TimeWeighting};
use approx::assert_abs_diff_eq;
use std::time::Duration;
#[cfg(feature = "loopback-api")]
#[test]
fn test_slm_on_measurement() -> anyhow::Result<()> {
let tmpdir = tempfile::tempdir()?;
let mut smgr = StreamMgr::new_with_devices();
let in_cfg = DaqConfig {
api: crate::daq::DaqApiDescriptor::Loopback,
device_name: "Loopback".into(),
inchannel_config: vec![
DaqChannel::defaultAudio("ch0"),
DaqChannel::defaultAudio("ch1"),
],
outchannel_config: vec![],
dtype: crate::daq::DataType::F32,
sampleRateIndex: 0,
framesPerBlockIndex: 0,
monitorOutput: false,
};
let out_cfg = {
let mut cfg = in_cfg.clone();
cfg.inchannel_config = vec![];
cfg.outchannel_config = vec![
DaqChannel::defaultAudio("ch0"),
DaqChannel::defaultAudio("ch1"),
];
cfg
};
smgr.startStream(StreamType::Input, &in_cfg)?;
smgr.startStream(StreamType::Output, &out_cfg)?;
smgr.setSiggenSource(SourceDescriptor::Sine {
frequency: (1000.).try_into().unwrap(),
})?;
smgr.siggenCommand(SiggenCommand::SetMuteAllChannels { mute: false })?;
smgr.siggenCommand(SiggenCommand::SetAllGains {
g: sq2.try_into().unwrap(),
})?;
let duration = Duration::from_secs(3);
let rec_settings = RecordSettings::new(
"slm_test",
false,
Some(duration),
None,
None,
Some(MeasurementType::NotSpecific {}),
false,
Some(tmpdir.path()),
vec![],
)?;
let mut recording = Recording::new(rec_settings, &mut smgr)?;
loop {
use crate::daq::RecordStatus;
if matches!(recording.status(), RecordStatus::Finished { .. }) {
break;
}
std::thread::sleep(Duration::from_millis(10));
}
recording.stop();
let m = recording.getMeasurement().unwrap();
drop(recording);
drop(smgr);
let fs = *m.read().samplerate();
let fsp = StrictlyPositive::new(fs)?;
let desc = StandardFilterDescriptor::Overall().unwrap();
let slm_settings = SLMSettingsBuilder::default()
.fs(fsp)
.timeWeighting(TimeWeighting::Fast {})
.freqWeighting(FreqWeighting::Z)
.filterDescriptors([desc])
.Lref(StrictlyPositive::new(2e-5).unwrap())
.build()
.unwrap();
let result = m.SLM(slm_settings.clone(), 0, None, None, true).unwrap();
assert_abs_diff_eq!(result.Lmax[&desc], 94.0, epsilon = 0.03);
assert_abs_diff_eq!(result.Lpk[&desc], 96.99, epsilon = 0.001);
assert_abs_diff_eq!(result.Leq[&desc], 93.98, epsilon = 0.002);
assert!(
result.Lt.is_none(),
"Lt should be None when only_stats=true"
);
assert!(result.t.is_none());
let result2 = m.SLM(slm_settings, 0, None, None, false).unwrap();
assert_abs_diff_eq!(result2.Lmax[&desc], 94.0, epsilon = 0.03);
assert_abs_diff_eq!(result2.Lpk[&desc], 96.99, epsilon = 0.001);
assert_abs_diff_eq!(result2.Leq[&desc], 93.98, epsilon = 0.002);
let lt = result2.Lt.as_ref().expect("Lt should be present");
let band_lt = lt.get(&desc).expect("descriptor should be in Lt");
assert!(!band_lt.is_empty(), "Should have time points");
let time = result2.t.as_ref().expect("time axis should be present");
assert_eq!(time.len(), band_lt.len());
Ok(())
}
#[cfg(feature = "loopback-api")]
#[test]
fn test_slm_on_measurement_with_precapture() -> anyhow::Result<()> {
use crate::config::PRECAP_DURATION;
let tmpdir = tempfile::tempdir()?;
let mut smgr = StreamMgr::new_with_devices();
let in_cfg = DaqConfig {
api: crate::daq::DaqApiDescriptor::Loopback,
device_name: "Loopback".into(),
inchannel_config: vec![
DaqChannel::defaultAudio("ch0"),
DaqChannel::defaultAudio("ch1"),
],
outchannel_config: vec![],
dtype: crate::daq::DataType::F32,
sampleRateIndex: 0,
framesPerBlockIndex: 0,
monitorOutput: false,
};
let out_cfg = {
let mut cfg = in_cfg.clone();
cfg.inchannel_config = vec![];
cfg.outchannel_config = vec![
DaqChannel::defaultAudio("ch0"),
DaqChannel::defaultAudio("ch1"),
];
cfg
};
smgr.startStream(StreamType::Input, &in_cfg)?;
smgr.startStream(StreamType::Output, &out_cfg)?;
smgr.setSiggenSource(SourceDescriptor::Sine {
frequency: (1000.).try_into().unwrap(),
})?;
smgr.siggenCommand(SiggenCommand::SetMuteAllChannels { mute: false })?;
smgr.siggenCommand(SiggenCommand::SetAllGains {
g: sq2.try_into().unwrap(),
})?;
std::thread::sleep(PRECAP_DURATION + Duration::from_secs(1));
let duration = Duration::from_secs(2);
let rec_settings = RecordSettings::new(
"slm_precap_test",
false,
Some(duration),
None,
None,
Some(MeasurementType::SoundLevels {}),
false,
Some(tmpdir.path()),
vec![],
)?;
let mut recording = Recording::new(rec_settings, &mut smgr)?;
loop {
if matches!(recording.status(), RecordStatus::Finished { .. }) {
break;
}
std::thread::sleep(Duration::from_millis(10));
}
recording.stop();
let m = recording.getMeasurement().unwrap();
drop(recording);
drop(smgr);
let precap_n = m.read().precapture_nframes();
assert!(precap_n > 0, "Measurement should have pre-capture data");
let fs = *m.read().samplerate();
let fsp = StrictlyPositive::new(fs)?;
let desc = StandardFilterDescriptor::Overall().unwrap();
let slm_settings = SLMSettingsBuilder::default()
.fs(fsp)
.timeWeighting(TimeWeighting::Fast {})
.freqWeighting(FreqWeighting::Z)
.filterDescriptors([desc])
.build()
.unwrap();
let result = m.SLM(slm_settings, 0, None, None, false).unwrap();
assert_abs_diff_eq!(result.Lmax[&desc], 94.0, epsilon = 0.03);
assert_abs_diff_eq!(result.Lpk[&desc], 96.99, epsilon = 0.001);
assert_abs_diff_eq!(result.Leq[&desc], 93.98, epsilon = 0.002);
let lt = result.Lt.as_ref().expect("Lt should be present");
let band_lt = lt.get(&desc).expect("descriptor should be in Lt");
assert!(!band_lt.is_empty(), "Should have time points");
let time = result.t.as_ref().expect("time axis should be present");
assert_eq!(time.len(), band_lt.len());
assert!(time[0] < 0.1, "First output should start near t=0");
assert_abs_diff_eq!(*band_lt.last().unwrap(), 93.98, epsilon = 0.005);
Ok(())
}
}