diff --git a/.gitignore b/.gitignore index 745742c..155d90f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ __pycache__ python/lasprs/_lasprs* .venv +.vscode/launch.json diff --git a/Cargo.toml b/Cargo.toml index e80e094..5642d06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,24 +17,57 @@ name = "lasprs" crate-type = ["cdylib", "rlib"] [dependencies] +# Error handling anyhow = "1.0.75" +# Numerics # Optional future feature for ndarray: blas ndarray = { version = "0.15.3", features = ["rayon"] } num = "0.4.1" -rayon = "1.8.0" -numpy = { version = "0.20" } -strum_macros = "0.25.3" -pyo3 = { version = "0.20", features=["anyhow", "extension-module"]} -rand = "0.8.5" -rand_distr = "0.4.3" # blas-src = { version = "0.8", features = ["openblas"] } # openblas-src = { version = "0.10", features = ["cblas", "system"] } +# Parallel iterators +rayon = "1.8.0" + +# Python bindings +pyo3 = { version = "0.20", features=["anyhow", "extension-module"], optional=true } +numpy = { version = "0.20" } + +# White noise etc +rand = "0.8.5" +rand_distr = "0.4.3" + +# Cross-platform audio lib +cpal = { version = "0.15.2", optional=true } + +# Nice enumerations +strum = "0.25.0" +strum_macros = "0.25.3" + +# Conditional compilation enhancements +cfg-if = "1.0.0" + +# Reinterpret buffers. This is a workaround for the #[feature(specialize)] not +# being available in stable rust. +reinterpret = "0.2.1" + +# Faster channels for multithreaded communication +crossbeam = "0.8.2" + +# Serialization +serde = { version = "1.0.193", features = ["derive"] } +toml = "0.8.8" + +# Initialize array for non-copy type +array-init = "2.1.0" + [features] -default = ["f64"] +default = ["f64", "cpal_api"] # Use this for debugging extension # default = ["f64", "extension-module", "pyo3/extension-module"] +cpal_api = ["dep:cpal"] +# default = ["f64", "cpal_api"] f64 = [] f32 = [] -extension-module = ["pyo3/extension-module"] +extension-module = ["dep:pyo3", "pyo3/extension-module"] diff --git a/src/bin/test_input.rs b/src/bin/test_input.rs new file mode 100644 index 0000000..be7d5bc --- /dev/null +++ b/src/bin/test_input.rs @@ -0,0 +1,14 @@ +use lasprs::daq::StreamMgr; +use anyhow::Result; +use std::io; + +fn main() -> Result<()> { + + let mut smgr = StreamMgr::new(); + + smgr.startDefaultInputStream()?; + + let _ = io::stdin().read_line(&mut (String::new())); + + Ok(()) +} diff --git a/src/config.rs b/src/config.rs index 2832ba0..533e19b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,7 +9,11 @@ pub type Flt = f64; #[cfg(feature = "f64")] pub const pi: Flt = std::f64::consts::PI; +/// The maximum number of input channels allowed. Compile time constant to make some structs Copy. +pub const MAX_INPUT_CHANNELS: usize = 128; + use num::complex::*; +/// Complex number floating point pub type Cflt = Complex; use numpy::ndarray::{Array1, Array2}; diff --git a/src/daq/api/api_cpal.rs b/src/daq/api/api_cpal.rs new file mode 100644 index 0000000..7099423 --- /dev/null +++ b/src/daq/api/api_cpal.rs @@ -0,0 +1,109 @@ +use super::Stream; +use crate::daq::deviceinfo::DeviceInfo; +use crate::daq::streammsg::*; +use anyhow::{bail, Result}; +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use cpal::{Device, Host, Sample, SampleFormat}; +use crossbeam::channel::Sender; +use std::sync::Arc; + +/// Cpal api +pub struct CpalApi { + host: cpal::Host, +} +impl Stream for cpal::Stream {} + +impl CpalApi { + pub fn new() -> CpalApi { + CpalApi { + host: cpal::default_host(), + } + } + pub fn getDeviceInfo(&self) -> Result> { + let devs = vec![]; + for dev in self.host.devices()? { + + } + Ok(devs) + } + + fn build_input_stream( + sf: cpal::SampleFormat, + config: cpal::StreamConfig, + device: &cpal::Device, + sender: Sender, + ) -> Result { + + let sender_errcallback = sender.clone(); + + let errfn = move |err: cpal::StreamError| match err { + cpal::StreamError::DeviceNotAvailable => sender_errcallback + .send(RawStreamData::StreamError(StreamError::DeviceNotAvailable)) + .unwrap(), + cpal::StreamError::BackendSpecific { err: _ } => sender_errcallback + .send(RawStreamData::StreamError(StreamError::DriverError)) + .unwrap(), + }; + + macro_rules! build_stream{ + ($($cpaltype:pat, $rtype:ty);*) => { + match sf { + $( + $cpaltype => device.build_input_stream( + &config, + move |data, _: &_| InStreamCallback::<$rtype>(data, &sender), + errfn, + None)? + ),*, + _ => bail!("Unsupported sample format '{}'", sf) + } + } + } + let stream: cpal::Stream = build_stream!( + SampleFormat::I8, i8; + SampleFormat::I16, i16; + SampleFormat::I32, i32; + SampleFormat::F32, f32 + ); + Ok(stream) + } + /// Start a default input stream + /// + /// + pub fn startDefaultInputStream( + &mut self, + sender: Sender, + ) -> Result> { + if let Some(device) = self.host.default_input_device() { + if let Ok(config) = device.default_input_config() { + let final_config = cpal::StreamConfig { + channels: config.channels(), + sample_rate: config.sample_rate(), + buffer_size: cpal::BufferSize::Fixed(4096), + }; + + let sf = config.sample_format(); + let stream = CpalApi::build_input_stream(sf, final_config, &device, sender)?; + stream.play()?; + println!("Stream started with sample format {:?}", sf); + + Ok(Box::new(stream)) + } else { + bail!("Could not obtain default input configuration") + } + } else { + bail!("Could not open default input device") + } + } + // pub fn getDeviceInfo(&self) -> Result> { + + // } +} + +fn InStreamCallback(input: &[T], sender: &Sender) +where + T: Copy + num::ToPrimitive + 'static, +{ + let msg = RawStreamData::from(input); + sender.send(msg).unwrap() +} diff --git a/src/daq/api/mod.rs b/src/daq/api/mod.rs new file mode 100644 index 0000000..d2cddd5 --- /dev/null +++ b/src/daq/api/mod.rs @@ -0,0 +1,24 @@ +/// Daq apis that are optionally compiled in. Examples: +/// +/// - CPAL (Cross-Platform Audio Library) +/// - ... +use strum::EnumMessage; +use strum_macros; +use serde::{Serialize, Deserialize}; + +cfg_if::cfg_if! { + if #[cfg(feature="cpal_api")] { + pub mod api_cpal; + } else { } +} + +/// A currently running stream +pub trait Stream { } + +#[derive(strum_macros::EnumMessage, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[allow(dead_code)] +pub enum StreamApiDescr { + /// CPAL api + #[strum(message = "Cpal", detailed_message = "Cross-Platform Audio Library")] + Cpal = 0, +} diff --git a/src/daq/deviceinfo.rs b/src/daq/deviceinfo.rs new file mode 100644 index 0000000..a8e86e1 --- /dev/null +++ b/src/daq/deviceinfo.rs @@ -0,0 +1,67 @@ +//! Data acquisition model. Provides abstract layers around DAQ devices. +#![allow(non_snake_case)] + +use super::datatype::DataType; +use super::qty::Qty; +use super::api::StreamApiDescr; + +/// Device info structure. Gives all information regarding a device, i.e. the number of input and +/// output channels, its name and available sample rates and types. +#[derive(Clone, Debug)] +#[allow(dead_code)] +pub struct DeviceInfo { + + /// The api in use for this device + pub api: StreamApiDescr, + + /// Name for the device. + pub name: String, + + /// Available data types for the sample + pub avDataTypes: Vec, + /// Preferred data type for device + pub prefDataType: DataType, + + /// Available frames per block + pub avFramesPerBlock: Vec, + /// Preferred frames per block for device + pub prefFramesPerBlock: u16, + + /// Available sample rates + pub avSampleRates: Vec, + /// Preferred sample rate for device + pub prefSampleRate: u16, + + /// Number of input channels available for this device + pub iChannelCount: u8, + /// Number of output channels available for this device + pub oChannelCount: u8, + + /// Whether the device is capable to provide IEPE constant current power supply. + pub hasInputIEPE: bool, + + /// Whether the device is capable of enabling a hardware AC-coupling + pub hasInputACCouplingSwitch: bool, + + ///Whether the device is able to trigger on input + pub hasInputTrigger: bool, + + /// Whether the device has an internal monitor of the output signal. If + /// true, the device is able to monitor output signals internally and able to + /// present output signals as virtual input signals. This only works together + /// Daq's that are able to run in full duplex mode. + pub hasInternalOutputMonitor: bool, + + /// This flag is used to be able to indicate that the device cannot run + /// input and output streams independently, without opening the device in + /// duplex mode. This is for example true for the UlDaq: only one handle to + /// the device can be given at the same time. + pub duplexModeForced: bool, + + /// The physical quantity of the output signal. For 'normal' audio + /// devices, this is typically a 'number' between +/- full scale. For some + /// devices however, the output quantity corresponds to a physical signal, + /// such a Volts. + pub physicalIOQty: Qty, +} + diff --git a/src/daq/mod.rs b/src/daq/mod.rs new file mode 100644 index 0000000..fbcdaf3 --- /dev/null +++ b/src/daq/mod.rs @@ -0,0 +1,227 @@ +//! Data acquisition model. Provides abstract layers around DAQ devices. + +mod api; +mod daqconfig; +mod datatype; +mod deviceinfo; +mod qty; +mod streammsg; + +pub use datatype::*; +pub use deviceinfo::*; +pub use qty::*; +pub use streammsg::*; + +#[cfg(feature = "cpal_api")] +use api::api_cpal::CpalApi; + +use crate::config::*; +use anyhow::{bail, Error, Result}; +use api::Stream; +use core::time; +use crossbeam::{ + channel::{unbounded, Receiver, Sender}, + thread, +}; +use deviceinfo::DeviceInfo; +use std::sync::{atomic::AtomicBool, Arc, Mutex}; +use std::thread::{JoinHandle, Thread}; +use streammsg::*; + +/// Keep track of whether the stream has been created. To ensure singleton behaviour. +static smgr_created: AtomicBool = AtomicBool::new(false); + +struct InputStream { + streamtype: StreamType, + stream: Box, + threadhandle: JoinHandle, + comm: Sender, +} + +/// Configure and manage input / output streams. +/// +pub struct StreamMgr { + // Input stream can be both input and duplex + input_stream: Option, + + // Output only stream + output_stream: Option>, + + // Signal generator + siggen: Option, + + #[cfg(feature = "cpal_api")] + cpal_api: CpalApi, + + /// The storage of queues. When no streams are running, they + /// are here. When stream is running, they will become available + /// in the JoinHandle of the thread. + instreamqueues: Option, +} + +impl StreamMgr { + /// Create new stream manager. A stream manager is supposed to be a singleton. + /// + /// # Panics + /// + /// When a StreamMgr object is already alive. + pub fn new() -> StreamMgr { + if smgr_created.load(std::sync::atomic::Ordering::Relaxed) { + panic!("BUG: Only one stream manager is supposed to be a singleton"); + } + smgr_created.store(true, std::sync::atomic::Ordering::Relaxed); + + StreamMgr { + input_stream: None, + output_stream: None, + siggen: None, + + #[cfg(feature = "cpal_api")] + cpal_api: CpalApi::new(), + + instreamqueues: Some(vec![]), + } + } + /// Obtain a list of devices that are available for each available API + fn getDeviceInfo(&mut self) -> Vec { + let mut devinfo = vec![]; + #[cfg(feature="cpal_api")] + devinfo.extend(self.cpal_api.getDeviceInfo()); + devinfo + } + + /// Start a default input stream, using default settings on everything. This is only possible + /// when + pub fn startDefaultInputStream(&mut self) -> Result<()> { + #![allow(unreachable_code)] + if !self.input_stream.is_none() { + bail!("Input stream is already running. Please first stop existing input stream.") + } + + let (tx, rx): (Sender, Receiver) = unbounded(); + + cfg_if::cfg_if! { + if #[cfg(feature="cpal_api")] { + let stream = self.cpal_api.startDefaultInputStream(tx)?; + } + else { + bail!("Unable to start default input stream: no CPAL api available") + } + } + + // Unwrap here, as the queues should be free to grab + let mut iqueues = self.instreamqueues.take().unwrap(); + + let (commtx, commrx) = unbounded(); + + // let metadata = StreamMetaData::new( + // nchannels: + // ).unwrap(); + let threadhandle = std::thread::spawn(move || { + let mut ctr: usize = 0; + 'infy: loop { + if let Ok(comm_msg) = commrx.try_recv() { + match comm_msg { + // New queue added + StreamCommand::AddInQueue(queue) => { + iqueues.push(queue); + // queue.send(streammsg::StreamMetaData(md)) + } + + // Remove queue from list + StreamCommand::RemoveInQueue(queue) => { + iqueues.retain(|q| !Arc::ptr_eq(q, &queue)) + } + + // Stop this thread. Returns the queue + StreamCommand::StopThread => { + for q in iqueues.iter() { + q.send(InStreamMsg::StreamStopped).unwrap(); + } + break 'infy; + } + } + } + if let Ok(msg) = rx.recv_timeout(time::Duration::from_millis(10)) { + // println!("Obtained raw stream data!"); + let msg = Arc::new(msg); + for q in iqueues.iter() { + q.send(InStreamMsg::RawStreamData(ctr, msg.clone())) + .unwrap(); + } + } + ctr += 1; + } + iqueues + }); + + cfg_if::cfg_if! { + if #[cfg(feature="cpal_api")] { + self.input_stream = Some(InputStream { + streamtype: StreamType::Input, + stream, + threadhandle, + comm: commtx, + }); + } else {} + } + + Ok(()) + } + + /// Stop existing input stream. + pub fn stopInputStream(&mut self) -> Result<()> { + if let Some(InputStream { + streamtype: _, // Ignored here + stream: _, + threadhandle, + comm, + }) = self.input_stream.take() + { + // println!("Stopping existing stream.."); + // Send thread to stop + comm.send(StreamCommand::StopThread).unwrap(); + + // Store stream queues back into StreamMgr + self.instreamqueues = Some(threadhandle.join().expect("Stream thread panicked!")); + } else { + bail!("Stream is not running.") + } + Ok(()) + } + /// Stop existing running stream. + /// + /// Args + /// + /// * st: The stream type. + pub fn stopStream(&mut self, st: StreamType) -> Result<()> { + match st { + StreamType::Input | StreamType::Duplex => self.stopInputStream(), + _ => bail!("Not implemented output stream"), + } + } + +} // impl StreamMgr +impl Drop for StreamMgr { + fn drop(&mut self) { + // Kill input stream if there is one + if self.input_stream.is_some() { + self.stopStream(StreamType::Input).unwrap(); + } + if self.output_stream.is_some() { + self.stopStream(StreamType::Output).unwrap(); + } + + // Decref the singleton + smgr_created.store(false, std::sync::atomic::Ordering::Relaxed); + } +} + +/// Daq devices +trait Daq {} + +#[cfg(test)] +mod tests { + + // #[test] +} diff --git a/src/daq/streammsg.rs b/src/daq/streammsg.rs new file mode 100644 index 0000000..b6c0ae1 --- /dev/null +++ b/src/daq/streammsg.rs @@ -0,0 +1,183 @@ +//! Provides stream messages that come from a running stream +use crate::config::*; +use crate::daq::DataType; +use crate::daq::Qty; +use anyhow::{bail, Result}; +use crossbeam::channel::Sender; +use reinterpret::reinterpret_slice; +use std::any::TypeId; +use std::sync::Arc; +use std::u128::MAX; + +use super::daqconfig::DaqChannel; + +/// Raw stream data coming from a stream. +#[derive(Clone)] +pub enum RawStreamData { + /// 8-bits integer + Datai8(Arc>), + /// 16-bits integer + Datai16(Arc>), + /// 32-bits integer + Datai32(Arc>), + /// 32-bits float + Dataf32(Arc>), + /// 64-bits float + Dataf64(Arc>), + + /// Unknown data type. We cannot do anything with it, we could instead also create an error, although this is easier to pass downstream. + UnknownDataType, + + /// A stream error occured + StreamError(StreamError), +} + +// Create InStreamData object from +impl From<&[T]> for RawStreamData +where + T: num::ToPrimitive + Clone + 'static, +{ + fn from(input: &[T]) -> RawStreamData { + // Apparently, this code does not work with a match. I have searched around and have not found the + // reason for this. So this is a bit of stupid boilerplate. + let i8type: TypeId = TypeId::of::(); + let i16type: TypeId = TypeId::of::(); + let i32type: TypeId = TypeId::of::(); + let f32type: TypeId = TypeId::of::(); + let f64type: TypeId = TypeId::of::(); + let thetype: TypeId = TypeId::of::(); + if i8type == thetype { + let v: Vec = unsafe { reinterpret_slice(input).to_vec() }; + RawStreamData::Datai8(Arc::new(v)) + } else if i16type == thetype { + let v: Vec = unsafe { reinterpret_slice(input).to_vec() }; + RawStreamData::Datai16(Arc::new(v)) + } else if i16type == thetype { + let v: Vec = unsafe { reinterpret_slice(input).to_vec() }; + RawStreamData::Datai16(Arc::new(v)) + } else if i32type == thetype { + let v: Vec = unsafe { reinterpret_slice(input).to_vec() }; + RawStreamData::Datai32(Arc::new(v)) + } else if f32type == thetype { + let v: Vec = unsafe { reinterpret_slice(input).to_vec() }; + RawStreamData::Dataf32(Arc::new(v)) + } else if f64type == thetype { + let v: Vec = unsafe { reinterpret_slice(input).to_vec() }; + RawStreamData::Dataf64(Arc::new(v)) + } else { + panic!("Not implemented sample type!") + } + } +} + +/// Stream metadata. All information required for +pub struct StreamMetaData { + /// The number of channels. Should be <= MAX_INPUT_CHANNELS + pub nchannels: usize, + + /// Information for each channel in the stream + pub channelInfo: [DaqChannel; MAX_INPUT_CHANNELS], + + /// The data type of the device [Number / voltage] + pub rawDatatype: DataType, + + /// Sample rate in [Hz] + pub samplerate: Flt, +} +impl StreamMetaData { + /// Create new metadata object. Throws an error if the number of channels for the sensitivity and quantities does + /// not match. + /// + /// # Args + /// + /// * nchannels: The number of channels that are send + /// * sens: Sensitivity values for each channel + /// * rawdtype: The data type of the raw stream data. For sound cards this is Number, for DAQ's, this might be a voltage. + /// * qtys_: The physical quantities for each channel + /// * sr: The sample rate in \[Hz\] + /// + /// # Panics + /// + /// If the number of channels > MAX_INPUT_CHANNELS + pub fn new(channel_data: &[DaqChannel], rawdtype: DataType, sr: Flt) -> Result { + if channel_data.len() > MAX_INPUT_CHANNELS { + bail!("Too many channels provided.") + } + let nchannels = channel_data.len(); + let channelInfo: [DaqChannel; MAX_INPUT_CHANNELS] = + array_init::array_init(|_i: usize| DaqChannel::default()); + + Ok(StreamMetaData { + nchannels, + channelInfo, + rawDatatype: rawdtype, + samplerate: sr, + }) + } +} +/// Input stream messages, to be send to handlers. +#[derive(Clone)] +pub enum InStreamMsg { + /// Raw stream data that is coming from a device. This is interleaved data. The number of channels is correct and + /// specified in the stream metadata. + RawStreamData(usize, Arc), + + /// An error has occured in the stream + StreamError(StreamError), + + /// Stream data converted to floating point with sample width as + /// compiled in. + ConvertedStreamData(usize, Arc), + + /// new Stream metadata enters the scene. Probably a new stream started. + StreamStarted(Arc), + + /// An existing stream stopped. + StreamStopped, +} + +/// Store a queue in a shared pointer, to share sending +/// and receiving part of the queue. +pub type SharedInQueue = Arc>; +/// Vector of queues for stream messages +pub type InQueues = Vec; + +/// Commands that can be sent to a running stream +pub enum StreamCommand { + /// Add a new queue to a running stream + AddInQueue(SharedInQueue), + /// Remove a queue to a running stream + RemoveInQueue(SharedInQueue), + + /// Stop the thread, do not listen for data anymore. + StopThread, +} + +/// Stream types that can be started +/// +pub enum StreamType { + /// Input-only stream + Input, + /// Output-only stream + Output, + /// Input and output at the same time + Duplex, +} + +/// Errors that happen in a stream +#[derive(strum_macros::EnumMessage, Debug, Clone)] +pub enum StreamError { + /// Input overrun + #[strum(message = "InputXRunError", detailed_message = "Input buffer overrun")] + InputXRunError, + /// Output underrun + #[strum(message = "OutputXRunError", detailed_message = "Output buffer overrun")] + OutputXRunError, + /// Driver specific error + #[strum(message = "DriverError", detailed_message = "Driver error")] + DriverError, + + /// Device + #[strum(detailed_message = "Device not available")] + DeviceNotAvailable +} diff --git a/src/filter.rs b/src/filter.rs index f606bca..3380cc9 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -5,12 +5,17 @@ #![allow(non_snake_case)] use super::config::*; use anyhow::{bail, Result}; +use cfg_if::cfg_if; use numpy::ndarray::{ArrayD, ArrayViewD, ArrayViewMutD}; use numpy::{IntoPyArray, PyArray1, PyArrayDyn, PyArrayLike1, PyReadonlyArrayDyn}; +use rayon::prelude::*; + +cfg_if! { +if #[cfg(feature = "extension-module")] { use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use pyo3::{pymodule, types::PyModule, PyResult}; -use rayon::prelude::*; +} else {} } pub trait Filter: Send { //! The filter trait is implemented by Biquad, SeriesBiquad, and BiquadBank @@ -170,7 +175,7 @@ impl Filter for Biquad { /// /// # Examples /// -/// See [tests] +/// See (tests) /// ``` #[derive(Clone, Debug)] #[cfg_attr(feature = "extension-module", pyclass)] @@ -359,7 +364,6 @@ impl BiquadBank { } self.set_gains_dB(gains_dB.as_slice()?); Ok(()) - } #[pyo3(name = "len")] /// See: [BiquadBank::len()] diff --git a/src/lib.rs b/src/lib.rs index 45852ee..dea7cff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,30 +3,32 @@ //! This crate contains structures and functions to perform acoustic measurements, interact with //! data acquisition devices and apply common acoustic analysis operations on them. - #![warn(missing_docs)] - #![allow(non_snake_case)] - #![allow(non_upper_case_globals)] - #![allow(unused_imports)] +#![warn(missing_docs)] +#![allow(non_snake_case)] - mod config; - pub mod filter; - // pub mod window; - // pub mod ps; - pub mod siggen; +#![allow(non_upper_case_globals)] +#![allow(unused_imports)] - extern crate pyo3; - #[cfg(feature = "extension-module")] - use pyo3::prelude::*; +mod config; +pub mod filter; - /// A Python module implemented in Rust. - #[cfg(feature = "extension-module")] - #[pymodule] - #[pyo3(name="_lasprs")] - fn lasprs(py: Python, m: &PyModule) -> PyResult<()> { +// pub mod window; +// pub mod ps; +pub mod daq; +pub mod siggen; - pyo3_add_submodule_filter(py, &m)?; - Ok(()) - } +#[cfg(feature = "extension-module")] +use pyo3::prelude::*; + +/// A Python module implemented in Rust. +#[cfg(feature = "extension-module")] +#[pymodule] +#[pyo3(name="_lasprs")] +fn lasprs(py: Python, m: &PyModule) -> PyResult<()> { + + pyo3_add_submodule_filter(py, &m)?; + Ok(()) +} /// Add filter submodule to extension #[cfg(feature = "extension-module")] diff --git a/src/siggen.rs b/src/siggen.rs index 65a0423..d0bbf85 100644 --- a/src/siggen.rs +++ b/src/siggen.rs @@ -5,6 +5,7 @@ //! ## Create some white noise and print it. //! //! ``` +//! use lasprs::siggen::Siggen; //! let mut wn = Siggen::newWhiteNoise(); //! wn.setGain(0.1); //! wn.setMute(false); @@ -15,12 +16,14 @@ //! ``` use super::config::*; use super::filter::Filter; +#[cfg(feature="extension-module")] use pyo3::prelude::*; use rand::prelude::*; use rand::rngs::ThreadRng; use rand_distr::StandardNormal; -trait Source: Send { +/// Source for the signal generator. Implementations are sine waves, sweeps, noise. +pub trait Source: Send { /// Generate the 'pure' source signal. Output is placed inside the `sig` argument. fn genSignal_unscaled(&mut self, sig: &mut [Flt]); /// Reset the source state, i.e. set phase to 0, etc @@ -104,10 +107,14 @@ impl Source for Sine { } } -/// Sweep signal #[derive(Clone)] -/// Signal generator. Able to create acoustic output signals +/// Signal generator. Able to create acoustic output signals. See above example on how to use. +/// Typical signal that can be created are: +/// +/// * (Siggen::newWhiteNoise) +/// * (Siggen::newSine) +/// pub struct Siggen { // The source dynamic signal. Noise, a sine wave, sweep, etc source: Box, @@ -138,7 +145,7 @@ impl Siggen { } /// Create a sine wave signal generator /// - /// * freq: Frequency of the sine wave in [Hz] + /// * freq: Frequency of the sine wave in \[Hz\] pub fn newSineWave(freq: Flt) -> Siggen { Siggen::new(Box::new(Sine::new(freq))) } @@ -191,7 +198,7 @@ impl Siggen { /// /// Args /// - /// * fs: (New) Sampling frequency [Hz] + /// * fs: (New) Sampling frequency \[Hz\] /// pub fn reset(&mut self, fs: Flt) { self.source.reset(fs); @@ -199,6 +206,11 @@ impl Siggen { f.reset(); } } + + /// Set mut on signal generator. If true, only DC signal offset is outputed from (Sigen::genSignal). + pub fn setMute(&mut self, mute: bool) { + self.muted = mute + } } #[cfg(test)] @@ -207,8 +219,19 @@ mod test { #[test] fn test_whitenoise() { - let mut t = &[0.; 10]; - Siggen::newWiteNoise().genSignal(&mut t); + // This code is just to check syntax. We should really be listening to these outputs. + let mut t = [0.; 10]; + Siggen::newWhiteNoise().genSignal(&mut t); println!("{:?}", &t); } + + #[test] + fn test_sine() { + // This code is just to check syntax. We should really be listening to these outputs. + let mut s = [0.; 9]; + let mut siggen = Siggen::newSineWave(1.); + siggen.reset(1.); + siggen.genSignal(&mut s); + println!("{:?}", &s); + } }