Compare commits

..

No commits in common. "636213c2b7c784f295b09c7282a6706af5ca1eda" and "58093dd5cd2edfac521821bf243610ca7c023cb9" have entirely different histories.

34 changed files with 617 additions and 1316 deletions

1
.gitignore vendored
View File

@ -6,4 +6,3 @@ python/lasprs/_lasprs*
.vscode/launch.json .vscode/launch.json
.vscode .vscode
examples_py/.ipynb_checkpoints examples_py/.ipynb_checkpoints
.ipynb_checkpoints

View File

@ -1,6 +1,6 @@
[package] [package]
name = "lasprs" name = "lasprs"
version = "0.6.4" version = "0.6.3"
edition = "2021" edition = "2021"
authors = ["J.A. de Jong <j.a.dejong@ascee.nl>"] authors = ["J.A. de Jong <j.a.dejong@ascee.nl>"]
description = "Library for Acoustic Signal Processing (Rust edition, with optional Python bindings via pyo3)" description = "Library for Acoustic Signal Processing (Rust edition, with optional Python bindings via pyo3)"
@ -16,15 +16,11 @@ crate-type = ["cdylib", "rlib"]
[dependencies] [dependencies]
# Error handling # Error handling
anyhow = "1.0.91" anyhow = "1.0.86"
# Numerics # Numerics
# Optional future feature for ndarray: blas # Optional future feature for ndarray: blas
ndarray = { version = "0.16.1", features = ["rayon"] } ndarray = { version = "0.15.6", features = ["rayon"] }
# This is required for HDF5, as it apparently doesn't update anymore.
ndarray15p6 = { package = "ndarray", version = "0.15.6", features = ["rayon"] }
num = "0.4.3" num = "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"] }
@ -33,15 +29,15 @@ num = "0.4.3"
rayon = "1.10.0" rayon = "1.10.0"
# Python bindings # Python bindings
pyo3 = { version = "0.22.5", optional = true, features = [ pyo3 = { version = "0.21.2", optional = true, features = [
"extension-module", "extension-module",
"anyhow", "anyhow",
] } ] }
# Python bindings for Numpy arrays # Python bindings for Numpy arrays
numpy = { version = "0.22.0", optional = true } numpy = { version = "0.21.0", optional = true }
# White noise etc # White noise etc
rand = { version = "0.8.5", features = ["small_rng"] } rand = "0.8.5"
rand_distr = "0.4.3" rand_distr = "0.4.3"
# Cross-platform audio lib # Cross-platform audio lib
@ -109,6 +105,9 @@ smallvec = "1.13.2"
# Compile time constant floating point operations # Compile time constant floating point operations
softfloat = "1.0.0" softfloat = "1.0.0"
[dev-dependencies]
ndarray-rand = "0.14.0"
[features] [features]
default = ["f64", "cpal-api", "record"] default = ["f64", "cpal-api", "record"]
# Use this to test if everything works well in f32 # Use this to test if everything works well in f32

View File

