Compare commits

...

8 Commits

Author SHA1 Message Date
636213c2b7 Bump 0.6.4 2024-10-26 21:54:54 +02:00
8a573266df Removed dependency on ndarray_rand. Crate is not updated. Updated pyo3 to new version and updated enum pyclass derive macros. Switch to SmallRng for white noise random number generation. 2024-10-26 21:53:56 +02:00
0567e7fb92 Added siggencommand. Exported sweep types. Added debug derives on some more classes. StreamMgr now returns result on stream commands, by polling back on a channel for result values. 2024-10-26 11:53:34 +02:00
45da6370ec Wrappers for sweep signal. Some cleanup and documentation 2024-10-13 13:12:37 +02:00
cde2c74467 Implemented sweep signal generator. Some sweep implementations can error when something goes wrong. Changing params on running signal generator give results back that need to be handled 2024-10-13 13:02:49 +02:00
8ee5fcbf02 Split up siggen code in source and siggen submods 2024-10-08 12:07:18 +02:00
4da8a1c74a Added RtAps::reset() method, in progress of placing signal generator code in module. Therefore, not working intermediate code 2024-10-08 12:01:21 +02:00
b7c2f9c3b8 Removed pub on method that does not need to be pub 2024-10-08 11:54:00 +02:00
34 changed files with 1316 additions and 617 deletions

1
.gitignore vendored
View File

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

View File

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

View File

