First work on CPAL api. Input default seems to work. This is not yet production code!

This commit is contained in:
Anne de Jong 2023-12-13 11:02:06 +01:00
parent 92a66f4f20
commit 3984989873
12 changed files with 729 additions and 38 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@
__pycache__ __pycache__
python/lasprs/_lasprs* python/lasprs/_lasprs*
.venv .venv
.vscode/launch.json

View File

@ -17,24 +17,57 @@ name = "lasprs"
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
[dependencies] [dependencies]
# Error handling
anyhow = "1.0.75" anyhow = "1.0.75"
# Numerics
# Optional future feature for ndarray: blas # Optional future feature for ndarray: blas
ndarray = { version = "0.15.3", features = ["rayon"] } ndarray = { version = "0.15.3", features = ["rayon"] }
num = "0.4.1" 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"] } # blas-src = { version = "0.8", features = ["openblas"] }
# openblas-src = { version = "0.10", features = ["cblas", "system"] } # 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] [features]
default = ["f64"] default = ["f64", "cpal_api"]
# Use this for debugging extension # Use this for debugging extension
# default = ["f64", "extension-module", "pyo3/extension-module"] # default = ["f64", "extension-module", "pyo3/extension-module"]
cpal_api = ["dep:cpal"]
# default = ["f64", "cpal_api"]
f64 = [] f64 = []
f32 = [] f32 = []
extension-module = ["pyo3/extension-module"] extension-module = ["dep:pyo3", "pyo3/extension-module"]

14
src/bin/test_input.rs Normal file
View File

@ -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(())
}

View File

@ -9,7 +9,11 @@ pub type Flt = f64;
#[cfg(feature = "f64")] #[cfg(feature = "f64")]
pub const pi: Flt = std::f64::consts::PI; 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::*; use num::complex::*;
/// Complex number floating point
pub type Cflt = Complex<Flt>; pub type Cflt = Complex<Flt>;
use numpy::ndarray::{Array1, Array2}; use numpy::ndarray::{Array1, Array2};

109
src/daq/api/api_cpal.rs Normal file
View File

@ -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<Vec<DeviceInfo>> {
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<RawStreamData>,
) -> Result<cpal::Stream> {
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<RawStreamData>,
) -> Result<Box<dyn Stream>> {
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<Vec<DeviceInfo>> {
// }
}
fn InStreamCallback<T>(input: &[T], sender: &Sender<RawStreamData>)
where
T: Copy + num::ToPrimitive + 'static,
{
let msg = RawStreamData::from(input);
sender.send(msg).unwrap()
}

24
src/daq/api/mod.rs Normal file
View File

@ -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,
}

67
src/daq/deviceinfo.rs Normal file
View File

@ -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<DataType>,
/// Preferred data type for device
pub prefDataType: DataType,
/// Available frames per block
pub avFramesPerBlock: Vec<u16>,
/// Preferred frames per block for device
pub prefFramesPerBlock: u16,
/// Available sample rates
pub avSampleRates: Vec<u16>,
/// 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,
}

227
src/daq/mod.rs Normal file
View File

@ -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<dyn Stream>,
threadhandle: JoinHandle<InQueues>,
comm: Sender<StreamCommand>,
}
/// Configure and manage input / output streams.
///
pub struct StreamMgr {
// Input stream can be both input and duplex
input_stream: Option<InputStream>,
// Output only stream
output_stream: Option<Box<dyn Stream>>,
// Signal generator
siggen: Option<crate::siggen::Siggen>,
#[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<InQueues>,
}
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<DeviceInfo> {
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<RawStreamData>, Receiver<RawStreamData>) = 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]
}

183
src/daq/streammsg.rs Normal file
View File

