lasprs 0.14.1

Library for Acoustic Signal Processing (Rust edition, with optional Python bindings via pyo3)
use super::*;
use crate::measurement::*;
use crate::*;
use anyhow::{Result, anyhow};
use serde::{Deserialize, Serialize};
use snafu::prelude::*;
use std::{collections::HashMap, default, ops::Index, path::PathBuf, rc::Rc};

#[derive(Serialize, Default, Deserialize, Clone, Debug, PartialEq)]
#[cfg_attr(
    feature = "python-bindings",
    gen_stub_pyclass_enum,
    pyclass(eq, eq_int, from_py_object)
)]
pub enum CouplingMode {
    /// DC coupling
    #[default]
    DC,
    /// AC coupling
    AC,
    /// AC coupling with IEPE (constant current power supply)
    ACWithIEPE,
}

/// DAQ Configuration for a single channel
#[derive(Serialize, Deserialize, Clone, Debug)]
#[cfg_attr(
    feature = "python-bindings",
    gen_stub_pyclass,
    pyclass(get_all, set_all, from_py_object)
)]
pub struct DaqChannel {
    /// Whether the channel is enabled
    pub enabled: bool,

    /// Readable name for channel
    pub name: String,

    /// To convert to physical units. Divide values by this to obtain it.
    pub sensitivity: Flt,

    /// Enabled hardware AC coupling (if)
    pub couplingMode: CouplingMode,

    /// The configured range (minumum, maximum) value
    pub range: (Flt, Flt),

    /// Physical quantity
    pub qty: Qty,

    /// Apply digital highpass filter to remove D.C. before processing
    /// Value is in Hz. A value <= 0 means it is disabled.
    pub digitalHighpassCutOn: Option<Positive>,
}

impl PartialEq for DaqChannel {
    /// Custom implementation of PartialEq that only compares name and
    /// sensitivity
    fn eq(&self, other: &Self) -> bool {
        self.name == other.name && self.sensitivity == other.sensitivity && self.qty == other.qty
    }
}

impl Default for DaqChannel {
    fn default() -> Self {
        DaqChannel {
            enabled: false,
            name: "".into(),
            sensitivity: 1.0,
            couplingMode: Default::default(),
            range: (-1.0, 1.0),
            qty: Qty::Number,
            digitalHighpassCutOn: None,
        }
    }
}
impl DaqChannel {
    /// Default channel configuration for audio input from a certain channel
    pub fn defaultAudio<T: Into<String>>(name: T) -> Self {
        DaqChannel {
            enabled: true,
            name: name.into(),
            sensitivity: 1.0,
            couplingMode: CouplingMode::AC,
            range: (-1.0, 1.0),
            qty: Qty::Number,
            digitalHighpassCutOn: None,
        }
    }

    /// Create a vector of `DaqChannel` from separate slices of channel names,
    /// quantities, and sensitivities. Sets all other fields to default values.
    ///
    /// # Arguments
    ///
    /// * `nchannels` - Number of channels to create
    /// * `names` - Optional slice of channel names
    /// * `qtys` - Optional slice of channel quantities
    /// * `sensitivities` - Optional slice of channel sensitivities
    /// * `context` - Context string for error messages
    ///
    pub fn fromMultipleSeparateSlices(
        nchannels: usize,
        names: Option<&[&str]>,
        qtys: Option<&[Qty]>,
        sensitivities: Option<&[Flt]>,
        context: &str,
    ) -> std::result::Result<Vec<Self>, MeasurementError> {
        // Validate optional arguments against channel count
        let channel_names = if let Some(names) = names {
            ensure!(
                names.len() == nchannels,
                LogicSnafu {
                    name: context,
                    message: format!(
                        "Number of channel names ({}) does not match channel count ({nchannels})",
                        names.len()
                    )
                }
            );
            names.iter().map(|n| n.to_string()).collect::<Vec<_>>()
        } else {
            Vec::from_iter((0..nchannels).map(|i| format!("Unnamed input channel {i}")))
        };
        let sensitivities = if let Some(sens) = sensitivities {
            ensure!(
                sens.len() == nchannels,
                LogicSnafu {
                    name: context,
                    message: format!(
                        "Number of sensitivities ({}) must match channel count ({nchannels})",
                        sens.len()
                    )
                }
            );
            sens.to_vec()
        } else {
            Vec::from_iter((0..nchannels).map(|_| 1.0))
        };
        let quantities = if let Some(qty) = qtys {
            ensure!(
                qty.len() == nchannels,
                LogicSnafu {
                    name: context,
                    message: format!(
                        "Number of quantities ({}) must match channel count ({nchannels})",
                        qty.len()
                    )
                }
            );
            qty.to_vec()
        } else {
            Vec::from_iter((0..nchannels).map(|_| Qty::default()))
        };
        Ok(channel_names
            .into_iter()
            .zip(quantities.into_iter().zip(sensitivities))
            .map(|(name, (qty, sensitivity))| Self {
                name,
                qty,
                sensitivity,
                ..Self::default()
            })
            .collect::<Vec<_>>())
    }
}
#[cfg_attr(feature = "python-bindings", pymethods, gen_stub_pymethods)]
impl DaqChannel {
    #[cfg(feature = "python-bindings")]
    #[new]
    fn new() -> Self {
        Self::default()
    }
}