@ -1,10 +1,10 @@
use anyhow::Result;
use crossbeam::channel::{ unbounded, Receiver, TryRecvError };
use lasprs::daq::{ DaqConfig, StreamMgr, StreamStatus, StreamType };
use crossbeam::channel::{unbounded, Receiver, TryRecvError};
use lasprs::daq::{DaqConfig, StreamMgr, StreamStatus, StreamType};
use lasprs::siggen::Siggen;
use std::io;
use std::time::Duration;
use std::{ thread, time };
use std::{thread, time};
// use
/// Spawns a thread and waits for a single line, pushes it to the receiver and returns
@ -30,13 +30,13 @@ fn main() -> Result<()> {
let stdin_channel = stdin_channel_wait_for_return();
println!("Creating signal generator...");
let mut siggen = Siggen::newSine(2, 432.0);
let mut siggen = Siggen::newSine(1., 2, 432.0).unwrap();
// Reduce all gains a bit...
siggen.setAllGains(0.1);
// Apply signal generator
smgr.setSiggen(siggen);
smgr.setSiggen(siggen)?;
println!("Starting stream...");
let devs = smgr.getDeviceInfo();

View File

@ -1,16 +1,15 @@
use anyhow::Result;
use crossbeam::channel::{unbounded, Receiver, TryRecvError};
use lasprs::daq::{StreamMgr, StreamStatus, StreamType};
use lasprs::siggen::Siggen;
use lasprs::siggen::{Siggen, SweepType};
use std::io;
use std::{thread, time};
// use
/// Spawns a thread and waits for a single line, pushes it to the receiver and returns
fn stdin_channel_wait_for_return() -> Receiver<String> {
let (tx, rx) = unbounded();
thread::spawn(move || {
thread::spawn(move || {
let mut buffer = String::new();
io::stdin().read_line(&mut buffer).unwrap();
// Do not care whether we succeed here.
@ -28,7 +27,19 @@ fn main() -> Result<()> {
let stdin_channel = stdin_channel_wait_for_return();
println!("Creating signal generator...");
let mut siggen = Siggen::newSine(2, 432.);
// let mut siggen = Siggen::newSine(44100., 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
// siggen.setDCOffset(&[0.1, 0.]);
@ -36,12 +47,11 @@ fn main() -> Result<()> {
// Reduce all gains a bit...
siggen.setAllGains(0.1);
println!("Starting stream...");
smgr.startDefaultOutputStream()?;
// Apply signal generator
smgr.setSiggen(siggen);
smgr.setSiggen(siggen)?;
println!("Press <enter> key to quit...");
'infy: loop {
@ -52,12 +62,12 @@ fn main() -> Result<()> {
}
sleep(100);
match smgr.getStatus(StreamType::Output) {
StreamStatus::NotRunning{} => {
StreamStatus::NotRunning {} => {
println!("Stream is not running?");
break 'infy;
}
StreamStatus::Running{} => {}
StreamStatus::Error{e} => {
StreamStatus::Running {} => {}
StreamStatus::Error { e } => {
println!("Stream error: {}", e);
break 'infy;
}

View File

@ -32,10 +32,7 @@ pub trait Stream {
}
/// Stream API descriptor: type and corresponding text
// 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)]
#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
#[derive(strum_macros::EnumMessage, Debug, Clone, PartialEq, Serialize, Deserialize, strum_macros::Display)]
#[allow(dead_code)]
pub enum StreamApiDescr {

View File

@ -7,10 +7,7 @@ use crate::config::*;
/// Data type description for samples coming from a stream
#[derive(strum_macros::EnumMessage, PartialEq, Copy, Debug, Clone, Serialize, Deserialize)]
// 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)]
#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
#[allow(dead_code)]
pub enum DataType {
/// 32-bit floats

View File

@ -50,10 +50,7 @@ use api::*;
use crate::config::*;
/// Stream types that can be started
///
// 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)]
#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
#[derive(PartialEq, Clone, Copy)]
pub enum StreamType {
/// Input-only stream

View File

@ -7,9 +7,7 @@ use strum_macros;
use serde::{Serialize, Deserialize};
/// Physical quantities that are I/O of a Daq device.
// 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)]
#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
#[derive(PartialEq, Serialize, Deserialize, strum_macros::EnumMessage, Debug, Clone, Copy)]
#[allow(dead_code)]
pub enum Qty {

View File

@ -161,28 +161,32 @@ impl Recording {
nchannels: usize,
) -> Result<()> {
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) => {
let arr = ndarray::ArrayView2::<i8>::from_shape((framesPerBlock, nchannels), dat)?;
let arr = ndarray15p6::ArrayView2::<i8>::from_shape((framesPerBlock, nchannels), dat)?;
ds.write_slice(arr, (ctr, .., ..))?;
}
RawStreamData::Datai16(dat) => {
let arr =
ndarray::ArrayView2::<i16>::from_shape((framesPerBlock, nchannels), dat)?;
ndarray15p6::ArrayView2::<i16>::from_shape((framesPerBlock, nchannels), dat)?;
ds.write_slice(arr, (ctr, .., ..))?;
}
RawStreamData::Datai32(dat) => {
let arr =
ndarray::ArrayView2::<i32>::from_shape((framesPerBlock, nchannels), dat)?;
ndarray15p6::ArrayView2::<i32>::from_shape((framesPerBlock, nchannels), dat)?;
ds.write_slice(arr, (ctr, .., ..))?;
}
RawStreamData::Dataf32(dat) => {
let arr =
ndarray::ArrayView2::<f32>::from_shape((framesPerBlock, nchannels), dat)?;
ndarray15p6::ArrayView2::<f32>::from_shape((framesPerBlock, nchannels), dat)?;
ds.write_slice(arr, (ctr, .., ..))?;
}
RawStreamData::Dataf64(dat) => {
let arr =
ndarray::ArrayView2::<f64>::from_shape((framesPerBlock, nchannels), dat)?;
ndarray15p6::ArrayView2::<f64>::from_shape((framesPerBlock, nchannels), dat)?;
ds.write_slice(arr, (ctr, .., ..))?;
}
}

View File

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

View File

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

View File

@ -2,13 +2,8 @@ use strum_macros::Display;
use crate::config::*;
/// Errors that happen in a stream
#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
#[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 {
/// Input overrun
#[strum(

View File

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

View File

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

View File

@ -2,7 +2,7 @@ use super::*;
use super::seriesbiquad::*;
use rayon::prelude::*;
#[cfg_attr(feature = "python-bindings", pyclass)]
#[derive(Clone)]
#[derive(Clone, Debug)]
/// 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
/// parallel filters, or it can directly be used to eq a signal. For the latter process, also a

View File

@ -4,6 +4,8 @@
//! 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.
#![allow(non_snake_case)]
use std::fmt::Debug;
use super::config::*;
mod biquad;
@ -23,7 +25,7 @@ pub use seriesbiquad::SeriesBiquad;
pub use zpkmodel::{PoleOrZero, ZPKModel, FilterSpec};
/// Implementations of this trait are able to DSP-filter input data.
pub trait Filter: Send {
pub trait Filter: Send + Debug {
//! 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
@ -39,7 +41,6 @@ pub trait Filter: Send {
}
/// Implementations are able to generate transfer functions of itself
pub trait TransferFunction<'a, T>: Send
where
T: AsArray<'a, Flt>,

View File

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

View File

@ -2,7 +2,10 @@
//!
//!
use crate::config::*;
use ndarray::ArrayView1;
use ndarray::{ArrayView1, IntoDimension};
use rand::{rngs::SmallRng, thread_rng, Rng, SeedableRng};
use rand_distr::StandardNormal;
use smallvec::Array;
/// Compute maximum value of an array of float values
pub fn max<T>(arr: ArrayView<Flt, T>) -> Flt
@ -33,3 +36,22 @@ where
{
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,11 +308,9 @@ impl AvPowerSpectra {
#[cfg(test)]
mod test {
use approx::assert_abs_diff_eq;
use ndarray_rand::rand_distr::Normal;
use ndarray_rand::RandomExt;
use super::*;
use crate::config::*;
use crate::{config::*, math::randNormal};
use super::{ApsMode, AvPowerSpectra, CPSResult, Overlap, WindowType};
use Overlap::Percentage;
@ -359,8 +357,7 @@ mod test {
let mut aps = AvPowerSpectra::new(settings);
assert_eq!(aps.overlap_keep, 0);
let distr = Normal::new(1.0, 1.0).expect("Distribution cannot be built");
let timedata_some = Dmat::random((nfft, 1), distr);
let timedata_some = randNormal((nfft,1));
let timedata_zeros = Dmat::zeros((nfft, 1));
// Clone here, as first_result reference is overwritten by subsequent
@ -382,8 +379,7 @@ mod test {
#[test]
fn test_tf1() {
let nfft = 4800;
let distr = Normal::new(1.0, 1.0).unwrap();
let mut timedata = Dmat::random((nfft, 1), distr);
let mut timedata = randNormal((nfft, 1));
timedata
.push_column(timedata.column(0).mapv(|a| 2. * a).view())
.unwrap();
@ -405,8 +401,7 @@ mod test {
#[test]
fn test_tf2() {
let nfft = 4800;
let distr = Normal::new(1.0, 1.0).unwrap();
let mut timedata = Dmat::random((nfft, 1), distr);
let mut timedata = randNormal((nfft, 1));
timedata
.push_column(timedata.column(0).mapv(|a| 2. * a).view())
.unwrap();
@ -431,8 +426,7 @@ mod test {
#[test]
fn test_ap() {
let nfft = 1024;
let distr = Normal::new(1.0, 1.0).unwrap();
let timedata = Dmat::random((150 * nfft, 1), distr);
let timedata = randNormal((150 * nfft, 1));
let timedata_mean_square = (&timedata * &timedata).sum() / (timedata.len() as Flt);
for wt in [

View File

@ -2,11 +2,7 @@ use crate::config::*;
use strum::IntoEnumIterator;
use strum_macros::{Display, EnumIter, EnumMessage};
/// Sound level frequency weighting type (A, C, Z)
// 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)]
#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
#[derive(Copy, Display, Debug, EnumMessage, Default, Clone, PartialEq, EnumIter)]
pub enum FreqWeighting {
/// A-weighting

View File

@ -252,7 +252,6 @@ mod test {
use approx::{abs_diff_eq, assert_relative_eq, assert_ulps_eq, ulps_eq};
// For absolute value
use num::complex::ComplexFloat;
use rand_distr::StandardNormal;
/// Generate a sine wave at the order i
fn generate_sinewave(nfft: usize, order: usize) -> Dcol {
@ -267,6 +266,8 @@ mod test {
)
}
use crate::math::randNormal;
use super::*;
#[test]
/// Test whether DC part of single-sided FFT has right properties
@ -349,7 +350,6 @@ mod test {
);
}
use ndarray_rand::RandomExt;
// Test parseval's theorem for some random data
#[test]
fn test_parseval() {
@ -358,7 +358,7 @@ mod test {
let mut ps = PowerSpectra::newFromWindow(rect);
// Start with a time signal
let t: Dmat = Dmat::random((nfft, 1), StandardNormal);
let t: Dmat = randNormal((nfft,1));
let tavg = t.sum() / (nfft as Flt);
let t_dc_power = tavg.powi(2);
@ -394,7 +394,7 @@ mod test {
let mut ps = PowerSpectra::newFromWindow(window);
// Start with a time signal
let t: Dmat = 2. * Dmat::random((nfft, 1), StandardNormal);
let t: Dmat = randNormal((nfft,1));
let tavg = t.sum() / (nfft as Flt);
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
/// extracted by blocks of other size. Also, we can keep samples in a buffer to
/// create, for example, overlapping windows of time data.
#[derive(Default, Debug)]
#[derive(Default, Debug, Clone)]
pub struct TimeBuffer {
data: Vec<VecDeque<Flt>>,
}

View File

@ -73,11 +73,8 @@ fn hamming(N: usize) -> Dcol {
/// * Blackman
///
/// The [WindowType::default] is [WindowType::Hann].
#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
#[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 {
/// Von Hann window
#[default]

View File

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

View File

@ -136,6 +136,11 @@ impl RtAps {
let mut lck = self.status.lock();
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 {
fn drop(&mut self) {
@ -158,6 +163,11 @@ impl RtAps {
}
None
}
#[pyo3(name = "reset")]
fn reset_py(&self) {
self.reset()
}
}
#[cfg(test)]

View File

@ -1,468 +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);
//!
//! ```
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);
}
}

31
src/siggen/mod.rs Normal file
View File

@ -0,0 +1,31 @@
//! 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;

345
src/siggen/siggen.rs Normal file
View File

@ -0,0 +1,345 @@
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

@ -0,0 +1,78 @@
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;
});
}
}

39
src/siggen/siggencmd.rs Normal file
View File

@ -0,0 +1,39 @@
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,
},
}

265
src/siggen/source.rs Normal file
View File

@ -0,0 +1,265 @@
//! 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()
}
}

295
src/siggen/sweep.rs Normal file
View File

@ -0,0 +1,295 @@
//! 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()
.unwrap();
let mut siggen = Siggen::newSine(1, 1000.);
let mut siggen = Siggen::newSine(1., 1, 1000.).unwrap();
siggen.setAllMute(false);
siggen.reset(fs);
let mut data = vec![0.; N];

View File

@ -3,11 +3,7 @@ use crate::config::*;
use strum::EnumMessage;
use strum_macros::Display;
/// Time weighting to use in level detection of Sound Level Meter.
///
// 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)]
#[cfg_attr(feature = "python-bindings", pyclass(eq))]
#[derive(Clone, Copy, Debug, PartialEq, Display)]
pub enum TimeWeighting {
// I know that the curly braces here are not required and add some
@ -37,10 +33,6 @@ pub enum TimeWeighting {
#[cfg(feature = "python-bindings")]
#[cfg_attr(feature = "python-bindings", pymethods)]
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 {
format!("{self}")
}