@ -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<Vec<i8>>),
/// 16-bits integer
Datai16(Arc<Vec<i16>>),
/// 32-bits integer
Datai32(Arc<Vec<i32>>),
/// 32-bits float
Dataf32(Arc<Vec<f32>>),
/// 64-bits float
Dataf64(Arc<Vec<f64>>),
/// 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<T> 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::<i8>();
let i16type: TypeId = TypeId::of::<i16>();
let i32type: TypeId = TypeId::of::<i32>();
let f32type: TypeId = TypeId::of::<f32>();
let f64type: TypeId = TypeId::of::<f64>();
let thetype: TypeId = TypeId::of::<T>();
if i8type == thetype {
let v: Vec<i8> = unsafe { reinterpret_slice(input).to_vec() };
RawStreamData::Datai8(Arc::new(v))
} else if i16type == thetype {
let v: Vec<i16> = unsafe { reinterpret_slice(input).to_vec() };
RawStreamData::Datai16(Arc::new(v))
} else if i16type == thetype {
let v: Vec<i16> = unsafe { reinterpret_slice(input).to_vec() };
RawStreamData::Datai16(Arc::new(v))
} else if i32type == thetype {
let v: Vec<i32> = unsafe { reinterpret_slice(input).to_vec() };
RawStreamData::Datai32(Arc::new(v))
} else if f32type == thetype {
let v: Vec<f32> = unsafe { reinterpret_slice(input).to_vec() };
RawStreamData::Dataf32(Arc::new(v))
} else if f64type == thetype {
let v: Vec<f64> = 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<StreamMetaData> {
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<RawStreamData>),
/// An error has occured in the stream
StreamError(StreamError),
/// Stream data converted to floating point with sample width as
/// compiled in.
ConvertedStreamData(usize, Arc<crate::config::Dmat>),
/// new Stream metadata enters the scene. Probably a new stream started.
StreamStarted(Arc<StreamMetaData>),
/// 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<Sender<InStreamMsg>>;
/// Vector of queues for stream messages
pub type InQueues = Vec<SharedInQueue>;
/// 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
}

View File

@ -5,12 +5,17 @@
#![allow(non_snake_case)] #![allow(non_snake_case)]
use super::config::*; use super::config::*;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use cfg_if::cfg_if;
use numpy::ndarray::{ArrayD, ArrayViewD, ArrayViewMutD}; use numpy::ndarray::{ArrayD, ArrayViewD, ArrayViewMutD};
use numpy::{IntoPyArray, PyArray1, PyArrayDyn, PyArrayLike1, PyReadonlyArrayDyn}; use numpy::{IntoPyArray, PyArray1, PyArrayDyn, PyArrayLike1, PyReadonlyArrayDyn};
use rayon::prelude::*;
cfg_if! {
if #[cfg(feature = "extension-module")] {
use pyo3::exceptions::PyValueError; use pyo3::exceptions::PyValueError;
use pyo3::prelude::*; use pyo3::prelude::*;
use pyo3::{pymodule, types::PyModule, PyResult}; use pyo3::{pymodule, types::PyModule, PyResult};
use rayon::prelude::*; } else {} }
pub trait Filter: Send { pub trait Filter: Send {
//! The filter trait is implemented by Biquad, SeriesBiquad, and BiquadBank //! The filter trait is implemented by Biquad, SeriesBiquad, and BiquadBank
@ -170,7 +175,7 @@ impl Filter for Biquad {
/// ///
/// # Examples /// # Examples
/// ///
/// See [tests] /// See (tests)
/// ``` /// ```
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[cfg_attr(feature = "extension-module", pyclass)] #[cfg_attr(feature = "extension-module", pyclass)]
@ -359,7 +364,6 @@ impl BiquadBank {
} }
self.set_gains_dB(gains_dB.as_slice()?); self.set_gains_dB(gains_dB.as_slice()?);
Ok(()) Ok(())
} }
#[pyo3(name = "len")] #[pyo3(name = "len")]
/// See: [BiquadBank::len()] /// See: [BiquadBank::len()]

View File