/// Configuration of a device.
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(
    feature = "python-bindings",
    gen_stub_pyclass,
    pyclass(get_all, set_all, from_py_object)
)]
pub struct DaqConfig {
    /// The API
    pub api: DaqApiDescriptor,

    /// Device name. Should match when starting a stream
    pub device_name: String,

    /// Configuration of the input channels
    pub inchannel_config: Vec<DaqChannel>,

    /// Configuration of the output channels
    pub outchannel_config: Vec<DaqChannel>,

    /// The data type to use
    pub dtype: DataType,

    /// The index to use in the list of possible sample rates
    pub sampleRateIndex: usize,

    /// The index to use in the list of possible frames per block
    pub framesPerBlockIndex: usize,

    /// Used when output channels should be monitored, i.e. reverse-looped back as input channels.
    pub monitorOutput: bool,
}
impl Eq for DaqConfig {}

#[cfg_attr(feature = "python-bindings", gen_stub_pymethods, pymethods)]
impl DaqConfig {
    #[cfg(feature = "python-bindings")]
    #[pyo3(name = "newFromDeviceInfo")]
    #[staticmethod]
    fn newFromDeviceInfo_py(d: &DeviceInfo) -> PyResult<DaqConfig> {
        Ok(DaqConfig::newFromDeviceInfo(d))
    }

    #[cfg(feature = "python-bindings")]
    fn __repr__(&self) -> String {
        format!("{self:#?}")
    }

    /// Returns the total number of channels that appear in a running input stream.
    pub fn numberEnabledInChannels(&self) -> usize {
        self.inchannel_config.iter().filter(|ch| ch.enabled).count()
    }
    /// Returns the total number of channels that appear in a running output stream.
    pub fn numberEnabledOutChannels(&self) -> usize {
        self.outchannel_config
            .iter()
            .filter(|ch| ch.enabled)
            .count()
    }
    /// Provide samplerate, based on device and specified sample rate index
    pub fn sampleRate(&self, dev: &DeviceInfo) -> Flt {
        *dev.avSampleRates.get(self.sampleRateIndex).unwrap()
    }

    /// Provide samplerate, based on device and specified sample rate index
    pub fn framesPerBlock(&self, dev: &DeviceInfo) -> usize {
        dev.avFramesPerBlock[self.framesPerBlockIndex]
    }
    /// Returns vec of channel configuration for enabled input channels only
    pub fn enabledInChannels(&self) -> Vec<DaqChannel> {
        self.inchannel_config
            .iter()
            .filter(|ch| ch.enabled)
            .cloned()
            .collect()
    }
    /// Returns a list of enabled input channel numbers as indices
    /// in the list of all input channels (enabled and not)
    pub fn enabledInchannelsList(&self) -> Vec<usize> {
        self.inchannel_config
            .iter()
            .enumerate()
            .filter(|(_, ch)| ch.enabled)
            .map(|(i, _)| i)
            .collect()
    }
    /// Returns vec of channel configuration for enabled output channels only
    pub fn enabledOutChannels(&self) -> Vec<DaqChannel> {
        self.outchannel_config
            .iter()
            .filter(|ch| ch.enabled)
            .cloned()
            .collect()
    }

