Compare commits
8 Commits
58093dd5cd
...
636213c2b7
Author | SHA1 | Date | |
---|---|---|---|
636213c2b7 | |||
8a573266df | |||
0567e7fb92 | |||
45da6370ec | |||
cde2c74467 | |||
8ee5fcbf02 | |||
4da8a1c74a | |||
b7c2f9c3b8 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -6,3 +6,4 @@ python/lasprs/_lasprs*
|
||||
.vscode/launch.json
|
||||
.vscode
|
||||
examples_py/.ipynb_checkpoints
|
||||
.ipynb_checkpoints
|
||||
|
19
Cargo.toml
19
Cargo.toml
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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, .., ..))?;
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
||||
|
@ -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]);
|
||||
|
@ -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(
|
||||
|
@ -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]
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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>,
|
||||
|
@ -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>()?;
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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 [
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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>>,
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -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]
|
||||
|
@ -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)]
|
||||
|
468
src/siggen.rs
468
src/siggen.rs
@ -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
31
src/siggen/mod.rs
Normal 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
345
src/siggen/siggen.rs
Normal 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);
|
||||
}
|
||||
}
|
78
src/siggen/siggenchannel.rs
Normal file
78
src/siggen/siggenchannel.rs
Normal 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
39
src/siggen/siggencmd.rs
Normal 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
265
src/siggen/source.rs
Normal 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
295
src/siggen/sweep.rs
Normal 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));
|
||||
}
|
||||
}
|
@ -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];
|
||||
|
@ -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}")
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user