@ -3,30 +3,32 @@
//! This crate contains structures and functions to perform acoustic measurements, interact with //! This crate contains structures and functions to perform acoustic measurements, interact with
//! data acquisition devices and apply common acoustic analysis operations on them. //! data acquisition devices and apply common acoustic analysis operations on them.
#![warn(missing_docs)] #![warn(missing_docs)]
#![allow(non_snake_case)] #![allow(non_snake_case)]
#![allow(non_upper_case_globals)]
#![allow(unused_imports)]
mod config; #![allow(non_upper_case_globals)]
pub mod filter; #![allow(unused_imports)]
// pub mod window;
// pub mod ps;
pub mod siggen;
extern crate pyo3; mod config;
#[cfg(feature = "extension-module")] pub mod filter;
use pyo3::prelude::*;
/// A Python module implemented in Rust. // pub mod window;
#[cfg(feature = "extension-module")] // pub mod ps;
#[pymodule] pub mod daq;
#[pyo3(name="_lasprs")] pub mod siggen;
fn lasprs(py: Python, m: &PyModule) -> PyResult<()> {
#[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)?; pyo3_add_submodule_filter(py, &m)?;
Ok(()) Ok(())
} }
/// Add filter submodule to extension /// Add filter submodule to extension
#[cfg(feature = "extension-module")] #[cfg(feature = "extension-module")]

View File

@ -5,6 +5,7 @@
//! ## Create some white noise and print it. //! ## Create some white noise and print it.
//! //!
//! ``` //! ```
//! use lasprs::siggen::Siggen;
//! let mut wn = Siggen::newWhiteNoise(); //! let mut wn = Siggen::newWhiteNoise();
//! wn.setGain(0.1); //! wn.setGain(0.1);
//! wn.setMute(false); //! wn.setMute(false);
@ -15,12 +16,14 @@
//! ``` //! ```
use super::config::*; use super::config::*;
use super::filter::Filter; use super::filter::Filter;
#[cfg(feature="extension-module")]
use pyo3::prelude::*; use pyo3::prelude::*;
use rand::prelude::*; use rand::prelude::*;
use rand::rngs::ThreadRng; use rand::rngs::ThreadRng;
use rand_distr::StandardNormal; 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. /// Generate the 'pure' source signal. Output is placed inside the `sig` argument.
fn genSignal_unscaled(&mut self, sig: &mut [Flt]); fn genSignal_unscaled(&mut self, sig: &mut [Flt]);
/// Reset the source state, i.e. set phase to 0, etc /// Reset the source state, i.e. set phase to 0, etc
@ -104,10 +107,14 @@ impl Source for Sine {
} }
} }
/// Sweep signal
#[derive(Clone)] #[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 { pub struct Siggen {
// The source dynamic signal. Noise, a sine wave, sweep, etc // The source dynamic signal. Noise, a sine wave, sweep, etc
source: Box<dyn Source>, source: Box<dyn Source>,
@ -138,7 +145,7 @@ impl Siggen {
} }
/// Create a sine wave signal generator /// 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 { pub fn newSineWave(freq: Flt) -> Siggen {
Siggen::new(Box::new(Sine::new(freq))) Siggen::new(Box::new(Sine::new(freq)))
} }
@ -191,7 +198,7 @@ impl Siggen {
/// ///
/// Args /// Args
/// ///
/// * fs: (New) Sampling frequency [Hz] /// * fs: (New) Sampling frequency \[Hz\]
/// ///
pub fn reset(&mut self, fs: Flt) { pub fn reset(&mut self, fs: Flt) {
self.source.reset(fs); self.source.reset(fs);
@ -199,6 +206,11 @@ impl Siggen {
f.reset(); 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)] #[cfg(test)]
@ -207,8 +219,19 @@ mod test {
#[test] #[test]
fn test_whitenoise() { fn test_whitenoise() {
let mut t = &[0.; 10]; // This code is just to check syntax. We should really be listening to these outputs.
Siggen::newWiteNoise().genSignal(&mut t); let mut t = [0.; 10];
Siggen::newWhiteNoise().genSignal(&mut t);
println!("{:?}", &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);
}
} }