@ -30,13 +30,13 @@ fn main() -> Result<()> {
let stdin_channel = stdin_channel_wait_for_return(); let stdin_channel = stdin_channel_wait_for_return();
println!("Creating signal generator..."); println!("Creating signal generator...");
let mut siggen = Siggen::newSine(1., 2, 432.0).unwrap(); let mut siggen = Siggen::newSine(2, 432.0);
// Reduce all gains a bit... // Reduce all gains a bit...
siggen.setAllGains(0.1); siggen.setAllGains(0.1);
// Apply signal generator // Apply signal generator
smgr.setSiggen(siggen)?; smgr.setSiggen(siggen);
println!("Starting stream..."); println!("Starting stream...");
let devs = smgr.getDeviceInfo(); let devs = smgr.getDeviceInfo();

View File

@ -1,13 +1,14 @@
use anyhow::Result; use anyhow::Result;
use crossbeam::channel::{unbounded, Receiver, TryRecvError}; use crossbeam::channel::{unbounded, Receiver, TryRecvError};
use lasprs::daq::{StreamMgr, StreamStatus, StreamType}; use lasprs::daq::{StreamMgr, StreamStatus, StreamType};
use lasprs::siggen::{Siggen, SweepType}; use lasprs::siggen::Siggen;
use std::io; use std::io;
use std::{thread, time}; use std::{thread, time};
// use // use
/// Spawns a thread and waits for a single line, pushes it to the receiver and returns /// Spawns a thread and waits for a single line, pushes it to the receiver and returns
fn stdin_channel_wait_for_return() -> Receiver<String> { fn stdin_channel_wait_for_return() -> Receiver<String> {
let (tx, rx) = unbounded(); let (tx, rx) = unbounded();
thread::spawn(move || { thread::spawn(move || {
let mut buffer = String::new(); let mut buffer = String::new();
@ -27,19 +28,7 @@ fn main() -> Result<()> {
let stdin_channel = stdin_channel_wait_for_return(); let stdin_channel = stdin_channel_wait_for_return();
println!("Creating signal generator..."); println!("Creating signal generator...");
// let mut siggen = Siggen::newSine(44100., 2, 432.)?; let mut siggen = Siggen::newSine(2, 432.);
let mut siggen = Siggen::newSweep(
44100.,
1, // nchannels: usize,
100., // fl: Flt,
10000., //fu: Flt,
1.0, // sweep_time: Flt,
// 1.0, //quiet_time: Flt,
0., //quiet_time: Flt,
// SweepType::ForwardLin//sweep_type: SweepType,
// SweepType::ForwardLog, //sweep_type: SweepType,
SweepType::ContinuousLog, //sweep_type: SweepType,
)?;
// Some things that can be done // Some things that can be done
// siggen.setDCOffset(&[0.1, 0.]); // siggen.setDCOffset(&[0.1, 0.]);
@ -47,11 +36,12 @@ fn main() -> Result<()> {
// Reduce all gains a bit... // Reduce all gains a bit...
siggen.setAllGains(0.1); siggen.setAllGains(0.1);
println!("Starting stream..."); println!("Starting stream...");
smgr.startDefaultOutputStream()?; smgr.startDefaultOutputStream()?;
// Apply signal generator // Apply signal generator
smgr.setSiggen(siggen)?; smgr.setSiggen(siggen);
println!("Press <enter> key to quit..."); println!("Press <enter> key to quit...");
'infy: loop { 'infy: loop {

View File

@ -32,7 +32,10 @@ pub trait Stream {
} }
/// Stream API descriptor: type and corresponding text /// Stream API descriptor: type and corresponding text
#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))] // Do the following when Pyo3 0.22 can finally be used combined with rust-numpy:
//#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
// For now:
#[cfg_attr(feature = "python-bindings", pyclass)]
#[derive(strum_macros::EnumMessage, Debug, Clone, PartialEq, Serialize, Deserialize, strum_macros::Display)] #[derive(strum_macros::EnumMessage, Debug, Clone, PartialEq, Serialize, Deserialize, strum_macros::Display)]
#[allow(dead_code)] #[allow(dead_code)]
pub enum StreamApiDescr { pub enum StreamApiDescr {

View File

@ -7,7 +7,10 @@ use crate::config::*;
/// Data type description for samples coming from a stream /// Data type description for samples coming from a stream
#[derive(strum_macros::EnumMessage, PartialEq, Copy, Debug, Clone, Serialize, Deserialize)] #[derive(strum_macros::EnumMessage, PartialEq, Copy, Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))] // Do the following when Pyo3 0.22 can finally be used combined with rust-numpy:
//#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
// For now:
#[cfg_attr(feature = "python-bindings", pyclass)]
#[allow(dead_code)] #[allow(dead_code)]
pub enum DataType { pub enum DataType {
/// 32-bit floats /// 32-bit floats

View File

@ -50,7 +50,10 @@ use api::*;
use crate::config::*; use crate::config::*;
/// Stream types that can be started /// Stream types that can be started
/// ///
#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))] // Do the following when Pyo3 0.22 can finally be used combined with rust-numpy:
// #[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
// For now:
#[cfg_attr(feature = "python-bindings", pyclass)]
#[derive(PartialEq, Clone, Copy)] #[derive(PartialEq, Clone, Copy)]
pub enum StreamType { pub enum StreamType {
/// Input-only stream /// Input-only stream

View File

@ -7,7 +7,9 @@ use strum_macros;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
/// Physical quantities that are I/O of a Daq device. /// Physical quantities that are I/O of a Daq device.
#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))] // Do the following when Pyo3 0.22 can finally be used combined with rust-numpy:
// #[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
#[cfg_attr(feature = "python-bindings", pyclass)]
#[derive(PartialEq, Serialize, Deserialize, strum_macros::EnumMessage, Debug, Clone, Copy)] #[derive(PartialEq, Serialize, Deserialize, strum_macros::EnumMessage, Debug, Clone, Copy)]
#[allow(dead_code)] #[allow(dead_code)]
pub enum Qty { pub enum Qty {

View File

@ -161,32 +161,28 @@ impl Recording {
nchannels: usize, nchannels: usize,
) -> Result<()> { ) -> Result<()> {
match data.getRaw() { match data.getRaw() {
// The code below uses ndarray 0.15.6, which is the version required
// to communicate with rust-hdf5. It requires input to be C-ordered,
// or interleaved. This happens to be the default for ndarray as
// well.
RawStreamData::Datai8(dat) => { RawStreamData::Datai8(dat) => {
let arr = ndarray15p6::ArrayView2::<i8>::from_shape((framesPerBlock, nchannels), dat)?; let arr = ndarray::ArrayView2::<i8>::from_shape((framesPerBlock, nchannels), dat)?;
ds.write_slice(arr, (ctr, .., ..))?; ds.write_slice(arr, (ctr, .., ..))?;
} }
RawStreamData::Datai16(dat) => { RawStreamData::Datai16(dat) => {
let arr = let arr =
ndarray15p6::ArrayView2::<i16>::from_shape((framesPerBlock, nchannels), dat)?; ndarray::ArrayView2::<i16>::from_shape((framesPerBlock, nchannels), dat)?;
ds.write_slice(arr, (ctr, .., ..))?; ds.write_slice(arr, (ctr, .., ..))?;
} }
RawStreamData::Datai32(dat) => { RawStreamData::Datai32(dat) => {
let arr = let arr =
ndarray15p6::ArrayView2::<i32>::from_shape((framesPerBlock, nchannels), dat)?; ndarray::ArrayView2::<i32>::from_shape((framesPerBlock, nchannels), dat)?;
ds.write_slice(arr, (ctr, .., ..))?; ds.write_slice(arr, (ctr, .., ..))?;
} }
RawStreamData::Dataf32(dat) => { RawStreamData::Dataf32(dat) => {
let arr = let arr =
ndarray15p6::ArrayView2::<f32>::from_shape((framesPerBlock, nchannels), dat)?; ndarray::ArrayView2::<f32>::from_shape((framesPerBlock, nchannels), dat)?;
ds.write_slice(arr, (ctr, .., ..))?; ds.write_slice(arr, (ctr, .., ..))?;
} }
RawStreamData::Dataf64(dat) => { RawStreamData::Dataf64(dat) => {
let arr = let arr =
ndarray15p6::ArrayView2::<f64>::from_shape((framesPerBlock, nchannels), dat)?; ndarray::ArrayView2::<f64>::from_shape((framesPerBlock, nchannels), dat)?;
ds.write_slice(arr, (ctr, .., ..))?; ds.write_slice(arr, (ctr, .., ..))?;
} }
} }

View File

@ -3,7 +3,6 @@ use super::streammgr::SharedInQueue;
/// Commands that can be sent to a running stream /// Commands that can be sent to a running stream
#[derive(Debug)]
pub enum StreamCommand { pub enum StreamCommand {
/// Add a new queue to a running INPUT stream /// Add a new queue to a running INPUT stream
AddInQueue(SharedInQueue), AddInQueue(SharedInQueue),
@ -11,9 +10,6 @@ pub enum StreamCommand {
/// New signal generator config to be used in OUTPUT stream /// New signal generator config to be used in OUTPUT stream
NewSiggen(Siggen), NewSiggen(Siggen),
/// Apply command to the signal generator.
SiggenCommand(SiggenCommand),
/// Stop the thread, do not listen for data anymore. /// Stop the thread, do not listen for data anymore.
StopThread, StopThread,

View File

@ -307,7 +307,7 @@ mod test {
const Nframes: usize = 20; const Nframes: usize = 20;
const Nch: usize = 2; const Nch: usize = 2;
let mut signal = [0.; Nch * Nframes]; let mut signal = [0.; Nch * Nframes];
let mut siggen = Siggen::newSine(fs, Nch, 1.).unwrap(); let mut siggen = Siggen::newSine(Nch, 1.);
siggen.reset(fs); siggen.reset(fs);
siggen.setMute(&[false, true]); siggen.setMute(&[false, true]);

View File

@ -2,8 +2,13 @@ use strum_macros::Display;
use crate::config::*; use crate::config::*;
/// Errors that happen in a stream /// Errors that happen in a stream
#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
#[derive(strum_macros::EnumMessage, PartialEq, Debug, Clone, Display, Copy)] #[derive(strum_macros::EnumMessage, PartialEq, Debug, Clone, Display, Copy)]
// Do the following when Pyo3 0.22 can finally be used combined with rust-numpy:
//#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
// For now:
#[cfg_attr(feature = "python-bindings", pyclass)]
pub enum StreamError { pub enum StreamError {
/// Input overrun /// Input overrun
#[strum( #[strum(

View File

@ -66,7 +66,7 @@ impl StreamMetaData {
#[cfg_attr(feature = "python-bindings", pymethods)] #[cfg_attr(feature = "python-bindings", pymethods)]
impl StreamMetaData { impl StreamMetaData {
#[getter] #[getter]
fn channelInfo(&self) -> Vec<DaqChannel> { pub fn channelInfo(&self) -> Vec<DaqChannel> {
self.channelInfo.clone() self.channelInfo.clone()
} }
#[getter] #[getter]

View File

@ -2,9 +2,9 @@
use super::*; use super::*;
use crate::{ use crate::{
config::*, config::*,
siggen::{self, Siggen, SiggenCommand}, siggen::{self, Siggen},
}; };
use anyhow::{anyhow, bail, Error, Result}; use anyhow::{bail, Error, Result};
use api::StreamApiDescr; use api::StreamApiDescr;
use array_init::from_iter; use array_init::from_iter;
use core::time; use core::time;
@ -13,11 +13,8 @@ use crossbeam::{
channel::{unbounded, Receiver, Sender, TrySendError}, channel::{unbounded, Receiver, Sender, TrySendError},
thread, thread,
}; };
use std::sync::{atomic::AtomicBool, Arc, Mutex};
use std::thread::{JoinHandle, Thread}; use std::thread::{JoinHandle, Thread};
use std::{
sync::{atomic::AtomicBool, Arc, Mutex},
time::Duration,
};
use streamcmd::StreamCommand; use streamcmd::StreamCommand;
use streamdata::*; use streamdata::*;
use streammetadata::*; use streammetadata::*;
@ -36,8 +33,7 @@ struct StreamInfo<T> {
streamtype: StreamType, streamtype: StreamType,
stream: Box<dyn Stream>, stream: Box<dyn Stream>,
threadhandle: JoinHandle<T>, threadhandle: JoinHandle<T>,
commtx: Sender<StreamCommand>, comm: Sender<StreamCommand>,
commrx: Receiver<Result<()>>,
} }
/// Keep track of whether the stream has been created. To ensure singleton behaviour. /// Keep track of whether the stream has been created. To ensure singleton behaviour.
@ -112,9 +108,8 @@ impl StreamMgr {
self.getStatus(st) self.getStatus(st)
} }
#[pyo3(name = "setSiggen")] #[pyo3(name = "setSiggen")]
fn setSiggen_py(&mut self, siggen: Siggen) -> PyResult<()> { fn setSiggen_py(&mut self, siggen: Siggen) {
self.setSiggen(siggen)?; self.setSiggen(siggen)
Ok(())
} }
#[pyo3(name = "getStreamMetaData")] #[pyo3(name = "getStreamMetaData")]
fn getStreamMetaData_py(&self, st: StreamType) -> Option<StreamMetaData> { fn getStreamMetaData_py(&self, st: StreamType) -> Option<StreamMetaData> {
@ -122,13 +117,7 @@ impl StreamMgr {
// value (not the Arc) has to be cloned. // value (not the Arc) has to be cloned.
self.getStreamMetaData(st).map(|b| (*b).clone()) self.getStreamMetaData(st).map(|b| (*b).clone())
} }
#[pyo3(name = "siggenCommand")]
fn siggenCommand_py(&mut self, cmd: SiggenCommand) -> PyResult<()> {
self.siggenCommand(cmd)?;
Ok(())
} }
}
impl Default for StreamMgr { impl Default for StreamMgr {
fn default() -> Self { fn default() -> Self {
Self::new() Self::new()
@ -154,7 +143,7 @@ impl StreamMgr {
devs: vec![], devs: vec![],
input_stream: None, input_stream: None,
output_stream: None, output_stream: None,
siggen: Some(Siggen::newSilence(1., 1)), siggen: None,
#[cfg(feature = "cpal-api")] #[cfg(feature = "cpal-api")]
cpal_api: CpalApi::new(), cpal_api: CpalApi::new(),
@ -206,27 +195,18 @@ impl StreamMgr {
/// Set a new signal generator. Returns an error if it is unapplicable. /// Set a new signal generator. Returns an error if it is unapplicable.
/// It is unapplicable if the number of channels of output does not match the /// It is unapplicable if the number of channels of output does not match the
/// number of output channels in a running stream. /// number of output channels in a running stream.
pub fn setSiggen(&mut self, siggen: Siggen) -> Result<()> { pub fn setSiggen(&mut self, siggen: Siggen) {
// Current signal generator. Where to place it? // Current signal generator. Where to place it?
if let Some(os) = &self.output_stream { if let Some(istream) = &self.input_stream {
assert!(self.siggen.is_none());
os.commtx.send(StreamCommand::NewSiggen(siggen)).unwrap();
os.commrx.recv().unwrap()
} else if let Some(istream) = &self.input_stream {
if let StreamType::Duplex = istream.streamtype { if let StreamType::Duplex = istream.streamtype {
assert!(self.siggen.is_none()); assert!(self.siggen.is_none());
istream istream.comm.send(StreamCommand::NewSiggen(siggen)).unwrap();
.commtx
.send(StreamCommand::NewSiggen(siggen))
.unwrap();
istream.commrx.recv().unwrap()
} else {
self.siggen = Some(siggen);
Ok(())
} }
} else if let Some(os) = &self.output_stream {
assert!(self.siggen.is_none());
os.comm.send(StreamCommand::NewSiggen(siggen)).unwrap();
} else { } else {
self.siggen = Some(siggen); self.siggen = Some(siggen);
Ok(())
} }
} }
@ -255,7 +235,7 @@ impl StreamMgr {
/// of queues that get data from the stream. /// of queues that get data from the stream.
pub fn addInQueue(&mut self, tx: Sender<InStreamMsg>) { pub fn addInQueue(&mut self, tx: Sender<InStreamMsg>) {
if let Some(is) = &self.input_stream { if let Some(is) = &self.input_stream {
is.commtx.send(StreamCommand::AddInQueue(tx)).unwrap() is.comm.send(StreamCommand::AddInQueue(tx)).unwrap()
} else { } else {
self.instreamqueues.as_mut().unwrap().push(tx); self.instreamqueues.as_mut().unwrap().push(tx);
} }
@ -265,14 +245,8 @@ impl StreamMgr {
&mut self, &mut self,
meta: Arc<StreamMetaData>, meta: Arc<StreamMetaData>,
rx: Receiver<InStreamMsg>, rx: Receiver<InStreamMsg>,
) -> ( ) -> (JoinHandle<InQueues>, Sender<StreamCommand>) {
JoinHandle<InQueues>, let (commtx, commrx) = unbounded();
Sender<StreamCommand>,
Receiver<Result<()>>,
) {
// Bi-directional communication between input stream thread and stream manager
let (commtx_ret, commrx) = unbounded();
let (commtx, commrx_ret) = unbounded();
// Unwrap here, as the queues should be free to grab // Unwrap here, as the queues should be free to grab
let mut iqueues = self let mut iqueues = self
@ -287,17 +261,8 @@ impl StreamMgr {
// New queue added // New queue added
StreamCommand::AddInQueue(queue) => { StreamCommand::AddInQueue(queue) => {
match queue.send(InStreamMsg::StreamStarted(meta.clone())) { match queue.send(InStreamMsg::StreamStarted(meta.clone())) {
Ok(()) => { Ok(()) => iqueues.push(queue),
iqueues.push(queue); Err(_) => {}
commtx.send(Ok(())).unwrap();
}
Err(e) => {
commtx
.send(Err(anyhow!(
"Cannot push to queue: {e}. Object destructed?"
)))
.unwrap();
}
} }
} }
@ -307,15 +272,11 @@ impl StreamMgr {
&mut iqueues, &mut iqueues,
InStreamMsg::StreamStopped, InStreamMsg::StreamStopped,
); );
commtx.send(Ok(())).unwrap();
break 'infy; break 'infy;
} }
StreamCommand::NewSiggen(_) => { StreamCommand::NewSiggen(_) => {
panic!("Error: signal generator send to input-only stream."); panic!("Error: signal generator send to input-only stream.");
} }
StreamCommand::SiggenCommand(_) => {
panic!("Error: signal generator command send to input-only stream.");
}
} }
} }
if let Ok(msg) = rx.recv_timeout(time::Duration::from_millis(10)) { if let Ok(msg) = rx.recv_timeout(time::Duration::from_millis(10)) {
@ -324,7 +285,7 @@ impl StreamMgr {
} }
iqueues iqueues
}); });
(threadhandle, commtx_ret, commrx_ret) (threadhandle, commtx)
} }
// Match device info struct on given daq config. // Match device info struct on given daq config.
@ -342,13 +303,8 @@ impl StreamMgr {
&mut self, &mut self,
meta: Arc<StreamMetaData>, meta: Arc<StreamMetaData>,
tx: Sender<RawStreamData>, tx: Sender<RawStreamData>,
) -> ( ) -> (JoinHandle<Siggen>, Sender<StreamCommand>) {
JoinHandle<Siggen>, let (commtx, commrx) = unbounded();
Sender<StreamCommand>,
Receiver<Result<()>>,
) {
let (commtx_res, commrx) = unbounded();
let (commtx, commrx_res) = unbounded();
// Number of channels to output for // Number of channels to output for
let nchannels = meta.nchannels(); let nchannels = meta.nchannels();
@ -358,9 +314,8 @@ impl StreamMgr {
let mut siggen = self let mut siggen = self
.siggen .siggen
.take() .take()
.unwrap_or_else(|| Siggen::newSilence(meta.samplerate, nchannels)); .unwrap_or_else(|| Siggen::newSilence(nchannels));
siggen.setAllMute(true);
if siggen.nchannels() != nchannels { if siggen.nchannels() != nchannels {
// Updating number of channels // Updating number of channels
siggen.setNChannels(nchannels); siggen.setNChannels(nchannels);
@ -368,15 +323,9 @@ impl StreamMgr {
siggen.reset(meta.samplerate); siggen.reset(meta.samplerate);
let threadhandle = std::thread::spawn(move || { let threadhandle = std::thread::spawn(move || {
// What is a good sleep time? We have made sure that there are let mut floatbuf: Vec<Flt> = Vec::with_capacity(nchannels * meta.framesPerBlock);
// two buffers available for the output stream. We choose to wake up twice per frame.
let sleep_time_us = Duration::from_micros(
(0.5 * 1e6 * meta.framesPerBlock as Flt / meta.samplerate) as u64,
);
let mut floatbuf: Vec<Flt> = vec![0.; nchannels * meta.framesPerBlock];
'infy: loop { 'infy: loop {
if let Ok(comm_msg) = commrx.recv_timeout(sleep_time_us) { if let Ok(comm_msg) = commrx.try_recv() {
match comm_msg { match comm_msg {
// New queue added // New queue added
StreamCommand::AddInQueue(_) => { StreamCommand::AddInQueue(_) => {
@ -385,7 +334,6 @@ impl StreamMgr {
// Stop this thread. Returns the queue // Stop this thread. Returns the queue
StreamCommand::StopThread => { StreamCommand::StopThread => {
commtx.send(Ok(())).unwrap();
break 'infy; break 'infy;
} }
StreamCommand::NewSiggen(new_siggen) => { StreamCommand::NewSiggen(new_siggen) => {
@ -396,20 +344,16 @@ impl StreamMgr {
// println!("Updating channels"); // println!("Updating channels");
siggen.setNChannels(nchannels); siggen.setNChannels(nchannels);
} }
commtx.send(Ok(())).unwrap();
}
StreamCommand::SiggenCommand(cmd) => {
// Apply command to signal generator.
let res = siggen.applyCommand(cmd);
commtx.send(res).unwrap();
} }
} }
} }
if tx.len() < 1 { while tx.len() < 2 {
// Obtain signal from signal generator unsafe {
floatbuf.set_len(nchannels * meta.framesPerBlock);
}
// Obtain signal
siggen.genSignal(&mut floatbuf); siggen.genSignal(&mut floatbuf);
// println!("level: {}", floatbuf.iter().sum::<Flt>());
// Convert signal generator data to raw data and push to the stream thread
let msg = match meta.rawDatatype { let msg = match meta.rawDatatype {
DataType::I8 => { DataType::I8 => {
let v = Vec::<i8>::from_iter(floatbuf.iter().map(|f| f.to_sample())); let v = Vec::<i8>::from_iter(floatbuf.iter().map(|f| f.to_sample()));
@ -433,17 +377,14 @@ impl StreamMgr {
} }
}; };
if let Err(_e) = tx.send(msg) { if let Err(_e) = tx.send(msg) {
// An error occured while trying to send the raw data to // println!("Error sending raw stream data to output stream!");
// the stream. This might be because the stream has break 'infy;
// stopped or has an error.
// There is nothing we can do here, but we should not stop the thread.
} }
} }
} }
siggen siggen
}); });
(threadhandle, commtx_res, commrx_res) (threadhandle, commtx)
} }
/// Start a stream of certain type, using given configuration /// Start a stream of certain type, using given configuration
@ -477,14 +418,13 @@ impl StreamMgr {
_ => bail!("API {} not implemented!", cfg.api), _ => bail!("API {} not implemented!", cfg.api),
}; };
let meta = stream.metadata(); let meta = stream.metadata();
let (threadhandle, commtx, commrx) = self.startOuputStreamThread(meta, tx); let (threadhandle, commtx) = self.startOuputStreamThread(meta, tx);
self.output_stream = Some(StreamInfo { self.output_stream = Some(StreamInfo {
streamtype: StreamType::Input, streamtype: StreamType::Input,
stream, stream,
threadhandle, threadhandle,
commtx, comm: commtx,
commrx,
}); });
Ok(()) Ok(())
@ -532,14 +472,13 @@ impl StreamMgr {
sendMsgToAllQueuesRemoveUnused(iqueues, InStreamMsg::StreamStarted(meta.clone())); sendMsgToAllQueuesRemoveUnused(iqueues, InStreamMsg::StreamStarted(meta.clone()));
let (threadhandle, commtx, commrx) = self.startInputStreamThread(meta, rx); let (threadhandle, commtx) = self.startInputStreamThread(meta, rx);
self.input_stream = Some(StreamInfo { self.input_stream = Some(StreamInfo {
streamtype: stype, streamtype: stype,
stream, stream,
threadhandle, threadhandle,
commtx, comm: commtx,
commrx,
}); });
Ok(()) Ok(())
@ -564,14 +503,13 @@ impl StreamMgr {
let meta = stream.metadata(); let meta = stream.metadata();
sendMsgToAllQueuesRemoveUnused(iqueues, InStreamMsg::StreamStarted(meta.clone())); sendMsgToAllQueuesRemoveUnused(iqueues, InStreamMsg::StreamStarted(meta.clone()));
let (threadhandle, commtx, commrx) = self.startInputStreamThread(meta, rx); let (threadhandle, commtx) = self.startInputStreamThread(meta, rx);
self.input_stream = Some(StreamInfo { self.input_stream = Some(StreamInfo {
streamtype: StreamType::Input, streamtype: StreamType::Input,
stream, stream,
threadhandle, threadhandle,
commtx, comm: commtx,
commrx,
}); });
Ok(()) Ok(())
@ -599,14 +537,15 @@ impl StreamMgr {
let (tx, rx)= unbounded(); let (tx, rx)= unbounded();
let stream = self.cpal_api.startDefaultOutputStream(rx)?; let stream = self.cpal_api.startDefaultOutputStream(rx)?;
let meta = stream.metadata(); let meta = stream.metadata();
let (threadhandle, commtx, commrx) = self.startOuputStreamThread(meta, tx); let (threadhandle, commtx) = self.startOuputStreamThread(meta, tx);
// Inform all listeners of new stream data
self.output_stream = Some(StreamInfo { self.output_stream = Some(StreamInfo {
streamtype: StreamType::Input, streamtype: StreamType::Input,
stream, stream,
threadhandle, threadhandle,
commtx, comm: commtx,
commrx,
}); });
Ok(()) Ok(())
@ -624,22 +563,19 @@ impl StreamMgr {
streamtype: _, // Ignored here streamtype: _, // Ignored here
stream: _, stream: _,
threadhandle, threadhandle,
commtx, comm,
commrx,
}) = self.input_stream.take() }) = self.input_stream.take()
{ {
// println!("Stopping existing stream.."); // println!("Stopping existing stream..");
// Send thread to stop // Send thread to stop
commtx.send(StreamCommand::StopThread).unwrap(); comm.send(StreamCommand::StopThread).unwrap();
// Store stream queues back into StreamMgr // Store stream queues back into StreamMgr
self.instreamqueues = Some(threadhandle.join().expect("Stream thread panicked!")); self.instreamqueues = Some(threadhandle.join().expect("Stream thread panicked!"));
let res = commrx.recv().unwrap();
return res;
} else { } else {
bail!("Stream is not running.") bail!("Stream is not running.")
} }
Ok(())
} }
/// Stop existing output stream /// Stop existing output stream
pub fn stopOutputStream(&mut self) -> Result<()> { pub fn stopOutputStream(&mut self) -> Result<()> {
@ -647,17 +583,21 @@ impl StreamMgr {
streamtype: _, // Ignored here streamtype: _, // Ignored here
stream: _, stream: _,
threadhandle, threadhandle,
commtx, comm,
commrx,
}) = self.output_stream.take() }) = self.output_stream.take()
{ {
commtx.send(StreamCommand::StopThread).unwrap(); if comm.send(StreamCommand::StopThread).is_err() {
// eprintln!("Wainting for threadhandle to join..."); // Failed to send command over channel. This means the thread is
// already finished due to some other reason.
assert!(threadhandle.is_finished());
}
// println!("Wainting for threadhandle to join...");
self.siggen = Some(threadhandle.join().expect("Output thread panicked!")); self.siggen = Some(threadhandle.join().expect("Output thread panicked!"));
commrx.recv().unwrap() // println!("Threadhandle joined!");
} else { } else {
bail!("Stream is not running."); bail!("Stream is not running.");
} }
Ok(())
} }
/// Stop existing running stream. /// Stop existing running stream.
/// ///
@ -670,40 +610,6 @@ impl StreamMgr {
StreamType::Output => self.stopOutputStream(), StreamType::Output => self.stopOutputStream(),
} }
} }
/// Apply a signal generator command to control the output stream's signal
/// generator. see [SiggenCommand] for types of commands. Muting, setting
/// gain etc. A result code is given back and should be checked for errors.
pub fn siggenCommand(&mut self, cmd: SiggenCommand) -> Result<()> {
if let Some(stream) = self.output_stream.as_ref() {
stream
.commtx
.send(StreamCommand::SiggenCommand(cmd))
.unwrap();
return stream.commrx.recv().unwrap();
} else if let Some(stream) = self.input_stream.as_ref() {
// When its duplex, it should have a signal generator
if matches!(stream.streamtype, StreamType::Duplex) {
stream
.commtx
.send(StreamCommand::SiggenCommand(cmd))
.unwrap();
stream.commrx.recv().unwrap()
} else {
return self
.siggen
.as_mut()
.expect("siggen should be in rest pos")
.applyCommand(cmd);
}
} else {
return self
.siggen
.as_mut()
.expect("siggen should be in rest pos")
.applyCommand(cmd);
}
}
} // impl StreamMgr } // impl StreamMgr
impl Drop for StreamMgr { impl Drop for StreamMgr {
fn drop(&mut self) { fn drop(&mut self) {

View File

@ -2,7 +2,7 @@ use super::*;
use super::seriesbiquad::*; use super::seriesbiquad::*;
use rayon::prelude::*; use rayon::prelude::*;
#[cfg_attr(feature = "python-bindings", pyclass)] #[cfg_attr(feature = "python-bindings", pyclass)]
#[derive(Clone, Debug)] #[derive(Clone)]
/// Multiple biquad filter that operate in parallel on a signal, and can apply a gain value to each /// Multiple biquad filter that operate in parallel on a signal, and can apply a gain value to each
/// of the returned values. The BiquadBank can be used to decompose a signal by running it through /// of the returned values. The BiquadBank can be used to decompose a signal by running it through
/// parallel filters, or it can directly be used to eq a signal. For the latter process, also a /// parallel filters, or it can directly be used to eq a signal. For the latter process, also a

View File

@ -4,8 +4,6 @@
//! Contains [Biquad], [SeriesBiquad], and [BiquadBank]. These are all constructs that work on //! Contains [Biquad], [SeriesBiquad], and [BiquadBank]. These are all constructs that work on
//! blocks of input data, and apply filters on it. TODO: implement FIR filter. //! blocks of input data, and apply filters on it. TODO: implement FIR filter.
#![allow(non_snake_case)] #![allow(non_snake_case)]
use std::fmt::Debug;
use super::config::*; use super::config::*;
mod biquad; mod biquad;
@ -25,7 +23,7 @@ pub use seriesbiquad::SeriesBiquad;
pub use zpkmodel::{PoleOrZero, ZPKModel, FilterSpec}; pub use zpkmodel::{PoleOrZero, ZPKModel, FilterSpec};
/// Implementations of this trait are able to DSP-filter input data. /// Implementations of this trait are able to DSP-filter input data.
pub trait Filter: Send + Debug { pub trait Filter: Send {
//! The filter trait is implemented by, for example, [Biquad], [SeriesBiquad], and [BiquadBank]. //! The filter trait is implemented by, for example, [Biquad], [SeriesBiquad], and [BiquadBank].
/// Filter input to generate output. A vector of output floats is generated with the same /// Filter input to generate output. A vector of output floats is generated with the same
@ -41,6 +39,7 @@ pub trait Filter: Send + Debug {
} }
/// Implementations are able to generate transfer functions of itself /// Implementations are able to generate transfer functions of itself
pub trait TransferFunction<'a, T>: Send pub trait TransferFunction<'a, T>: Send
where where
T: AsArray<'a, Flt>, T: AsArray<'a, Flt>,

View File

@ -33,7 +33,6 @@
mod config; mod config;
use config::*; use config::*;
use filter::*;
pub use config::Flt; pub use config::Flt;
pub mod daq; pub mod daq;
@ -41,6 +40,7 @@ pub mod filter;
pub mod ps; pub mod ps;
mod math; mod math;
pub mod siggen; pub mod siggen;
use filter::*;
pub mod rt; pub mod rt;
pub mod slm; pub mod slm;
@ -61,10 +61,6 @@ fn lasprs(m: &Bound<'_, PyModule>) -> PyResult<()> {
// Signal generator // Signal generator
m.add_class::<siggen::Siggen>()?; m.add_class::<siggen::Siggen>()?;
m.add_class::<siggen::SiggenCommand>()?;
m.add_class::<siggen::SweepType>()?;
m.add_class::<siggen::SiggenCommand>()?;
m.add_class::<siggen::Source>()?;
// SLM // SLM
m.add_class::<slm::TimeWeighting>()?; m.add_class::<slm::TimeWeighting>()?;

View File

@ -2,10 +2,7 @@
//! //!
//! //!
use crate::config::*; use crate::config::*;
use ndarray::{ArrayView1, IntoDimension}; use ndarray::ArrayView1;
use rand::{rngs::SmallRng, thread_rng, Rng, SeedableRng};
use rand_distr::StandardNormal;
use smallvec::Array;
/// Compute maximum value of an array of float values /// Compute maximum value of an array of float values
pub fn max<T>(arr: ArrayView<Flt, T>) -> Flt pub fn max<T>(arr: ArrayView<Flt, T>) -> Flt
@ -36,22 +33,3 @@ where
{ {
arr.fold(0., |acc, new| if new.abs() > acc { new.abs() } else { acc }) arr.fold(0., |acc, new| if new.abs() > acc { new.abs() } else { acc })
} }
/// Generate an array of *PSEUDO* random numbers from the standard normal
/// distribution. Typically used for white noise generation. To be used for
/// noise signals, not for anything that needs to be truly random.
///
/// # Args
///
/// - `shape` - Shape of the returned array
///
pub fn randNormal<Sh, D>(shape: Sh) -> ndarray::Array<Flt,D>
where
Sh: ShapeBuilder<Dim=D>,
D: Dimension
{
// Explicit conversion to Fortran order
let mut rng = SmallRng::from_entropy();
let shape = shape.f().into_shape_with_order();
ArrayBase::from_shape_simple_fn(shape, || rng.sample(StandardNormal))
}

View File

@ -308,9 +308,11 @@ impl AvPowerSpectra {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use approx::assert_abs_diff_eq; use approx::assert_abs_diff_eq;
use ndarray_rand::rand_distr::Normal;
use ndarray_rand::RandomExt;
use super::*; use super::*;
use crate::{config::*, math::randNormal}; use crate::config::*;
use super::{ApsMode, AvPowerSpectra, CPSResult, Overlap, WindowType}; use super::{ApsMode, AvPowerSpectra, CPSResult, Overlap, WindowType};
use Overlap::Percentage; use Overlap::Percentage;
@ -357,7 +359,8 @@ mod test {
let mut aps = AvPowerSpectra::new(settings); let mut aps = AvPowerSpectra::new(settings);
assert_eq!(aps.overlap_keep, 0); assert_eq!(aps.overlap_keep, 0);
let timedata_some = randNormal((nfft,1)); let distr = Normal::new(1.0, 1.0).expect("Distribution cannot be built");
let timedata_some = Dmat::random((nfft, 1), distr);
let timedata_zeros = Dmat::zeros((nfft, 1)); let timedata_zeros = Dmat::zeros((nfft, 1));
// Clone here, as first_result reference is overwritten by subsequent // Clone here, as first_result reference is overwritten by subsequent
@ -379,7 +382,8 @@ mod test {
#[test] #[test]
fn test_tf1() { fn test_tf1() {
let nfft = 4800; let nfft = 4800;
let mut timedata = randNormal((nfft, 1)); let distr = Normal::new(1.0, 1.0).unwrap();
let mut timedata = Dmat::random((nfft, 1), distr);
timedata timedata
.push_column(timedata.column(0).mapv(|a| 2. * a).view()) .push_column(timedata.column(0).mapv(|a| 2. * a).view())
.unwrap(); .unwrap();
@ -401,7 +405,8 @@ mod test {
#[test] #[test]
fn test_tf2() { fn test_tf2() {
let nfft = 4800; let nfft = 4800;
let mut timedata = randNormal((nfft, 1)); let distr = Normal::new(1.0, 1.0).unwrap();
let mut timedata = Dmat::random((nfft, 1), distr);
timedata timedata
.push_column(timedata.column(0).mapv(|a| 2. * a).view()) .push_column(timedata.column(0).mapv(|a| 2. * a).view())
.unwrap(); .unwrap();
@ -426,7 +431,8 @@ mod test {
#[test] #[test]
fn test_ap() { fn test_ap() {
let nfft = 1024; let nfft = 1024;
let timedata = randNormal((150 * nfft, 1)); let distr = Normal::new(1.0, 1.0).unwrap();
let timedata = Dmat::random((150 * nfft, 1), distr);
let timedata_mean_square = (&timedata * &timedata).sum() / (timedata.len() as Flt); let timedata_mean_square = (&timedata * &timedata).sum() / (timedata.len() as Flt);
for wt in [ for wt in [

View File

@ -2,7 +2,11 @@ use crate::config::*;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use strum_macros::{Display, EnumIter, EnumMessage}; use strum_macros::{Display, EnumIter, EnumMessage};
/// Sound level frequency weighting type (A, C, Z) /// Sound level frequency weighting type (A, C, Z)
#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
// Do the following when Pyo3 0.22 can finally be used combined with rust-numpy:
// #[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
// For now:
#[cfg_attr(feature = "python-bindings", pyclass)]
#[derive(Copy, Display, Debug, EnumMessage, Default, Clone, PartialEq, EnumIter)] #[derive(Copy, Display, Debug, EnumMessage, Default, Clone, PartialEq, EnumIter)]
pub enum FreqWeighting { pub enum FreqWeighting {
/// A-weighting /// A-weighting

View File

@ -252,6 +252,7 @@ mod test {
use approx::{abs_diff_eq, assert_relative_eq, assert_ulps_eq, ulps_eq}; use approx::{abs_diff_eq, assert_relative_eq, assert_ulps_eq, ulps_eq};
// For absolute value // For absolute value
use num::complex::ComplexFloat; use num::complex::ComplexFloat;
use rand_distr::StandardNormal;
/// Generate a sine wave at the order i /// Generate a sine wave at the order i
fn generate_sinewave(nfft: usize, order: usize) -> Dcol { fn generate_sinewave(nfft: usize, order: usize) -> Dcol {
@ -266,8 +267,6 @@ mod test {
) )
} }
use crate::math::randNormal;
use super::*; use super::*;
#[test] #[test]
/// Test whether DC part of single-sided FFT has right properties /// Test whether DC part of single-sided FFT has right properties
@ -350,6 +349,7 @@ mod test {
); );
} }
use ndarray_rand::RandomExt;
// Test parseval's theorem for some random data // Test parseval's theorem for some random data
#[test] #[test]
fn test_parseval() { fn test_parseval() {
@ -358,7 +358,7 @@ mod test {
let mut ps = PowerSpectra::newFromWindow(rect); let mut ps = PowerSpectra::newFromWindow(rect);
// Start with a time signal // Start with a time signal
let t: Dmat = randNormal((nfft,1)); let t: Dmat = Dmat::random((nfft, 1), StandardNormal);
let tavg = t.sum() / (nfft as Flt); let tavg = t.sum() / (nfft as Flt);
let t_dc_power = tavg.powi(2); let t_dc_power = tavg.powi(2);
@ -394,7 +394,7 @@ mod test {
let mut ps = PowerSpectra::newFromWindow(window); let mut ps = PowerSpectra::newFromWindow(window);
// Start with a time signal // Start with a time signal
let t: Dmat = randNormal((nfft,1)); let t: Dmat = 2. * Dmat::random((nfft, 1), StandardNormal);
let tavg = t.sum() / (nfft as Flt); let tavg = t.sum() / (nfft as Flt);
let t_dc_power = tavg.powi(2); let t_dc_power = tavg.powi(2);

View File

@ -8,7 +8,7 @@ use std::collections::VecDeque;
/// TimeBuffer, storage to add blocks of data in a ring buffer, that can be /// TimeBuffer, storage to add blocks of data in a ring buffer, that can be
/// extracted by blocks of other size. Also, we can keep samples in a buffer to /// extracted by blocks of other size. Also, we can keep samples in a buffer to
/// create, for example, overlapping windows of time data. /// create, for example, overlapping windows of time data.
#[derive(Default, Debug, Clone)] #[derive(Default, Debug)]
pub struct TimeBuffer { pub struct TimeBuffer {
data: Vec<VecDeque<Flt>>, data: Vec<VecDeque<Flt>>,
} }

View File

@ -73,8 +73,11 @@ fn hamming(N: usize) -> Dcol {
/// * Blackman /// * Blackman
/// ///
/// The [WindowType::default] is [WindowType::Hann]. /// The [WindowType::default] is [WindowType::Hann].
#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
#[derive(Display, Default, Copy, Clone, Debug, PartialEq, EnumMessage, EnumIter)] #[derive(Display, Default, Copy, Clone, Debug, PartialEq, EnumMessage, EnumIter)]
// Do the following when Pyo3 0.22 can finally be used combined with rust-numpy:
// #[cfg_attr(feature = "python-bindings", pyclass(eq))]
// For now:
#[cfg_attr(feature = "python-bindings", pyclass)]
pub enum WindowType { pub enum WindowType {
/// Von Hann window /// Von Hann window
#[default] #[default]

View File

@ -192,8 +192,8 @@ impl Drop for PPM {
/// Enumerator denoting, for each channel what the level approximately is. Low, /// Enumerator denoting, for each channel what the level approximately is. Low,
/// fine, high or clipped. /// fine, high or clipped.
#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))] #[cfg_attr(feature = "python-bindings", pyclass)]
#[derive(Copy, Debug, PartialEq, Clone, Default)] #[derive(Copy, Debug, Clone, Default)]
pub enum ClipState { pub enum ClipState {
/// Level is rather low /// Level is rather low
#[default] #[default]

View File

@ -136,11 +136,6 @@ impl RtAps {
let mut lck = self.status.lock(); let mut lck = self.status.lock();
lck.take() lck.take()
} }
/// Reset power spectra estimator, start with a clean sleeve
pub fn reset(&self) {
self.sender.send(RtApsMessage::ResetStatus).unwrap();
}
} }
impl Drop for RtAps { impl Drop for RtAps {
fn drop(&mut self) { fn drop(&mut self) {
@ -163,11 +158,6 @@ impl RtAps {
} }
None None
} }
#[pyo3(name = "reset")]
fn reset_py(&self) {
self.reset()
}
} }
#[cfg(test)] #[cfg(test)]

468
src/siggen.rs Normal file
View File

@ -0,0 +1,468 @@
//! This module provide signal generators. The import struct defined here is
//! [Siggen], which has several creation methods.
//!
//! # Examples
//!
//! ## Create some white noise and print it.
//!
//! ```
//! use lasprs::siggen::Siggen;
//! let mut wn = Siggen::newWhiteNoise(1);
//! // Set gains for all channels
//! wn.setAllGains(0.1);
//! // Unmute all channels
//! wn.setAllMute(false);
//! // Create a slice where data is stored.
//! let mut sig = [0. ; 1024];
//! // Fill `sig` with the signal data.
//! wn.genSignal(&mut sig);
//! // Print data.
//! println!("{:?}", &sig);
//!
//! ```
use super::config::*;
use super::filter::Filter;
use dasp_sample::{FromSample, Sample};
use rayon::prelude::*;
use std::fmt::Debug;
use std::iter::ExactSizeIterator;
use std::slice::IterMut;
use rand::prelude::*;
use rand::rngs::ThreadRng;
use rand_distr::StandardNormal;
/// Ratio between circumference and radius of a circle
const twopi: Flt = 2.0 * pi;
/// 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 dyn ExactSizeIterator<Item = &mut Flt>);
/// Reset the source state, i.e. set phase to 0, etc
fn reset(&mut self, fs: Flt);
/// Used to make the Siggen struct cloneable
fn clone_dyn(&self) -> Box<dyn Source>;
}
impl Clone for Box<dyn Source> {
fn clone(&self) -> Self {
self.clone_dyn()
}
}
#[derive(Clone)]
struct Silence {}
impl Source for Silence {
fn genSignal_unscaled(&mut self, sig: &mut dyn ExactSizeIterator<Item = &mut Flt>) {
sig.for_each(|s| {
*s = 0.0;
});
}
fn reset(&mut self, _fs: Flt) {}
fn clone_dyn(&self) -> Box<dyn Source> {
Box::new(self.clone())
}
}
/// White noise source
#[derive(Clone)]
struct WhiteNoise {}
impl WhiteNoise {
/// Generate new WhiteNoise generator
fn new() -> WhiteNoise {
WhiteNoise {}
}
}
impl Source for WhiteNoise {
fn genSignal_unscaled(&mut self, sig: &mut dyn ExactSizeIterator<Item = &mut Flt>) {
sig.for_each(|s| {
*s = thread_rng().sample(StandardNormal);
});
}
fn reset(&mut self, _fs: Flt) {}
fn clone_dyn(&self) -> Box<dyn Source> {
Box::new(self.clone())
}
}
/// Sine wave, with configurable frequency
#[derive(Clone)]
struct Sine {
// Sampling freq [Hz]
fs: Flt,
// current stored phase
phase: Flt,
// Signal frequency [rad/s]
omg: Flt,
}
impl Sine {
/// Create new sine source signal
///
/// Args:
///
/// * fs: Sampling freq [Hz]
/// *
fn new(freq: Flt) -> Sine {
Sine {
fs: -1.0,
phase: 0.0,
omg: 2.0 * pi * freq,
}
}
}
impl Source for Sine {
fn genSignal_unscaled(&mut self, sig: &mut dyn ExactSizeIterator<Item = &mut Flt>) {
if self.fs <= 0.0 {
sig.for_each(|s| {
*s = 0.0;
});
return;
}
sig.for_each(|s| {
*s = Flt::sin(self.phase);
self.phase += self.omg / self.fs;
self.phase %= twopi;
});
}
fn reset(&mut self, fs: Flt) {
self.fs = fs;
self.phase = 0.0;
}
fn clone_dyn(&self) -> Box<dyn Source> {
Box::new(self.clone())
}
}
/// 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)
///
#[derive(Clone)]
#[cfg_attr(feature = "python-bindings", pyclass)]
pub struct Siggen {
// The source dynamic signal. Noise, a sine wave, sweep, etc
source: Box<dyn Source>,
// Filter applied to the source signal
channels: Vec<SiggenChannelConfig>,
// Temporary source signal buffer
source_buf: Vec<Flt>,
// Output buffers (for filtered source signal)
chout_buf: Vec<Vec<Flt>>,
}
#[cfg(feature = "python-bindings")]
#[cfg_attr(feature = "python-bindings", pymethods)]
impl Siggen {
#[pyo3(name = "newWhiteNoise")]
#[staticmethod]
fn newWhiteNoise_py() -> Siggen {
Siggen::newWhiteNoise(0)
}
#[pyo3(name = "newSine")]
#[staticmethod]
fn newSine_py(freq: Flt) -> Siggen {
Siggen::newSine(0, freq)
}
}
/// Multiple channel signal generator. Can use a single source (coherent) to provide multiple signals
/// that can be sent out through different EQ's
impl Siggen {
/// Returns the number of channels this signal generator is generating for.
pub fn nchannels(&self) -> usize {
self.channels.len()
}
/// Silence: create a signal generator that does not output any dynamic
/// signal at all.
pub fn newSilence(nchannels: usize) -> Siggen {
Siggen {
channels: vec![SiggenChannelConfig::new(); nchannels],
source: Box::new(Silence {}),
source_buf: vec![],
chout_buf: vec![],
}
}
/// Create a white noise signal generator.
pub fn newWhiteNoise(nchannels: usize) -> Siggen {
Siggen::new(nchannels, Box::new(WhiteNoise::new()))
}
/// Set gains of all channels in signal generator to the same value
///
/// # Args
///
/// * g: New gain value
pub fn setAllGains(&mut self, g: Flt) {
self.channels.iter_mut().for_each(|set| set.setGain(g))
}
/// Set the number of channels to generate a signal for. Truncates the
/// output in case the value before calling this method is too little.
/// Appends new channel configs in case to little is available.
///
/// * nch: The new required number of channels
pub fn setNChannels(&mut self, nch: usize) {
self.channels.truncate(nch);
while self.channels.len() < nch {
self.channels.push(SiggenChannelConfig::new());
}
}
/// Set the DC offset for all channels
pub fn setDCOffset(&mut self, dc: &[Flt]) {
self.channels.iter_mut().zip(dc).for_each(|(ch, dc)| {
ch.DCOffset = *dc;
});
}
/// Create a sine wave signal generator
///
/// * freq: Frequency of the sine wave in \[Hz\]
pub fn newSine(nchannels: usize, freq: Flt) -> Siggen {
Siggen::new(nchannels, Box::new(Sine::new(freq)))
}
/// Create a new signal generator wiht an arbitrary source.
pub fn new(nchannels: usize, source: Box<dyn Source>) -> Siggen {
Siggen {
source,
channels: vec![SiggenChannelConfig::new(); nchannels],
source_buf: vec![],
chout_buf: vec![],
}
}
/// Creates *interleaved* output signal
pub fn genSignal<T>(&mut self, out: &mut [T])
where
T: Sample + FromSample<Flt> + Debug,
Flt: Sample,
{
let nch = self.nchannels();
let nsamples: usize = out.len() / nch;
assert!(out.len() % self.nchannels() == 0);
// Create source signal
self.source_buf.resize(nsamples, 0.0);
self.source
.genSignal_unscaled(&mut self.source_buf.iter_mut());
// println!("Source signal: {:?}", self.source_buf);
// Write output while casted to the correct type
// Iterate over each channel, and counter
self.chout_buf.resize(nch, vec![]);
for (channelno, (channel, chout)) in self
.channels
.iter_mut()
.zip(self.chout_buf.iter_mut())
.enumerate()
{
chout.resize(nsamples, 0.0);
// Create output signal, overwrite chout
channel.genSignal(&self.source_buf, chout);
// println!("Channel: {}, {:?}", channelno, chout);
let out_iterator = out.iter_mut().skip(channelno).step_by(nch);
out_iterator.zip(chout).for_each(|(out, chin)| {
*out = chin.to_sample();
});
}
// println!("{:?}", out);
}
/// Reset signal generator. Applies any kind of cleanup necessary.
///
/// Args
///
/// * fs: (New) Sampling frequency \[Hz\]
///
pub fn reset(&mut self, fs: Flt) {
self.source.reset(fs);
self.channels.iter_mut().for_each(|x| x.reset(fs))
}
/// Mute / unmute all channels at once
pub fn setAllMute(&mut self, mute: bool) {
self.channels.iter_mut().for_each(|s| {
s.setMute(mute);
});
}
/// Mute / unmute individual channels. Array of bools should have same size
/// as number of channels in signal generator.
pub fn setMute(&mut self, mute: &[bool]) {
assert!(mute.len() == self.nchannels());
self.channels.iter_mut().zip(mute).for_each(|(s, m)| {
s.setMute(*m);
});
}
}
/// Signal generator config for a certain channel
#[derive(Clone)]
struct SiggenChannelConfig {
muted: bool,
prefilter: Option<Box<dyn Filter>>,
gain: Flt,
DCOffset: Flt,
}
unsafe impl Send for SiggenChannelConfig {}
impl SiggenChannelConfig {
/// Set new pre-filter that filters the source signal
pub fn setPreFilter(&mut self, pref: Option<Box<dyn Filter>>) {
self.prefilter = pref;
}
/// Set the gain applied to the source signal
///
/// * g: Gain value. Can be any float. If set to 0.0, the source is effectively muted. Only
/// using (setMute) is a more efficient way to do this.
pub fn setGain(&mut self, g: Flt) {
self.gain = g;
}
/// Reset signal channel config. Only resets the prefilter state
pub fn reset(&mut self, _fs: Flt) {
if let Some(f) = &mut self.prefilter {
f.reset()
}
}
/// Generate new channel configuration using 'arbitrary' initial config: muted false, gain 1.0, DC offset 0.
/// and no prefilter
pub fn new() -> SiggenChannelConfig {
SiggenChannelConfig {
muted: false,
prefilter: None,
gain: 1.0,
DCOffset: 0.0,
}
}
/// Set mute on channel. If true, only DC signal offset is outputed from (SiggenChannelConfig::transform).
pub fn setMute(&mut self, mute: bool) {
self.muted = mute;
}
/// Generate new signal data, given input source data.
///
/// # Args
///
/// source: Input source signal.
/// result: Reference of array of float values to be filled with signal data.
///
/// # Details
///
/// - When muted, the DC offset is still applied
/// - The order of the generation is:
/// - If a prefilter is installed, this pre-filter is applied to the source signal.
/// - Gain is applied.
/// - Offset is applied (thus, no gain is applied to the DC offset).
///
pub fn genSignal(&mut self, source: &[Flt], result: &mut [Flt]) {
if self.muted {
result.iter_mut().for_each(|x| {
*x = 0.0;
});
} else {
result.copy_from_slice(source);
if let Some(f) = &mut self.prefilter {
f.filter(result);
}
}
result.iter_mut().for_each(|x| {
// First apply gain, then offset
*x *= self.gain;
*x += self.DCOffset;
});
}
}
#[cfg(test)]
mod test {
use approx::assert_abs_diff_eq;
use super::*;
use crate::Flt;
#[test]
fn test_whitenoise() {
// This code is just to check syntax. We should really be listening to these outputs.
let mut t = [0.0; 10];
Siggen::newWhiteNoise(1).genSignal(&mut t);
// println!("{:?}", &t);
}
#[test]
fn test_sine() {
// This code is just to check syntax. We should really be listening to
// these outputs.
const N: usize = 10000;
let mut s1 = [0.0; N];
let mut s2 = [0.0; N];
let mut siggen = Siggen::newSine(1, 1.0);
siggen.reset(10.0);
siggen.setAllMute(false);
siggen.genSignal(&mut s1);
siggen.reset(10.0);
siggen.genSignal(&mut s2);
let absdiff = s1
.iter()
.zip(s2.iter())
.map(|(s1, s2)| Flt::abs(*s1 - *s2))
.sum::<Flt>();
assert_abs_diff_eq!(absdiff, 0., epsilon = Flt::EPSILON * 100.);
}
#[test]
fn test_sine2() {
// Test if channels are properly separated etc. Check if RMS is correct
// for amplitude = 1.0.
const fs: Flt = 10.0;
// Number of samples per channel
const Nframes: usize = 10000;
const Nch: usize = 2;
let mut signal = [0.0; Nch * Nframes];
let mut siggen = Siggen::newSine(Nch, 1.0);
siggen.reset(fs);
siggen.setMute(&[false, true]);
// siggen.channels[0].DCOffset = 0.1;
// Split off in two terms, see if this works properly
siggen.genSignal(&mut signal[..Nframes / 2]);
siggen.genSignal(&mut signal[Nframes / 2..]);
// Mean square of the signal
let ms1 = signal.iter().step_by(2).map(|s1| *s1 * *s1).sum::<Flt>() / (Nframes as Flt);
println!("ms1: {}", ms1);
let ms2 = signal
.iter()
.skip(1)
.step_by(2)
.map(|s1| *s1 * *s1)
.sum::<Flt>()
/ (Nframes as Flt);
assert_abs_diff_eq!(Flt::abs(ms1 - 0.5) , 0., epsilon= Flt::EPSILON * 1e3);
assert_eq!(ms2, 0.0);
}
// A small test to learn a bit about sample types and conversion. This
// is the thing we want.
#[test]
fn test_sample() {
assert_eq!((0.5f32).to_sample::<i8>(), 64);
assert_eq!((1.0f32).to_sample::<i8>(), 127);
assert_eq!(-(1.0f32).to_sample::<i8>(), -127);
assert_eq!((1.0f32).to_sample::<i16>(), i16::MAX);
}
}

View File

@ -1,31 +0,0 @@
//! This module provide signal generators. The import struct defined here is
//! [Siggen], which has several creation methods.
//!
//! # Examples
//!
//! ## Create some white noise and print it.
//!
//! ```
//! use lasprs::siggen::Siggen;
//! let mut wn = Siggen::newWhiteNoise(1);
//! // Set gains for all channels
//! wn.setAllGains(0.1);
//! // Unmute all channels
//! wn.setAllMute(false);
//! // Create a slice where data is stored.
//! let mut sig = [0. ; 1024];
//! // Fill `sig` with the signal data.
//! wn.genSignal(&mut sig);
//! // Print data.
//! println!("{:?}", &sig);
//!
//! ```
mod siggen;
mod siggenchannel;
mod source;
mod siggencmd;
mod sweep;
pub use source::Source;
pub use siggen::Siggen;
pub use sweep::SweepType;
pub use siggencmd::SiggenCommand;

View File

@ -1,345 +0,0 @@
use super::siggenchannel::SiggenChannelConfig;
use super::source::{self, *};
use super::sweep::SweepType;
use super::SiggenCommand;
use crate::config::*;
use crate::filter::Filter;
use anyhow::{bail, Result};
use dasp_sample::{FromSample, Sample};
use rayon::prelude::*;
use std::fmt::Debug;
use std::iter::ExactSizeIterator;
use std::slice::IterMut;
/// Multiple channel 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]
/// * [Siggen::newSilence]
///
#[derive(Clone, Debug)]
#[cfg_attr(feature = "python-bindings", pyclass)]
pub struct Siggen {
// The source dynamic signal. Noise, a sine wave, sweep, etc
source: Source,
// Channel configuration for each output channel
channels: Vec<SiggenChannelConfig>,
// Temporary source signal buffer
source_buf: Vec<Flt>,
// Output buffers (for filtered source signal)
chout_buf: Vec<Vec<Flt>>,
}
#[cfg(feature = "python-bindings")]
#[cfg_attr(feature = "python-bindings", pymethods)]
impl Siggen {
#[pyo3(name = "newWhiteNoise")]
#[staticmethod]
fn newWhiteNoise_py(fs: Flt) -> Siggen {
Siggen::newWhiteNoise(fs, 0)
}
#[pyo3(name = "newSine")]
#[staticmethod]
fn newSine_py(fs: Flt, freq: Flt, nchannels: usize) -> PyResult<Siggen> {
Ok(Siggen::newSine(fs, nchannels, freq)?)
}
#[pyo3(name = "newSweep")]
#[staticmethod]
fn newSweep_py(
fs: Flt,
nchannels: usize,
fl: Flt,
fu: Flt,
sweep_time: Flt,
quiet_time: Flt,
sweep_type: SweepType,
) -> Result<Self> {
Ok(Siggen::newSweep(
fs, nchannels, fl, fu, sweep_time, quiet_time, sweep_type,
)?)
}
}
impl Siggen {
/// Create a new signal generator with an arbitrary source.
/// # Args
///
/// - `nchannels` - The number of channels to output
/// - `source` - Source function
pub fn new(nchannels: usize, source: Source) -> Siggen {
Siggen {
source,
channels: vec![SiggenChannelConfig::new(); nchannels],
source_buf: vec![],
chout_buf: vec![],
}
}
/// Create sine sweep signal generator
///
/// # Args
///
/// - `fs` - Sample rate \[Hz\]
/// - `nchannels`: The number of channels to output
/// - `fl` - Lower frequency \[Hz\]
/// - `fu` - Upper frequency \[Hz\]
/// - `sweep_time` - The duration of a single sweep \[s\]
/// - `quiet_time` - Time of silence after one sweep and start of the next \[s\]
/// - `sweep_type` - The type of the sweep, see [SweepType].
pub fn newSweep(
fs: Flt,
nchannels: usize,
fl: Flt,
fu: Flt,
sweep_time: Flt,
quiet_time: Flt,
sweep_type: SweepType,
) -> Result<Self> {
let source = Source::newSweep(fs, fl, fu, sweep_time, quiet_time, sweep_type)?;
Ok(Self::new(nchannels, source))
}
/// Create a sine wave signal generator
///
/// # Args
///
/// - `fs` - Sampling frequency \[Hz\]
/// - `nchannels`: The number of channels to output
/// * `freq` - Frequency of the sine wave in \[Hz\]
pub fn newSine(fs: Flt, nchannels: usize, freq: Flt) -> Result<Siggen> {
Ok(Siggen::new(nchannels, Source::newSine(fs, freq)?))
}
/// Silence: create a signal generator that does not output any dynamic
/// signal at all.
/// # Args
///
/// - `fs` - Sampling frequency \[Hz\]
/// - `nchannels` - The number of channels to output
pub fn newSilence(_fs: Flt, nchannels: usize) -> Siggen {
Siggen::new(nchannels, Source::newSilence())
}
/// Create a white noise signal generator.
///
/// # Args
///
/// - `fs` - Sampling frequency \[Hz\]
/// - `nchannels` - The number of channels to output
pub fn newWhiteNoise(_fs: Flt, nchannels: usize) -> Siggen {
Siggen::new(nchannels, Source::newWhiteNoise())
}
/// Returns the number of channels this signal generator is generating for.
pub fn nchannels(&self) -> usize {
self.channels.len()
}
/// Apply command to current signal generator to change its state.
pub fn applyCommand(&mut self, msg: SiggenCommand) -> Result<()> {
match msg {
SiggenCommand::ChangeSource { src } => {
self.source = src;
Ok(())
}
SiggenCommand::ResetSiggen { fs } => {
self.reset(fs);
Ok(())
}
SiggenCommand::SetMuteAllChannels { mute } => {
self.setAllMute(mute);
Ok(())
}
SiggenCommand::SetMuteChannel { ch, mute } => {
if ch > self.channels.len() {
bail!("Invalid channel index: {ch}");
}
self.channels[ch].setMute(mute);
Ok(())
}
SiggenCommand::SetAllGains { g } => {
self.setAllGains(g);
Ok(())
}
}
}
/// Set gains of all channels in signal generator to the same value
///
/// # Args
///
/// * g: New gain value
pub fn setAllGains(&mut self, g: Flt) {
self.channels.iter_mut().for_each(|set| set.setGain(g))
}
/// Set the number of channels to generate a signal for. Truncates the
/// output in case the value before calling this method is too little.
/// Appends new channel configs in case to little is available.
///
/// * nch: The new required number of channels
pub fn setNChannels(&mut self, nch: usize) {
self.channels.truncate(nch);
while self.channels.len() < nch {
self.channels.push(SiggenChannelConfig::new());
}
}
/// Set the DC offset for all channels
pub fn setDCOffset(&mut self, dc: &[Flt]) {
self.channels.iter_mut().zip(dc).for_each(|(ch, dc)| {
ch.DCOffset = *dc;
});
}
/// Creates *interleaved* output signal
pub fn genSignal<T>(&mut self, out: &mut [T])
where
T: Sample + FromSample<Flt> + Debug,
Flt: Sample,
{
let nch = self.nchannels();
let nsamples: usize = out.len() / nch;
assert!(out.len() % self.nchannels() == 0);
// Create source signal
self.source_buf.resize(nsamples, 0.0);
self.source
.genSignal_unscaled(&mut self.source_buf.iter_mut());
// println!("Source signal: {:?}", self.source_buf);
// Write output while casted to the correct type
// Iterate over each channel, and counter
self.chout_buf.resize(nch, vec![]);
for (channelno, (channel, chout)) in self
.channels
.iter_mut()
.zip(self.chout_buf.iter_mut())
.enumerate()
{
chout.resize(nsamples, 0.0);
// Create output signal, overwrite chout
channel.genSignal(&self.source_buf, chout);
// println!("Channel: {}, {:?}", channelno, chout);
let out_iterator = out.iter_mut().skip(channelno).step_by(nch);
out_iterator.zip(chout).for_each(|(out, chin)| {
*out = chin.to_sample();
});
}
// println!("{:?}", out);
}
/// Reset signal generator. Applies any kind of cleanup necessary.
///
/// Args
///
/// * fs: (New) Sampling frequency \[Hz\]
///
pub fn reset(&mut self, fs: Flt) {
self.source.reset(fs);
self.channels.iter_mut().for_each(|x| x.reset(fs))
}
/// Mute / unmute all channels at once
pub fn setAllMute(&mut self, mute: bool) {
self.channels.iter_mut().for_each(|s| {
s.setMute(mute);
});
}
/// Mute / unmute individual channels. Array of bools should have same size
/// as number of channels in signal generator.
pub fn setMute(&mut self, mute: &[bool]) {
assert!(mute.len() == self.nchannels());
self.channels.iter_mut().zip(mute).for_each(|(s, m)| {
s.setMute(*m);
});
}
}
#[cfg(test)]
mod test {
use approx::assert_abs_diff_eq;
use super::*;
use crate::Flt;
#[test]
fn test_whitenoise() {
// This code is just to check syntax. We should really be listening to these outputs.
let mut t = [0.0; 10];
Siggen::newWhiteNoise(1., 1).genSignal(&mut t);
// println!("{:?}", &t);
}
#[test]
fn test_sine() {
// This code is just to check syntax. We should really be listening to
// these outputs.
const N: usize = 10000;
let mut s1 = [0.0; N];
let mut s2 = [0.0; N];
let mut siggen = Siggen::newSine(1., 1, 1.0).unwrap();
siggen.reset(10.0);
siggen.setAllMute(false);
siggen.genSignal(&mut s1);
siggen.reset(10.0);
siggen.genSignal(&mut s2);
let absdiff = s1
.iter()
.zip(s2.iter())
.map(|(s1, s2)| Flt::abs(*s1 - *s2))
.sum::<Flt>();
assert_abs_diff_eq!(absdiff, 0., epsilon = Flt::EPSILON * 100.);
}
#[test]
fn test_sine2() {
// Test if channels are properly separated etc. Check if RMS is correct
// for amplitude = 1.0.
const fs: Flt = 10.0;
// Number of samples per channel
const Nframes: usize = 10000;
const Nch: usize = 2;
let mut signal = [0.0; Nch * Nframes];
let mut siggen = Siggen::newSine(fs, Nch, 1.0).unwrap();
siggen.reset(fs);
siggen.setMute(&[false, true]);
// siggen.channels[0].DCOffset = 0.1;
// Split off in two terms, see if this works properly
siggen.genSignal(&mut signal[..Nframes / 2]);
siggen.genSignal(&mut signal[Nframes / 2..]);
// Mean square of the signal
let ms1 = signal.iter().step_by(2).map(|s1| *s1 * *s1).sum::<Flt>() / (Nframes as Flt);
println!("ms1: {}", ms1);
let ms2 = signal
.iter()
.skip(1)
.step_by(2)
.map(|s1| *s1 * *s1)
.sum::<Flt>()
/ (Nframes as Flt);
assert_abs_diff_eq!(Flt::abs(ms1 - 0.5), 0., epsilon = Flt::EPSILON * 1e3);
assert_eq!(ms2, 0.0);
}
// A small test to learn a bit about sample types and conversion. This
// is the thing we want.
#[test]
fn test_sample() {
assert_eq!((0.5f32).to_sample::<i8>(), 64);
assert_eq!((1.0f32).to_sample::<i8>(), 127);
assert_eq!(-(1.0f32).to_sample::<i8>(), -127);
assert_eq!((1.0f32).to_sample::<i16>(), i16::MAX);
}
}

View File

@ -1,78 +0,0 @@
use crate::config::*;
use crate::filter::Filter;
/// Signal generator config for a certain channel
#[derive(Clone, Debug)]
pub struct SiggenChannelConfig {
muted: bool,
prefilter: Option<Box<dyn Filter>>,
gain: Flt,
pub DCOffset: Flt,
}
unsafe impl Send for SiggenChannelConfig {}
impl SiggenChannelConfig {
/// Set new pre-filter that filters the source signal
pub fn setPreFilter(&mut self, pref: Option<Box<dyn Filter>>) {
self.prefilter = pref;
}
/// Set the gain applied to the source signal
///
/// * g: Gain value. Can be any float. If set to 0.0, the source is effectively muted. Only
/// using (setMute) is a more efficient way to do this.
pub fn setGain(&mut self, g: Flt) {
self.gain = g;
}
/// Reset signal channel config. Only resets the prefilter state
pub fn reset(&mut self, _fs: Flt) {
if let Some(f) = &mut self.prefilter {
f.reset()
}
}
/// Generate new channel configuration using 'arbitrary' initial config: muted false, gain 1.0, DC offset 0.
/// and no prefilter
pub fn new() -> SiggenChannelConfig {
SiggenChannelConfig {
muted: false,
prefilter: None,
gain: 1.0,
DCOffset: 0.0,
}
}
/// Set mute on channel. If true, only DC signal offset is outputed from (SiggenChannelConfig::transform).
pub fn setMute(&mut self, mute: bool) {
self.muted = mute;
}
/// Generate new signal data, given input source data.
///
/// # Args
///
/// source: Input source signal.
/// result: Reference of array of float values to be filled with signal data.
///
/// # Details
///
/// - When muted, the DC offset is still applied
/// - The order of the generation is:
/// - If a prefilter is installed, this pre-filter is applied to the source signal.
/// - Gain is applied.
/// - Offset is applied (thus, no gain is applied to the DC offset).
///
pub fn genSignal(&mut self, source: &[Flt], result: &mut [Flt]) {
if self.muted {
result.iter_mut().for_each(|x| {
*x = 0.0;
});
} else {
result.copy_from_slice(source);
if let Some(f) = &mut self.prefilter {
f.filter(result);
}
}
result.iter_mut().for_each(|x| {
// First apply gain, then offset
*x *= self.gain;
*x += self.DCOffset;
});
}
}

View File

@ -1,39 +0,0 @@
use super::source::*;
use crate::config::*;
/// Messages that can be send to a given signal generator [Siggen], to change its behaviour
#[cfg_attr(feature = "python-bindings", pyclass)]
#[derive(Clone, Debug)]
pub enum SiggenCommand {
/// Change the source to a sine wave with given frequency.
ChangeSource{
/// New signal source to apply for signal generator
src: Source,
},
/// Reset the signal generator state
ResetSiggen {
/// Sampling frequency \[Hz\]
fs: Flt,
},
/// Set all gains to value g
SetAllGains {
/// Linear gain level to apply to all channels
g: Flt,
},
/// Change the mute state for a certain channel
SetMuteChannel {
/// channel index
ch: usize,
/// mute state
mute: bool,
},
/// Change the mute state for all channels
SetMuteAllChannels {
/// mute state
mute: bool,
},
}

View File

@ -1,265 +0,0 @@
//! All sources for a signal generator. Sine waves, sweeps, noise, etc.
use super::sweep::{SweepParams, SweepType};
use crate::config::*;
use std::fmt::Debug;
use std::ops::{Deref, DerefMut};
/// Ratio between circumference and radius of a circle
const twopi: Flt = 2.0 * pi;
use crate::config::*;
use anyhow::{bail, Result};
use rand::prelude::*;
use rand::rngs::ThreadRng;
use rand_distr::StandardNormal;
/// Signal source for a signal generator. A signal source is capable of creating
/// new signal data.
#[cfg_attr(feature = "python-bindings", pyclass)]
#[derive(Clone, Debug)]
pub struct Source {
src: Box<dyn SourceImpl>,
}
impl Source {
/// Create a sine wave signal source
///
/// # Args
///
/// - `fs` - Sampling frequency \[Hz\]
/// * `freq` - Frequency of the sine wave in \[Hz\]
pub fn newSine(fs: Flt, freq: Flt) -> Result<Source> {
Ok(Source {
src: Box::new(Sine::new(fs, freq)?),
})
}
/// Silence: create a signal source that does not output any dynamic
/// signal at all.
pub fn newSilence() -> Source {
Source {
src: Box::new(Silence {}),
}
}
/// Create a white noise signal source
pub fn newWhiteNoise() -> Source {
Source {
src: Box::new(WhiteNoise { rng: SmallRng::from_entropy()}),
}
}
/// Sine sweep source
///
/// # Args
///
/// - `fs` - Sample rate \[Hz\]
/// - `fl` - Lower frequency \[Hz\]
/// - `fu` - Upper frequency \[Hz\]
/// - `sweep_time` - The duration of a single sweep \[s\]
/// - `quiet_time` - Time of silence after one sweep and start of the next \[s\]
/// - `sweep_type` - The type of the sweep, see [SweepType].
pub fn newSweep(
fs: Flt,
fl: Flt,
fu: Flt,
sweep_time: Flt,
quiet_time: Flt,
sweep_type: SweepType,
) -> Result<Source> {
Ok(Source {
src: Box::new(Sweep::new(fs, fl, fu, sweep_time, quiet_time, sweep_type)?),
})
}
}
#[cfg(feature = "python-bindings")]
#[cfg_attr(feature = "python-bindings", pymethods)]
impl Source {
#[staticmethod]
#[pyo3(name = "newSine")]
fn newSine_py(fs: Flt, freq: Flt) -> PyResult<Source> {
Ok(Self::newSine(fs, freq)?)
}
#[pyo3(name = "newSilence")]
#[staticmethod]
fn newSilence_py() -> Source {
Self::newSilence()
}
#[staticmethod]
#[pyo3(name = "newWhiteNoise")]
fn newWhiteNoise_py() -> Source {
Self::newWhiteNoise()
}
#[staticmethod]
#[pyo3(name = "newSweep")]
fn newSweep_py(
fs: Flt,
fl: Flt,
fu: Flt,
sweep_time: Flt,
quiet_time: Flt,
sweep_type: SweepType,
) -> PyResult<Source> {
Ok(Self::newSweep(
fs, fl, fu, sweep_time, quiet_time, sweep_type,
)?)
}
}
#[derive(Clone, Debug)]
/// Silence source. Most simple one does only send out a 0.
struct Silence {}
impl SourceImpl for Silence {
fn genSignal_unscaled(&mut self, sig: &mut dyn ExactSizeIterator<Item = &mut Flt>) {
sig.for_each(|s| {
*s = 0.0;
});
}
fn reset(&mut self, _fs: Flt) {}
fn clone_dyn(&self) -> Box<dyn SourceImpl> {
Box::new(self.clone())
}
}
/// White noise source. Can be colored by applying a color filter to the source
#[derive(Clone, Debug)]
struct WhiteNoise {
// SmallRng is a cheap random number generator
rng: SmallRng
}
impl SourceImpl for WhiteNoise {
fn genSignal_unscaled(&mut self, sig: &mut dyn ExactSizeIterator<Item = &mut Flt>) {
sig.for_each(|s| {
*s = self.rng.sample(StandardNormal);
});
}
fn reset(&mut self, _fs: Flt) {}
fn clone_dyn(&self) -> Box<dyn SourceImpl> {
Box::new(self.clone())
}
}
/// Sine wave, with configurable frequency
#[derive(Clone, Debug)]
struct Sine {
// Sampling freq \[Hz\]
fs: Flt,
// current stored phase
phase: Flt,
// Signal frequency \[rad/s\]
omg: Flt,
}
impl Sine {
/// Create new sine source signal
///
/// Args:
///
/// * fs: Sampling freq [Hz]
/// *
pub fn new(fs: Flt, freq: Flt) -> Result<Sine> {
if fs <= 0. {
bail!("Invalid sampling frequency");
}
if freq >= fs / 2. {
bail!("Frequency of sine wave should be smaller than Nyquist frequency");
}
Ok(Sine {
fs,
phase: 0.0,
omg: 2.0 * pi * freq,
})
}
}
impl SourceImpl for Sine {
fn genSignal_unscaled(&mut self, sig: &mut dyn ExactSizeIterator<Item = &mut Flt>) {
if self.fs <= 0.0 {
sig.for_each(|s| {
*s = 0.0;
});
return;
}
sig.for_each(|s| {
*s = Flt::sin(self.phase);
self.phase += self.omg / self.fs;
self.phase %= twopi;
});
}
fn reset(&mut self, fs: Flt) {
self.fs = fs;
self.phase = 0.0;
}
fn clone_dyn(&self) -> Box<dyn SourceImpl> {
Box::new(self.clone())
}
}
#[cfg_attr(feature = "python-bindings", pyclass)]
#[derive(Debug, Clone)]
struct Sweep {
params: SweepParams,
// Generated time-periodic buffer
gen: Dcol,
N: usize,
}
impl Sweep {
fn new(
fs: Flt,
fl_: Flt,
fu_: Flt,
sweep_time: Flt,
quiet_time: Flt,
sweeptype: SweepType,
) -> Result<Self> {
let params = SweepParams::new(fs, fl_, fu_, sweep_time, quiet_time, sweeptype)?;
let gen = params.getSignal();
Ok(Sweep { params, gen, N: 0 })
}
}
// Linear forward or backward sweep phase
impl SourceImpl for Sweep {
fn genSignal_unscaled(&mut self, sig: &mut dyn ExactSizeIterator<Item = &mut Flt>) {
let sweep_iter = self.gen.as_slice().unwrap().iter().cycle().skip(self.N);
for (sig, sweep_sample) in sig.zip(sweep_iter) {
*sig = *sweep_sample;
self.N += 1;
}
// Modulo number of samples in generator
self.N %= self.gen.len();
}
fn reset(&mut self, fs: Flt) {
self.gen = self.params.reset(fs);
self.N = 0;
}
fn clone_dyn(&self) -> Box<dyn SourceImpl> {
Box::new(self.clone())
}
}
impl Deref for Source {
type Target = Box<dyn SourceImpl>;
fn deref(&self) -> &Self::Target {
&self.src
}
}
impl DerefMut for Source {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.src
}
}
/// Source for the signal generator. Implementations are sine waves, sweeps, noise.
pub trait SourceImpl: Send + Debug {
/// Generate the 'pure' source signal. Output is placed inside the `sig` argument.
fn genSignal_unscaled(&mut self, sig: &mut dyn ExactSizeIterator<Item = &mut Flt>);
/// Reset the source state, i.e. set phase to 0, etc
fn reset(&mut self, fs: Flt);
/// Used to make the Siggen struct cloneable
fn clone_dyn(&self) -> Box<dyn SourceImpl>;
}
impl Clone for Box<dyn SourceImpl> {
fn clone(&self) -> Self {
self.clone_dyn()
}
}

View File

@ -1,295 +0,0 @@
//! Sweep signal generation code
use strum::EnumMessage;
use strum_macros::{Display, EnumMessage};
use {
crate::config::*,
anyhow::{bail, Result},
};
const NITER_NEWTON: usize = 20;
const twopi: Flt = 2. * pi;
/// Enumerator representing the type of sweep source to create. Used as
/// parameter in [Siggen::newSweep].
#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
#[derive(Debug, PartialEq, Clone, Display, EnumMessage)]
pub enum SweepType {
/// Forward only logarithmic sweep, repeats itself
#[strum(message = "Forward logarithmic")]
ForwardLog,
/// Reverse only logarithmic sweep, repeats itself
#[strum(message = "Backward logarithmic")]
BackwardLog,
/// Continuous logarithmic sweep, repeats itself
#[strum(message = "Continuous logarithmic")]
ContinuousLog,
/// Forward only linear sweep, repeats itself
#[strum(message = "Forward linear")]
ForwardLin,
/// Reverse only linear sweep, repeats itself
#[strum(message = "Backward linear")]
BackwardLin,
/// Continuous linear sweep, repeats itself
#[strum(message = "Continuous linear")]
ContinuousLin,
}
#[cfg(feature = "python-bindings")]
#[cfg_attr(feature = "python-bindings", pymethods)]
impl SweepType {
#[staticmethod]
fn all() -> Vec<SweepType> {
use SweepType::*;
vec![
ForwardLin,
ForwardLog,
BackwardLin,
BackwardLog,
ContinuousLin,
ContinuousLog,
]
}
fn __str__(&self) -> String {
self.get_message().unwrap().into()
}
}
#[derive(Debug, Clone)]
pub struct SweepParams {
// These parameters are described at [Source::newSweep]
fs: Flt,
fl: Flt,
fu: Flt,
sweep_time: Flt,
quiet_time: Flt,
sweeptype: SweepType,
}
impl SweepParams {
pub fn new(
fs: Flt,
fl: Flt,
fu: Flt,
sweep_time: Flt,
quiet_time: Flt,
sweeptype: SweepType,
) -> Result<Self> {
if fs <= 0. {
bail!("Invalid sampling frequency: {} Hz", fs);
}
if fl > fu {
bail!("Lower frequency should be smaller than upper frequency");
}
if fu >= fs / 2. {
bail!("Upper frequency should be smaller than sampling frequency");
}
if sweep_time <= 0. {
bail!("Invalid sweep time, should be > 0.");
}
if 1. / sweep_time > fs / 2. {
bail!("Invalid sweep time: too short");
}
// For backward sweeps, we just reverse the start and stop frequency.
let (fl, fu) = if matches!(sweeptype, SweepType::BackwardLin | SweepType::BackwardLog) {
(fu, fl)
} else {
(fl, fu)
};
Ok(SweepParams {
fs,
fl,
fu,
sweep_time,
quiet_time,
sweeptype,
})
}
pub fn reset(&mut self, fs: Flt) -> Dcol {
self.fs = fs;
self.getSignal()
}
fn Ns(&self) -> usize {
(self.sweep_time * self.fs) as usize
}
/// Returns the phase as a function of time
fn getPhase(&self) -> Dcol {
match self.sweeptype {
SweepType::BackwardLin | SweepType::ForwardLin => self.getLinSweepFBPhase(),
SweepType::BackwardLog | SweepType::ForwardLog => self.getLogSweepFBPhase(),
SweepType::ContinuousLin => self.getLinSweepContPhase(),
SweepType::ContinuousLog => self.getLogSweepContPhase(),
}
}
pub fn getSignal(&self) -> Dcol {
let fs = self.fs;
// Number of samples in sweep
let Ns = (self.sweep_time * fs) as usize;
// Number of samples in quiet time
let Nq = (self.quiet_time * fs) as usize;
// Total number of samples
let N = Ns + Nq;
let phase = self.getPhase();
Dcol::from_iter((0..N).map(|i| if i < Ns { Flt::sin(phase[i]) } else { 0. }))
}
// Linear forward or backward sweep phase
fn getLinSweepFBPhase(&self) -> Dcol {
assert!(matches!(
self.sweeptype,
SweepType::BackwardLin | SweepType::ForwardLin
));
let (Ns, fl, fu, fs) = (self.Ns(), self.fl, self.fu, self.fs);
// Time step
let Dt = 1. / fs;
let Nsf = Ns as Flt;
let K = (Dt * (fl * Nsf + 0.5 * (Nsf - 1.) * (fu - fl))).floor();
let eps_num = K / Dt - fl * Nsf - 0.5 * (Nsf - 1.) * (fu - fl);
let eps = eps_num / (0.5 * (Nsf - 1.));
let mut phase = 0.;
Dcol::from_iter((0..Ns).map(|n| {
let freq = fl + (n as Flt - 1.) / (Ns as Flt) * (fu + eps - fl);
let phase_out = phase;
phase += twopi * Dt * freq;
phase_out
}))
}
// Logarithmic forward or backward sweep phase
fn getLogSweepFBPhase(&self) -> Dcol {
assert!(matches!(
self.sweeptype,
SweepType::BackwardLog | SweepType::ForwardLog
));
let (Ns, fl, fu, fs) = (self.Ns(), self.fl, self.fu, self.fs);
// // Time step
let Dt = 1. / fs;
let Nsf = Ns as Flt;
let mut k = fu / fl;
let K = (Dt * fl * (k - 1.) / ((k.powf(1.0 / Nsf)) - 1.)).floor();
/* Iterate k to the right solution */
(0..10).for_each(|_| {
let E = 1. + K / (Dt * fl) * (k.powf(1.0 / Nsf) - 1.) - k;
let dEdk = K / (Dt * fl) * k.powf(1.0 / Nsf) / (Nsf * k) - 1.;
k -= E / dEdk;
});
let mut phase = 0.;
Dcol::from_iter((0..Ns).map(|n| {
let nf = n as Flt;
let fnn = fl * k.powf(nf / Nsf);
let phase_old = phase;
phase += twopi * Dt * fnn;
phase_old
}))
}
// Continuous log sweep phase
fn getLogSweepContPhase(&self) -> Dcol {
assert!(matches!(self.sweeptype, SweepType::ContinuousLog));
let (Ns, fl, fu, fs) = (self.Ns(), self.fl, self.fu, self.fs);
// // Time step
let Dt = 1. / fs;
let Nf = Ns / 2;
let Nff = Nf as Flt;
let Nb = Ns - Nf;
let Nbf = Nb as Flt;
let k1 = fu / fl;
let phif1 = twopi * Dt * fl * (k1 - 1.) / (k1.powf(1.0 / Nff) - 1.);
let K =
(phif1 / twopi + Dt * fu * (1. / k1 - 1.) / ((1. / k1).powf(1.0 / Nbf) - 1.)).floor();
let mut k = k1;
/* Newton iterations to converge k to the value such that the sweep is
* continuous */
(0..NITER_NEWTON).for_each(|_| {
let E = (k - 1.) / (k.powf(1.0 / Nff) - 1.) + (k - 1.) / (1. - k.powf(-1.0 / Nbf))
- K / Dt / fl;
// /* All parts of the derivative of above error E to k */
let dEdk1 = 1. / (k.powf(1.0 / Nff) - 1.);
let dEdk2 = (1. / k - 1.) / (k.powf(-1.0 / Nbf) - 1.);
let dEdk3 = -1. / (k * (k.powf(-1.0 / Nbf) - 1.));
let dEdk4 = k.powf(-1.0 / Nbf) * (1. / k - 1.)
/ (Nbf * Flt::powi(Flt::powf(k, -1.0 / Nbf) - 1., 2));
let dEdk5 = -Flt::powf(k, 1.0 / Nff) * (k - 1.)
/ (Nff * k * Flt::powi(Flt::powf(k, 1.0 / Nff) - 1., 2));
let dEdk = dEdk1 + dEdk2 + dEdk3 + dEdk4 + dEdk5;
k -= E / dEdk;
});
let mut phase = 0.;
Dcol::from_iter((0..Ns).map(|n| {
let nf = n as Flt;
let fnn = if n <= Nf {
fl * k.powf(nf / Nff)
} else {
fl * k * (1. / k).powf((nf - Nff) / Nbf)
};
let phase_old = phase;
phase += twopi * Dt * fnn;
phase_old
}))
}
// Continuous linear sweep phase
fn getLinSweepContPhase(&self) -> Dcol {
assert!(matches!(self.sweeptype, SweepType::ContinuousLin));
let (Ns, fl, fu, fs) = (self.Ns(), self.fl, self.fu, self.fs);
let Dt = 1. / fs;
let Nf = Ns / 2;
let Nb = Ns - Nf;
let Nff = Nf as Flt;
let Nbf = Nb as Flt;
/* Phi halfway */
let phih = twopi * Dt * (fl * Nff + 0.5 * (Nff - 1.) * (fu - fl));
let K = (phih / twopi + Dt * (fu * Nbf - (Nb as Flt - 1.) * (fu - fl))).floor();
let eps_num1 = (K - phih / twopi) / Dt;
let eps_num2 = -fu * Nbf + (Nbf - 1.) * (fu - fl);
let eps = (eps_num1 + eps_num2) / (0.5 * (Nbf + 1.));
let mut phase = 0.;
Dcol::from_iter((0..Ns).map(|n| {
let nf = n as Flt;
let freq = if n < Nf {
fl + nf / Nff * (fu - fl)
} else {
fu - (nf - Nff) / Nbf * (fu + eps - fl)
};
let phase_out = phase;
phase += twopi * Dt * freq;
phase_out
}))
}
}
#[cfg(test)]
mod test {
use approx::assert_abs_diff_eq;
use super::*;
#[test]
fn test_phase_linsweep1() {
let fs = 10.;
let fl = 1.;
let fu = 1.;
let phase = SweepParams::new(fs, fl, fu, 10., 0., SweepType::ForwardLin)
.unwrap()
.getLinSweepFBPhase();
assert_abs_diff_eq!(phase[10], &(twopi));
}
}

View File

@ -285,7 +285,7 @@ mod test {
.build() .build()
.unwrap(); .unwrap();
let mut siggen = Siggen::newSine(1., 1, 1000.).unwrap(); let mut siggen = Siggen::newSine(1, 1000.);
siggen.setAllMute(false); siggen.setAllMute(false);
siggen.reset(fs); siggen.reset(fs);
let mut data = vec![0.; N]; let mut data = vec![0.; N];

View File

@ -3,7 +3,11 @@ use crate::config::*;
use strum::EnumMessage; use strum::EnumMessage;
use strum_macros::Display; use strum_macros::Display;
/// Time weighting to use in level detection of Sound Level Meter. /// Time weighting to use in level detection of Sound Level Meter.
#[cfg_attr(feature = "python-bindings", pyclass(eq))] ///
// Do the following when Pyo3 0.22 can finally be used combined with rust-numpy:
// #[cfg_attr(feature = "python-bindings", pyclass(eq))]
// For now:
#[cfg_attr(feature = "python-bindings", pyclass)]
#[derive(Clone, Copy, Debug, PartialEq, Display)] #[derive(Clone, Copy, Debug, PartialEq, Display)]
pub enum TimeWeighting { pub enum TimeWeighting {
// I know that the curly braces here are not required and add some // I know that the curly braces here are not required and add some
@ -33,6 +37,10 @@ pub enum TimeWeighting {
#[cfg(feature = "python-bindings")] #[cfg(feature = "python-bindings")]
#[cfg_attr(feature = "python-bindings", pymethods)] #[cfg_attr(feature = "python-bindings", pymethods)]
impl TimeWeighting { impl TimeWeighting {
// This method is still required in Pyo3 0.21, not anymore in 0.22
fn __eq__(&self, other: &Self) -> bool {
self == other
}
fn __str__(&self) -> String { fn __str__(&self) -> String {
format!("{self}") format!("{self}")
} }