use crate::config::*;
use crate::daq::{InStreamMsg, StreamDirection, StreamMetaData, StreamMgr};
use crate::slm::{SLM, SLMResult, SLMSettings};
use anyhow::{Result, anyhow, ensure};
use crossbeam::channel::{Receiver, Sender, unbounded};
use std::sync::Arc;
use std::time::Duration;
use triple_buffer::triple_buffer;
#[derive(Debug)]
enum RtSlmMessage {
StopThread,
ResetStatus,
}
#[derive(Debug, Clone)]
pub enum RtSlmResult {
NewResult(SLMResult),
NewMeta(Arc<StreamMetaData>),
Error(String),
}
#[cfg_attr(feature = "python-bindings", gen_stub_pyclass, pyclass)]
#[derive(Debug)]
pub struct RtSlm {
status: triple_buffer::Output<Option<RtSlmResult>>,
sender: Sender<RtSlmMessage>,
}
impl RtSlm {
pub fn new(mgr: &mut StreamMgr, settings: SLMSettings, channel: usize) -> Result<RtSlm> {
let meta = mgr
.getStreamMetaData(StreamDirection::Input)
.ok_or_else(|| anyhow!("Cannot create RtSlm: no input stream is running."))?;
ensure!(
channel < meta.nchannels(),
"Channel index {} out of bounds: input stream has {} channels.",
channel,
meta.nchannels()
);
let mut settings = settings;
settings.fs = meta.samplerate;
let slm = SLM::new(settings, None);
let (status_tx, status_rx) = triple_buffer(&None);
let (sender, rxmsg) = unbounded();
Self::startThread(slm, channel, status_tx, mgr, rxmsg);
Ok(RtSlm {
status: status_rx,
sender,
})
}
fn startThread(
mut slm: SLM,
channel: usize,
mut status_tx: triple_buffer::Input<Option<RtSlmResult>>,
smgr: &mut StreamMgr,
rxmsg: Receiver<RtSlmMessage>,
) {
let (tx, rxstream) = unbounded();
smgr.addInQueue(tx);
rayon::spawn(move || {
let mut channel_valid = true;
let mut last_result: Option<SLMResult> = None;
let mut pending_error: Option<String> = None;
'mainloop: loop {
if let Ok(msg) = rxstream.recv_timeout(Duration::from_millis(10)) {
match msg {
InStreamMsg::StreamStarted(new_meta, _) => {
channel_valid = channel < new_meta.nchannels();
if channel_valid {
slm.reset_stats();
status_tx.write(Some(RtSlmResult::NewMeta(new_meta)));
} else {
pending_error = Some(format!(
"Channel index {} out of bounds: input stream has {} channels.",
channel,
new_meta.nchannels()
));
}
}
InStreamMsg::StreamStopped | InStreamMsg::StreamError(_) => {
break 'mainloop;
}
InStreamMsg::InStreamData(id) => {
if channel_valid {
let flt = id.getFloatData(true);
let td = flt.column(channel).to_owned();
last_result = slm.run(td.as_slice().unwrap(), true);
}
}
}
}
if let Ok(msg) = rxmsg.recv_timeout(Duration::from_millis(1)) {
match msg {
RtSlmMessage::StopThread => {
break 'mainloop;
}
RtSlmMessage::ResetStatus => {
slm.reset_stats();
}
}
}
if let Some(result) = last_result.take() {
status_tx.write(Some(RtSlmResult::NewResult(result)));
}
if let Some(error) = pending_error.take() {
status_tx.write(Some(RtSlmResult::Error(error)));
}
} });
}
pub fn get_last(&mut self) -> Option<RtSlmResult> {
self.status.read().clone()
}
pub fn reset(&self) {
let _ = self.sender.send(RtSlmMessage::ResetStatus);
}
}
impl Drop for RtSlm {
fn drop(&mut self) {
let _ = self.sender.send(RtSlmMessage::StopThread);
}
}
#[cfg(feature = "python-bindings")]
#[cfg_attr(feature = "python-bindings", gen_stub_pymethods, pymethods)]
impl RtSlm {
#[new]
#[gen_stub(skip)]
fn new_py(smgr: &mut StreamMgr, settings: SLMSettings, channel: usize) -> PyResult<Self> {
Ok(RtSlm::new(smgr, settings, channel)?)
}
#[pyo3(name = "get_last")]
fn get_last_py(&mut self) -> Option<SLMResult> {
match self.get_last() {
Some(RtSlmResult::NewResult(res)) => Some(res),
_ => None,
}
}
#[pyo3(name = "reset")]
fn reset_py(&self) {
self.reset()
}
}
cfg_select! {
feature = "python-bindings" => {
pyo3_stub_gen::inventory::submit! {
gen_methods_from_python! {
r#"
class RtSlm:
def __new__(smgr: StreamMgr, settings: SLMSettings, channel: int) -> RtSlm:
"""Create a new Real time Sound Level Meter for a single channel of the
input stream. Raises when no input stream is running, or when the
channel index is out of bounds."""
"#
}
}
},
_ => {}
}
#[cfg(test)]
mod test {
use super::*;
use crate::{
FreqWeighting, StrictlyPositive, TimeWeighting,
daq::{DaqApiDescriptor, DaqChannel, DaqConfig, DataType, StreamType},
filter::StandardFilterDescriptor,
slm::SLMSettingsBuilder,
};
use std::thread;
#[cfg(feature = "loopback-api")]
#[test]
fn test_rtslm1() -> Result<()> {
let mut smgr = StreamMgr::new_with_devices();
let cfg = DaqConfig {
api: DaqApiDescriptor::Loopback,
device_name: "Loopback".into(),
inchannel_config: vec![
DaqChannel::defaultAudio("ch0"),
DaqChannel::defaultAudio("ch1"),
],
outchannel_config: vec![],
dtype: DataType::F32,
sampleRateIndex: 0,
framesPerBlockIndex: 0,
monitorOutput: false,
};
smgr.startStream(StreamType::Input, &cfg)?;
let meta = smgr
.getStreamMetaData(StreamDirection::Input)
.ok_or_else(|| anyhow!("Stream is not running"))?;
let desc = StandardFilterDescriptor::Overall().unwrap();
let settings = SLMSettingsBuilder::default()
.fs(meta.samplerate)
.timeWeighting(TimeWeighting::Fast {})
.freqWeighting(FreqWeighting::Z)
.filterDescriptors([desc])
.Lref(StrictlyPositive::new(2e-5).unwrap())
.build()
.unwrap();
assert!(
RtSlm::new(&mut smgr, settings.clone(), 5).is_err(),
"Out-of-bounds channel should error"
);
let mut rtslm = RtSlm::new(&mut smgr, settings, 0)?;
let mut got_result = false;
for _ in 0..50 {
if let Some(RtSlmResult::NewResult(_)) = rtslm.get_last() {
got_result = true;
break;
}
thread::sleep(Duration::from_millis(100));
}
assert!(got_result, "Should receive an SLM result within 5 seconds");
drop(rtslm);
Ok(())
}
}