    /// Returns the channel number of the highest enabled input channel, if any.
    pub fn highestEnabledInChannel(&self) -> Option<usize> {
        let mut highest = None;

        self.inchannel_config.iter().enumerate().for_each(|(i, c)| {
            if c.enabled {
                highest = Some(i);
            }
        });

        highest
    }
    /// Returns the channel number of the highest enabled output channel, if any.
    pub fn highestEnabledOutChannel(&self) -> Option<usize> {
        let mut highest = None;

        self.outchannel_config
            .iter()
            .enumerate()
            .for_each(|(i, c)| {
                if c.enabled {
                    highest = Some(i);
                }
            });

        highest
    }

    /// Change state of all output channels, enables them all, or disables them
    /// all.
    pub fn setAllOutputEnabled(&mut self, enabled: bool) {
        self.outchannel_config
            .iter_mut()
            .for_each(|ch| ch.enabled = enabled);
    }
}
impl DaqConfig {
    /// Creates a new default device configuration for a given device as specified with
    /// the DeviceInfo descriptor.
    pub fn newFromDeviceInfo(devinfo: &DeviceInfo) -> DaqConfig {
        let inchannel_config = (0..devinfo.iChannelCount)
            .map(|i| DaqChannel {
                name: format!("Unnamed input channel {i}"),
                ..Default::default()
            })
            .collect();
        let outchannel_config = (0..devinfo.oChannelCount)
            .map(|i| DaqChannel {
                name: format!("Unnamed output channel {i}"),
                ..Default::default()
            })
            .collect();

        let sampleRateIndex = devinfo
            .avSampleRates
            .iter()
            .position(|x| x == &devinfo.prefSampleRate)
            .unwrap_or(devinfo.avSampleRates.len() / 2);
        // Choose 4096 when in list, otherwise choose the highes available value in list
        let framesPerBlockIndex = devinfo
            .avFramesPerBlock
            .iter()
            .position(|x| x == &4096)
            .unwrap_or(devinfo.avFramesPerBlock.len() - 1);

        DaqConfig {
            api: devinfo.api.clone(),
            device_name: devinfo.device_name.clone(),
            inchannel_config,
            outchannel_config,
            dtype: devinfo.prefDataType,
            sampleRateIndex,
            framesPerBlockIndex,
            monitorOutput: false,
        }
    }

    /// Serialize DaqConfig object to TOML.
    ///
    /// Args
    ///
    /// * writer: Output writer, can be file or string, or anything that *is* std::io::Write
    ///
    pub fn serialize_TOML(&self, writer: &mut dyn std::io::Write) -> Result<()> {
        let ser_str = toml::to_string(&self)?;
        writer.write_all(ser_str.as_bytes())?;

        Ok(())
    }

    /// Deserialize structure from TOML data
    ///
    /// # Args
    ///
    /// * reader: implements the Read trait, from which we read the data.
    pub fn deserialize_TOML<T>(reader: &mut T) -> Result<DaqConfig>
    where
        T: std::io::Read,
    {
        let mut read_str = vec![];
        reader.read_to_end(&mut read_str)?;
        let read_str = String::from_utf8(read_str)?;
        DaqConfig::deserialize_TOML_str(&read_str)
    }

    /// Deserialize from TOML string
    ///
    /// # Args
    ///
    /// * st: string containing TOML data.
    pub fn deserialize_TOML_str(st: &str) -> Result<DaqConfig> {
        let res: DaqConfig = toml::from_str(st)?;
        Ok(res)
    }

    /// Write this configuration to a TOML file.
    ///
    /// Args
    ///
    /// * file: Name of file to write to
    ///
    pub fn serialize_TOML_file(&self, file: &PathBuf) -> Result<()> {
        let mut file = std::fs::File::create(file)?;
        self.serialize_TOML(&mut file)?;
        Ok(())
    }
}