Compare commits
No commits in common. "636213c2b7c784f295b09c7282a6706af5ca1eda" and "58093dd5cd2edfac521821bf243610ca7c023cb9" have entirely different histories.
636213c2b7
...
58093dd5cd
1
.gitignore
vendored
1
.gitignore
vendored
@ -6,4 +6,3 @@ python/lasprs/_lasprs*
|
|||||||
.vscode/launch.json
|
.vscode/launch.json
|
||||||
.vscode
|
.vscode
|
||||||
examples_py/.ipynb_checkpoints
|
examples_py/.ipynb_checkpoints
|
||||||
.ipynb_checkpoints
|
|
||||||
|
19
Cargo.toml
19
Cargo.toml
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lasprs"
|
name = "lasprs"
|
||||||
version = "0.6.4"
|
version = "0.6.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["J.A. de Jong <j.a.dejong@ascee.nl>"]
|
authors = ["J.A. de Jong <j.a.dejong@ascee.nl>"]
|
||||||
description = "Library for Acoustic Signal Processing (Rust edition, with optional Python bindings via pyo3)"
|
description = "Library for Acoustic Signal Processing (Rust edition, with optional Python bindings via pyo3)"
|
||||||
@ -16,15 +16,11 @@ crate-type = ["cdylib", "rlib"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Error handling
|
# Error handling
|
||||||
anyhow = "1.0.91"
|
anyhow = "1.0.86"
|
||||||
|
|
||||||
# Numerics
|
# Numerics
|
||||||
# Optional future feature for ndarray: blas
|
# Optional future feature for ndarray: blas
|
||||||
ndarray = { version = "0.16.1", features = ["rayon"] }
|
ndarray = { version = "0.15.6", features = ["rayon"] }
|
||||||
|
|
||||||
# This is required for HDF5, as it apparently doesn't update anymore.
|
|
||||||
ndarray15p6 = { package = "ndarray", version = "0.15.6", features = ["rayon"] }
|
|
||||||
|
|
||||||
num = "0.4.3"
|
num = "0.4.3"
|
||||||
# blas-src = { version = "0.8", features = ["openblas"] }
|
# blas-src = { version = "0.8", features = ["openblas"] }
|
||||||
# openblas-src = { version = "0.10", features = ["cblas", "system"] }
|
# openblas-src = { version = "0.10", features = ["cblas", "system"] }
|
||||||
@ -33,15 +29,15 @@ num = "0.4.3"
|
|||||||
rayon = "1.10.0"
|
rayon = "1.10.0"
|
||||||
|
|
||||||
# Python bindings
|
# Python bindings
|
||||||
pyo3 = { version = "0.22.5", optional = true, features = [
|
pyo3 = { version = "0.21.2", optional = true, features = [
|
||||||
"extension-module",
|
"extension-module",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
] }
|
] }
|
||||||
# Python bindings for Numpy arrays
|
# Python bindings for Numpy arrays
|
||||||
numpy = { version = "0.22.0", optional = true }
|
numpy = { version = "0.21.0", optional = true }
|
||||||
|
|
||||||
# White noise etc
|
# White noise etc
|
||||||
rand = { version = "0.8.5", features = ["small_rng"] }
|
rand = "0.8.5"
|
||||||
rand_distr = "0.4.3"
|
rand_distr = "0.4.3"
|
||||||
|
|
||||||
# Cross-platform audio lib
|
# Cross-platform audio lib
|
||||||
@ -109,6 +105,9 @@ smallvec = "1.13.2"
|
|||||||
# Compile time constant floating point operations
|
# Compile time constant floating point operations
|
||||||
softfloat = "1.0.0"
|
softfloat = "1.0.0"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
ndarray-rand = "0.14.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["f64", "cpal-api", "record"]
|
default = ["f64", "cpal-api", "record"]
|
||||||
# Use this to test if everything works well in f32
|
# Use this to test if everything works well in f32
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use crossbeam::channel::{unbounded, Receiver, TryRecvError};
|
use crossbeam::channel::{ unbounded, Receiver, TryRecvError };
|
||||||
use lasprs::daq::{DaqConfig, StreamMgr, StreamStatus, StreamType};
|
use lasprs::daq::{ DaqConfig, StreamMgr, StreamStatus, StreamType };
|
||||||
use lasprs::siggen::Siggen;
|
use lasprs::siggen::Siggen;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::{thread, time};
|
use std::{ thread, time };
|
||||||
// use
|
// use
|
||||||
|
|
||||||
/// Spawns a thread and waits for a single line, pushes it to the receiver and returns
|
/// Spawns a thread and waits for a single line, pushes it to the receiver and returns
|
||||||
@ -30,13 +30,13 @@ fn main() -> Result<()> {
|
|||||||
let stdin_channel = stdin_channel_wait_for_return();
|
let stdin_channel = stdin_channel_wait_for_return();
|
||||||
|
|
||||||
println!("Creating signal generator...");
|
println!("Creating signal generator...");
|
||||||
let mut siggen = Siggen::newSine(1., 2, 432.0).unwrap();
|
let mut siggen = Siggen::newSine(2, 432.0);
|
||||||
|
|
||||||
// Reduce all gains a bit...
|
// Reduce all gains a bit...
|
||||||
siggen.setAllGains(0.1);
|
siggen.setAllGains(0.1);
|
||||||
|
|
||||||
// Apply signal generator
|
// Apply signal generator
|
||||||
smgr.setSiggen(siggen)?;
|
smgr.setSiggen(siggen);
|
||||||
|
|
||||||
println!("Starting stream...");
|
println!("Starting stream...");
|
||||||
let devs = smgr.getDeviceInfo();
|
let devs = smgr.getDeviceInfo();
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use crossbeam::channel::{unbounded, Receiver, TryRecvError};
|
use crossbeam::channel::{unbounded, Receiver, TryRecvError};
|
||||||
use lasprs::daq::{StreamMgr, StreamStatus, StreamType};
|
use lasprs::daq::{StreamMgr, StreamStatus, StreamType};
|
||||||
use lasprs::siggen::{Siggen, SweepType};
|
use lasprs::siggen::Siggen;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::{thread, time};
|
use std::{thread, time};
|
||||||
// use
|
// use
|
||||||
|
|
||||||
/// Spawns a thread and waits for a single line, pushes it to the receiver and returns
|
/// Spawns a thread and waits for a single line, pushes it to the receiver and returns
|
||||||
fn stdin_channel_wait_for_return() -> Receiver<String> {
|
fn stdin_channel_wait_for_return() -> Receiver<String> {
|
||||||
|
|
||||||
let (tx, rx) = unbounded();
|
let (tx, rx) = unbounded();
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let mut buffer = String::new();
|
let mut buffer = String::new();
|
||||||
io::stdin().read_line(&mut buffer).unwrap();
|
io::stdin().read_line(&mut buffer).unwrap();
|
||||||
// Do not care whether we succeed here.
|
// Do not care whether we succeed here.
|
||||||
@ -27,19 +28,7 @@ fn main() -> Result<()> {
|
|||||||
let stdin_channel = stdin_channel_wait_for_return();
|
let stdin_channel = stdin_channel_wait_for_return();
|
||||||
|
|
||||||
println!("Creating signal generator...");
|
println!("Creating signal generator...");
|
||||||
// let mut siggen = Siggen::newSine(44100., 2, 432.)?;
|
let mut siggen = Siggen::newSine(2, 432.);
|
||||||
let mut siggen = Siggen::newSweep(
|
|
||||||
44100.,
|
|
||||||
1, // nchannels: usize,
|
|
||||||
100., // fl: Flt,
|
|
||||||
10000., //fu: Flt,
|
|
||||||
1.0, // sweep_time: Flt,
|
|
||||||
// 1.0, //quiet_time: Flt,
|
|
||||||
0., //quiet_time: Flt,
|
|
||||||
// SweepType::ForwardLin//sweep_type: SweepType,
|
|
||||||
// SweepType::ForwardLog, //sweep_type: SweepType,
|
|
||||||
SweepType::ContinuousLog, //sweep_type: SweepType,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Some things that can be done
|
// Some things that can be done
|
||||||
// siggen.setDCOffset(&[0.1, 0.]);
|
// siggen.setDCOffset(&[0.1, 0.]);
|
||||||
@ -47,11 +36,12 @@ fn main() -> Result<()> {
|
|||||||
// Reduce all gains a bit...
|
// Reduce all gains a bit...
|
||||||
siggen.setAllGains(0.1);
|
siggen.setAllGains(0.1);
|
||||||
|
|
||||||
|
|
||||||
println!("Starting stream...");
|
println!("Starting stream...");
|
||||||
smgr.startDefaultOutputStream()?;
|
smgr.startDefaultOutputStream()?;
|
||||||
|
|
||||||
// Apply signal generator
|
// Apply signal generator
|
||||||
smgr.setSiggen(siggen)?;
|
smgr.setSiggen(siggen);
|
||||||
|
|
||||||
println!("Press <enter> key to quit...");
|
println!("Press <enter> key to quit...");
|
||||||
'infy: loop {
|
'infy: loop {
|
||||||
@ -62,12 +52,12 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
sleep(100);
|
sleep(100);
|
||||||
match smgr.getStatus(StreamType::Output) {
|
match smgr.getStatus(StreamType::Output) {
|
||||||
StreamStatus::NotRunning {} => {
|
StreamStatus::NotRunning{} => {
|
||||||
println!("Stream is not running?");
|
println!("Stream is not running?");
|
||||||
break 'infy;
|
break 'infy;
|
||||||
}
|
}
|
||||||
StreamStatus::Running {} => {}
|
StreamStatus::Running{} => {}
|
||||||
StreamStatus::Error { e } => {
|
StreamStatus::Error{e} => {
|
||||||
println!("Stream error: {}", e);
|
println!("Stream error: {}", e);
|
||||||
break 'infy;
|
break 'infy;
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,10 @@ pub trait Stream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Stream API descriptor: type and corresponding text
|
/// Stream API descriptor: type and corresponding text
|
||||||
#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
|
// Do the following when Pyo3 0.22 can finally be used combined with rust-numpy:
|
||||||
|
//#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
|
||||||
|
// For now:
|
||||||
|
#[cfg_attr(feature = "python-bindings", pyclass)]
|
||||||
#[derive(strum_macros::EnumMessage, Debug, Clone, PartialEq, Serialize, Deserialize, strum_macros::Display)]
|
#[derive(strum_macros::EnumMessage, Debug, Clone, PartialEq, Serialize, Deserialize, strum_macros::Display)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub enum StreamApiDescr {
|
pub enum StreamApiDescr {
|
||||||
|
@ -7,7 +7,10 @@ use crate::config::*;
|
|||||||
|
|
||||||
/// Data type description for samples coming from a stream
|
/// Data type description for samples coming from a stream
|
||||||
#[derive(strum_macros::EnumMessage, PartialEq, Copy, Debug, Clone, Serialize, Deserialize)]
|
#[derive(strum_macros::EnumMessage, PartialEq, Copy, Debug, Clone, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
|
// Do the following when Pyo3 0.22 can finally be used combined with rust-numpy:
|
||||||
|
//#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
|
||||||
|
// For now:
|
||||||
|
#[cfg_attr(feature = "python-bindings", pyclass)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub enum DataType {
|
pub enum DataType {
|
||||||
/// 32-bit floats
|
/// 32-bit floats
|
||||||
|
@ -50,7 +50,10 @@ use api::*;
|
|||||||
use crate::config::*;
|
use crate::config::*;
|
||||||
/// Stream types that can be started
|
/// Stream types that can be started
|
||||||
///
|
///
|
||||||
#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
|
// Do the following when Pyo3 0.22 can finally be used combined with rust-numpy:
|
||||||
|
// #[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
|
||||||
|
// For now:
|
||||||
|
#[cfg_attr(feature = "python-bindings", pyclass)]
|
||||||
#[derive(PartialEq, Clone, Copy)]
|
#[derive(PartialEq, Clone, Copy)]
|
||||||
pub enum StreamType {
|
pub enum StreamType {
|
||||||
/// Input-only stream
|
/// Input-only stream
|
||||||
|
@ -7,7 +7,9 @@ use strum_macros;
|
|||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
/// Physical quantities that are I/O of a Daq device.
|
/// Physical quantities that are I/O of a Daq device.
|
||||||
#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
|
// Do the following when Pyo3 0.22 can finally be used combined with rust-numpy:
|
||||||
|
// #[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
|
||||||
|
#[cfg_attr(feature = "python-bindings", pyclass)]
|
||||||
#[derive(PartialEq, Serialize, Deserialize, strum_macros::EnumMessage, Debug, Clone, Copy)]
|
#[derive(PartialEq, Serialize, Deserialize, strum_macros::EnumMessage, Debug, Clone, Copy)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub enum Qty {
|
pub enum Qty {
|
||||||
|
@ -161,32 +161,28 @@ impl Recording {
|
|||||||
nchannels: usize,
|
nchannels: usize,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
match data.getRaw() {
|
match data.getRaw() {
|
||||||
// The code below uses ndarray 0.15.6, which is the version required
|
|
||||||
// to communicate with rust-hdf5. It requires input to be C-ordered,
|
|
||||||
// or interleaved. This happens to be the default for ndarray as
|
|
||||||
// well.
|
|
||||||
RawStreamData::Datai8(dat) => {
|
RawStreamData::Datai8(dat) => {
|
||||||
let arr = ndarray15p6::ArrayView2::<i8>::from_shape((framesPerBlock, nchannels), dat)?;
|
let arr = ndarray::ArrayView2::<i8>::from_shape((framesPerBlock, nchannels), dat)?;
|
||||||
ds.write_slice(arr, (ctr, .., ..))?;
|
ds.write_slice(arr, (ctr, .., ..))?;
|
||||||
}
|
}
|
||||||
RawStreamData::Datai16(dat) => {
|
RawStreamData::Datai16(dat) => {
|
||||||
let arr =
|
let arr =
|
||||||
ndarray15p6::ArrayView2::<i16>::from_shape((framesPerBlock, nchannels), dat)?;
|
ndarray::ArrayView2::<i16>::from_shape((framesPerBlock, nchannels), dat)?;
|
||||||
ds.write_slice(arr, (ctr, .., ..))?;
|
ds.write_slice(arr, (ctr, .., ..))?;
|
||||||
}
|
}
|
||||||
RawStreamData::Datai32(dat) => {
|
RawStreamData::Datai32(dat) => {
|
||||||
let arr =
|
let arr =
|
||||||
ndarray15p6::ArrayView2::<i32>::from_shape((framesPerBlock, nchannels), dat)?;
|
ndarray::ArrayView2::<i32>::from_shape((framesPerBlock, nchannels), dat)?;
|
||||||
ds.write_slice(arr, (ctr, .., ..))?;
|
ds.write_slice(arr, (ctr, .., ..))?;
|
||||||
}
|
}
|
||||||
RawStreamData::Dataf32(dat) => {
|
RawStreamData::Dataf32(dat) => {
|
||||||
let arr =
|
let arr =
|
||||||
ndarray15p6::ArrayView2::<f32>::from_shape((framesPerBlock, nchannels), dat)?;
|
ndarray::ArrayView2::<f32>::from_shape((framesPerBlock, nchannels), dat)?;
|
||||||
ds.write_slice(arr, (ctr, .., ..))?;
|
ds.write_slice(arr, (ctr, .., ..))?;
|
||||||
}
|
}
|
||||||
RawStreamData::Dataf64(dat) => {
|
RawStreamData::Dataf64(dat) => {
|
||||||
let arr =
|
let arr =
|
||||||
ndarray15p6::ArrayView2::<f64>::from_shape((framesPerBlock, nchannels), dat)?;
|
ndarray::ArrayView2::<f64>::from_shape((framesPerBlock, nchannels), dat)?;
|
||||||
ds.write_slice(arr, (ctr, .., ..))?;
|
ds.write_slice(arr, (ctr, .., ..))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ use super::streammgr::SharedInQueue;
|
|||||||
|
|
||||||
|
|
||||||
/// Commands that can be sent to a running stream
|
/// Commands that can be sent to a running stream
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum StreamCommand {
|
pub enum StreamCommand {
|
||||||
/// Add a new queue to a running INPUT stream
|
/// Add a new queue to a running INPUT stream
|
||||||
AddInQueue(SharedInQueue),
|
AddInQueue(SharedInQueue),
|
||||||
@ -11,9 +10,6 @@ pub enum StreamCommand {
|
|||||||
/// New signal generator config to be used in OUTPUT stream
|
/// New signal generator config to be used in OUTPUT stream
|
||||||
NewSiggen(Siggen),
|
NewSiggen(Siggen),
|
||||||
|
|
||||||
/// Apply command to the signal generator.
|
|
||||||
SiggenCommand(SiggenCommand),
|
|
||||||
|
|
||||||
/// Stop the thread, do not listen for data anymore.
|
/// Stop the thread, do not listen for data anymore.
|
||||||
StopThread,
|
StopThread,
|
||||||
|
|
||||||
|
@ -307,7 +307,7 @@ mod test {
|
|||||||
const Nframes: usize = 20;
|
const Nframes: usize = 20;
|
||||||
const Nch: usize = 2;
|
const Nch: usize = 2;
|
||||||
let mut signal = [0.; Nch * Nframes];
|
let mut signal = [0.; Nch * Nframes];
|
||||||
let mut siggen = Siggen::newSine(fs, Nch, 1.).unwrap();
|
let mut siggen = Siggen::newSine(Nch, 1.);
|
||||||
|
|
||||||
siggen.reset(fs);
|
siggen.reset(fs);
|
||||||
siggen.setMute(&[false, true]);
|
siggen.setMute(&[false, true]);
|
||||||
|
@ -2,8 +2,13 @@ use strum_macros::Display;
|
|||||||
use crate::config::*;
|
use crate::config::*;
|
||||||
|
|
||||||
/// Errors that happen in a stream
|
/// Errors that happen in a stream
|
||||||
#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
|
|
||||||
#[derive(strum_macros::EnumMessage, PartialEq, Debug, Clone, Display, Copy)]
|
#[derive(strum_macros::EnumMessage, PartialEq, Debug, Clone, Display, Copy)]
|
||||||
|
|
||||||
|
// Do the following when Pyo3 0.22 can finally be used combined with rust-numpy:
|
||||||
|
//#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
|
||||||
|
// For now:
|
||||||
|
#[cfg_attr(feature = "python-bindings", pyclass)]
|
||||||
|
|
||||||
pub enum StreamError {
|
pub enum StreamError {
|
||||||
/// Input overrun
|
/// Input overrun
|
||||||
#[strum(
|
#[strum(
|
||||||
|
@ -66,7 +66,7 @@ impl StreamMetaData {
|
|||||||
#[cfg_attr(feature = "python-bindings", pymethods)]
|
#[cfg_attr(feature = "python-bindings", pymethods)]
|
||||||
impl StreamMetaData {
|
impl StreamMetaData {
|
||||||
#[getter]
|
#[getter]
|
||||||
fn channelInfo(&self) -> Vec<DaqChannel> {
|
pub fn channelInfo(&self) -> Vec<DaqChannel> {
|
||||||
self.channelInfo.clone()
|
self.channelInfo.clone()
|
||||||
}
|
}
|
||||||
#[getter]
|
#[getter]
|
||||||
|
@ -2,9 +2,9 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
config::*,
|
config::*,
|
||||||
siggen::{self, Siggen, SiggenCommand},
|
siggen::{self, Siggen},
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, bail, Error, Result};
|
use anyhow::{bail, Error, Result};
|
||||||
use api::StreamApiDescr;
|
use api::StreamApiDescr;
|
||||||
use array_init::from_iter;
|
use array_init::from_iter;
|
||||||
use core::time;
|
use core::time;
|
||||||
@ -13,11 +13,8 @@ use crossbeam::{
|
|||||||
channel::{unbounded, Receiver, Sender, TrySendError},
|
channel::{unbounded, Receiver, Sender, TrySendError},
|
||||||
thread,
|
thread,
|
||||||
};
|
};
|
||||||
|
use std::sync::{atomic::AtomicBool, Arc, Mutex};
|
||||||
use std::thread::{JoinHandle, Thread};
|
use std::thread::{JoinHandle, Thread};
|
||||||
use std::{
|
|
||||||
sync::{atomic::AtomicBool, Arc, Mutex},
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
use streamcmd::StreamCommand;
|
use streamcmd::StreamCommand;
|
||||||
use streamdata::*;
|
use streamdata::*;
|
||||||
use streammetadata::*;
|
use streammetadata::*;
|
||||||
@ -36,8 +33,7 @@ struct StreamInfo<T> {
|
|||||||
streamtype: StreamType,
|
streamtype: StreamType,
|
||||||
stream: Box<dyn Stream>,
|
stream: Box<dyn Stream>,
|
||||||
threadhandle: JoinHandle<T>,
|
threadhandle: JoinHandle<T>,
|
||||||
commtx: Sender<StreamCommand>,
|
comm: Sender<StreamCommand>,
|
||||||
commrx: Receiver<Result<()>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Keep track of whether the stream has been created. To ensure singleton behaviour.
|
/// Keep track of whether the stream has been created. To ensure singleton behaviour.
|
||||||
@ -112,9 +108,8 @@ impl StreamMgr {
|
|||||||
self.getStatus(st)
|
self.getStatus(st)
|
||||||
}
|
}
|
||||||
#[pyo3(name = "setSiggen")]
|
#[pyo3(name = "setSiggen")]
|
||||||
fn setSiggen_py(&mut self, siggen: Siggen) -> PyResult<()> {
|
fn setSiggen_py(&mut self, siggen: Siggen) {
|
||||||
self.setSiggen(siggen)?;
|
self.setSiggen(siggen)
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
#[pyo3(name = "getStreamMetaData")]
|
#[pyo3(name = "getStreamMetaData")]
|
||||||
fn getStreamMetaData_py(&self, st: StreamType) -> Option<StreamMetaData> {
|
fn getStreamMetaData_py(&self, st: StreamType) -> Option<StreamMetaData> {
|
||||||
@ -122,13 +117,7 @@ impl StreamMgr {
|
|||||||
// value (not the Arc) has to be cloned.
|
// value (not the Arc) has to be cloned.
|
||||||
self.getStreamMetaData(st).map(|b| (*b).clone())
|
self.getStreamMetaData(st).map(|b| (*b).clone())
|
||||||
}
|
}
|
||||||
#[pyo3(name = "siggenCommand")]
|
|
||||||
fn siggenCommand_py(&mut self, cmd: SiggenCommand) -> PyResult<()> {
|
|
||||||
self.siggenCommand(cmd)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for StreamMgr {
|
impl Default for StreamMgr {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new()
|
Self::new()
|
||||||
@ -154,7 +143,7 @@ impl StreamMgr {
|
|||||||
devs: vec![],
|
devs: vec![],
|
||||||
input_stream: None,
|
input_stream: None,
|
||||||
output_stream: None,
|
output_stream: None,
|
||||||
siggen: Some(Siggen::newSilence(1., 1)),
|
siggen: None,
|
||||||
|
|
||||||
#[cfg(feature = "cpal-api")]
|
#[cfg(feature = "cpal-api")]
|
||||||
cpal_api: CpalApi::new(),
|
cpal_api: CpalApi::new(),
|
||||||
@ -206,27 +195,18 @@ impl StreamMgr {
|
|||||||
/// Set a new signal generator. Returns an error if it is unapplicable.
|
/// Set a new signal generator. Returns an error if it is unapplicable.
|
||||||
/// It is unapplicable if the number of channels of output does not match the
|
/// It is unapplicable if the number of channels of output does not match the
|
||||||
/// number of output channels in a running stream.
|
/// number of output channels in a running stream.
|
||||||
pub fn setSiggen(&mut self, siggen: Siggen) -> Result<()> {
|
pub fn setSiggen(&mut self, siggen: Siggen) {
|
||||||
// Current signal generator. Where to place it?
|
// Current signal generator. Where to place it?
|
||||||
if let Some(os) = &self.output_stream {
|
if let Some(istream) = &self.input_stream {
|
||||||
assert!(self.siggen.is_none());
|
|
||||||
os.commtx.send(StreamCommand::NewSiggen(siggen)).unwrap();
|
|
||||||
os.commrx.recv().unwrap()
|
|
||||||
} else if let Some(istream) = &self.input_stream {
|
|
||||||
if let StreamType::Duplex = istream.streamtype {
|
if let StreamType::Duplex = istream.streamtype {
|
||||||
assert!(self.siggen.is_none());
|
assert!(self.siggen.is_none());
|
||||||
istream
|
istream.comm.send(StreamCommand::NewSiggen(siggen)).unwrap();
|
||||||
.commtx
|
|
||||||
.send(StreamCommand::NewSiggen(siggen))
|
|
||||||
.unwrap();
|
|
||||||
istream.commrx.recv().unwrap()
|
|
||||||
} else {
|
|
||||||
self.siggen = Some(siggen);
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
} else if let Some(os) = &self.output_stream {
|
||||||
|
assert!(self.siggen.is_none());
|
||||||
|
os.comm.send(StreamCommand::NewSiggen(siggen)).unwrap();
|
||||||
} else {
|
} else {
|
||||||
self.siggen = Some(siggen);
|
self.siggen = Some(siggen);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -255,7 +235,7 @@ impl StreamMgr {
|
|||||||
/// of queues that get data from the stream.
|
/// of queues that get data from the stream.
|
||||||
pub fn addInQueue(&mut self, tx: Sender<InStreamMsg>) {
|
pub fn addInQueue(&mut self, tx: Sender<InStreamMsg>) {
|
||||||
if let Some(is) = &self.input_stream {
|
if let Some(is) = &self.input_stream {
|
||||||
is.commtx.send(StreamCommand::AddInQueue(tx)).unwrap()
|
is.comm.send(StreamCommand::AddInQueue(tx)).unwrap()
|
||||||
} else {
|
} else {
|
||||||
self.instreamqueues.as_mut().unwrap().push(tx);
|
self.instreamqueues.as_mut().unwrap().push(tx);
|
||||||
}
|
}
|
||||||
@ -265,14 +245,8 @@ impl StreamMgr {
|
|||||||
&mut self,
|
&mut self,
|
||||||
meta: Arc<StreamMetaData>,
|
meta: Arc<StreamMetaData>,
|
||||||
rx: Receiver<InStreamMsg>,
|
rx: Receiver<InStreamMsg>,
|
||||||
) -> (
|
) -> (JoinHandle<InQueues>, Sender<StreamCommand>) {
|
||||||
JoinHandle<InQueues>,
|
let (commtx, commrx) = unbounded();
|
||||||
Sender<StreamCommand>,
|
|
||||||
Receiver<Result<()>>,
|
|
||||||
) {
|
|
||||||
// Bi-directional communication between input stream thread and stream manager
|
|
||||||
let (commtx_ret, commrx) = unbounded();
|
|
||||||
let (commtx, commrx_ret) = unbounded();
|
|
||||||
|
|
||||||
// Unwrap here, as the queues should be free to grab
|
// Unwrap here, as the queues should be free to grab
|
||||||
let mut iqueues = self
|
let mut iqueues = self
|
||||||
@ -287,17 +261,8 @@ impl StreamMgr {
|
|||||||
// New queue added
|
// New queue added
|
||||||
StreamCommand::AddInQueue(queue) => {
|
StreamCommand::AddInQueue(queue) => {
|
||||||
match queue.send(InStreamMsg::StreamStarted(meta.clone())) {
|
match queue.send(InStreamMsg::StreamStarted(meta.clone())) {
|
||||||
Ok(()) => {
|
Ok(()) => iqueues.push(queue),
|
||||||
iqueues.push(queue);
|
Err(_) => {}
|
||||||
commtx.send(Ok(())).unwrap();
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
commtx
|
|
||||||
.send(Err(anyhow!(
|
|
||||||
"Cannot push to queue: {e}. Object destructed?"
|
|
||||||
)))
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -307,15 +272,11 @@ impl StreamMgr {
|
|||||||
&mut iqueues,
|
&mut iqueues,
|
||||||
InStreamMsg::StreamStopped,
|
InStreamMsg::StreamStopped,
|
||||||
);
|
);
|
||||||
commtx.send(Ok(())).unwrap();
|
|
||||||
break 'infy;
|
break 'infy;
|
||||||
}
|
}
|
||||||
StreamCommand::NewSiggen(_) => {
|
StreamCommand::NewSiggen(_) => {
|
||||||
panic!("Error: signal generator send to input-only stream.");
|
panic!("Error: signal generator send to input-only stream.");
|
||||||
}
|
}
|
||||||
StreamCommand::SiggenCommand(_) => {
|
|
||||||
panic!("Error: signal generator command send to input-only stream.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Ok(msg) = rx.recv_timeout(time::Duration::from_millis(10)) {
|
if let Ok(msg) = rx.recv_timeout(time::Duration::from_millis(10)) {
|
||||||
@ -324,7 +285,7 @@ impl StreamMgr {
|
|||||||
}
|
}
|
||||||
iqueues
|
iqueues
|
||||||
});
|
});
|
||||||
(threadhandle, commtx_ret, commrx_ret)
|
(threadhandle, commtx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match device info struct on given daq config.
|
// Match device info struct on given daq config.
|
||||||
@ -342,13 +303,8 @@ impl StreamMgr {
|
|||||||
&mut self,
|
&mut self,
|
||||||
meta: Arc<StreamMetaData>,
|
meta: Arc<StreamMetaData>,
|
||||||
tx: Sender<RawStreamData>,
|
tx: Sender<RawStreamData>,
|
||||||
) -> (
|
) -> (JoinHandle<Siggen>, Sender<StreamCommand>) {
|
||||||
JoinHandle<Siggen>,
|
let (commtx, commrx) = unbounded();
|
||||||
Sender<StreamCommand>,
|
|
||||||
Receiver<Result<()>>,
|
|
||||||
) {
|
|
||||||
let (commtx_res, commrx) = unbounded();
|
|
||||||
let (commtx, commrx_res) = unbounded();
|
|
||||||
|
|
||||||
// Number of channels to output for
|
// Number of channels to output for
|
||||||
let nchannels = meta.nchannels();
|
let nchannels = meta.nchannels();
|
||||||
@ -358,9 +314,8 @@ impl StreamMgr {
|
|||||||
let mut siggen = self
|
let mut siggen = self
|
||||||
.siggen
|
.siggen
|
||||||
.take()
|
.take()
|
||||||
.unwrap_or_else(|| Siggen::newSilence(meta.samplerate, nchannels));
|
.unwrap_or_else(|| Siggen::newSilence(nchannels));
|
||||||
|
|
||||||
siggen.setAllMute(true);
|
|
||||||
if siggen.nchannels() != nchannels {
|
if siggen.nchannels() != nchannels {
|
||||||
// Updating number of channels
|
// Updating number of channels
|
||||||
siggen.setNChannels(nchannels);
|
siggen.setNChannels(nchannels);
|
||||||
@ -368,15 +323,9 @@ impl StreamMgr {
|
|||||||
siggen.reset(meta.samplerate);
|
siggen.reset(meta.samplerate);
|
||||||
|
|
||||||
let threadhandle = std::thread::spawn(move || {
|
let threadhandle = std::thread::spawn(move || {
|
||||||
// What is a good sleep time? We have made sure that there are
|
let mut floatbuf: Vec<Flt> = Vec::with_capacity(nchannels * meta.framesPerBlock);
|
||||||
// two buffers available for the output stream. We choose to wake up twice per frame.
|
|
||||||
let sleep_time_us = Duration::from_micros(
|
|
||||||
(0.5 * 1e6 * meta.framesPerBlock as Flt / meta.samplerate) as u64,
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut floatbuf: Vec<Flt> = vec![0.; nchannels * meta.framesPerBlock];
|
|
||||||
'infy: loop {
|
'infy: loop {
|
||||||
if let Ok(comm_msg) = commrx.recv_timeout(sleep_time_us) {
|
if let Ok(comm_msg) = commrx.try_recv() {
|
||||||
match comm_msg {
|
match comm_msg {
|
||||||
// New queue added
|
// New queue added
|
||||||
StreamCommand::AddInQueue(_) => {
|
StreamCommand::AddInQueue(_) => {
|
||||||
@ -385,7 +334,6 @@ impl StreamMgr {
|
|||||||
|
|
||||||
// Stop this thread. Returns the queue
|
// Stop this thread. Returns the queue
|
||||||
StreamCommand::StopThread => {
|
StreamCommand::StopThread => {
|
||||||
commtx.send(Ok(())).unwrap();
|
|
||||||
break 'infy;
|
break 'infy;
|
||||||
}
|
}
|
||||||
StreamCommand::NewSiggen(new_siggen) => {
|
StreamCommand::NewSiggen(new_siggen) => {
|
||||||
@ -396,20 +344,16 @@ impl StreamMgr {
|
|||||||
// println!("Updating channels");
|
// println!("Updating channels");
|
||||||
siggen.setNChannels(nchannels);
|
siggen.setNChannels(nchannels);
|
||||||
}
|
}
|
||||||
commtx.send(Ok(())).unwrap();
|
|
||||||
}
|
|
||||||
StreamCommand::SiggenCommand(cmd) => {
|
|
||||||
// Apply command to signal generator.
|
|
||||||
let res = siggen.applyCommand(cmd);
|
|
||||||
commtx.send(res).unwrap();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if tx.len() < 1 {
|
while tx.len() < 2 {
|
||||||
// Obtain signal from signal generator
|
unsafe {
|
||||||
|
floatbuf.set_len(nchannels * meta.framesPerBlock);
|
||||||
|
}
|
||||||
|
// Obtain signal
|
||||||
siggen.genSignal(&mut floatbuf);
|
siggen.genSignal(&mut floatbuf);
|
||||||
|
// println!("level: {}", floatbuf.iter().sum::<Flt>());
|
||||||
// Convert signal generator data to raw data and push to the stream thread
|
|
||||||
let msg = match meta.rawDatatype {
|
let msg = match meta.rawDatatype {
|
||||||
DataType::I8 => {
|
DataType::I8 => {
|
||||||
let v = Vec::<i8>::from_iter(floatbuf.iter().map(|f| f.to_sample()));
|
let v = Vec::<i8>::from_iter(floatbuf.iter().map(|f| f.to_sample()));
|
||||||
@ -433,17 +377,14 @@ impl StreamMgr {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
if let Err(_e) = tx.send(msg) {
|
if let Err(_e) = tx.send(msg) {
|
||||||
// An error occured while trying to send the raw data to
|
// println!("Error sending raw stream data to output stream!");
|
||||||
// the stream. This might be because the stream has
|
break 'infy;
|
||||||
// stopped or has an error.
|
|
||||||
|
|
||||||
// There is nothing we can do here, but we should not stop the thread.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
siggen
|
siggen
|
||||||
});
|
});
|
||||||
(threadhandle, commtx_res, commrx_res)
|
(threadhandle, commtx)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start a stream of certain type, using given configuration
|
/// Start a stream of certain type, using given configuration
|
||||||
@ -477,14 +418,13 @@ impl StreamMgr {
|
|||||||
_ => bail!("API {} not implemented!", cfg.api),
|
_ => bail!("API {} not implemented!", cfg.api),
|
||||||
};
|
};
|
||||||
let meta = stream.metadata();
|
let meta = stream.metadata();
|
||||||
let (threadhandle, commtx, commrx) = self.startOuputStreamThread(meta, tx);
|
let (threadhandle, commtx) = self.startOuputStreamThread(meta, tx);
|
||||||
|
|
||||||
self.output_stream = Some(StreamInfo {
|
self.output_stream = Some(StreamInfo {
|
||||||
streamtype: StreamType::Input,
|
streamtype: StreamType::Input,
|
||||||
stream,
|
stream,
|
||||||
threadhandle,
|
threadhandle,
|
||||||
commtx,
|
comm: commtx,
|
||||||
commrx,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -532,14 +472,13 @@ impl StreamMgr {
|
|||||||
|
|
||||||
sendMsgToAllQueuesRemoveUnused(iqueues, InStreamMsg::StreamStarted(meta.clone()));
|
sendMsgToAllQueuesRemoveUnused(iqueues, InStreamMsg::StreamStarted(meta.clone()));
|
||||||
|
|
||||||
let (threadhandle, commtx, commrx) = self.startInputStreamThread(meta, rx);
|
let (threadhandle, commtx) = self.startInputStreamThread(meta, rx);
|
||||||
|
|
||||||
self.input_stream = Some(StreamInfo {
|
self.input_stream = Some(StreamInfo {
|
||||||
streamtype: stype,
|
streamtype: stype,
|
||||||
stream,
|
stream,
|
||||||
threadhandle,
|
threadhandle,
|
||||||
commtx,
|
comm: commtx,
|
||||||
commrx,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -564,14 +503,13 @@ impl StreamMgr {
|
|||||||
let meta = stream.metadata();
|
let meta = stream.metadata();
|
||||||
sendMsgToAllQueuesRemoveUnused(iqueues, InStreamMsg::StreamStarted(meta.clone()));
|
sendMsgToAllQueuesRemoveUnused(iqueues, InStreamMsg::StreamStarted(meta.clone()));
|
||||||
|
|
||||||
let (threadhandle, commtx, commrx) = self.startInputStreamThread(meta, rx);
|
let (threadhandle, commtx) = self.startInputStreamThread(meta, rx);
|
||||||
|
|
||||||
self.input_stream = Some(StreamInfo {
|
self.input_stream = Some(StreamInfo {
|
||||||
streamtype: StreamType::Input,
|
streamtype: StreamType::Input,
|
||||||
stream,
|
stream,
|
||||||
threadhandle,
|
threadhandle,
|
||||||
commtx,
|
comm: commtx,
|
||||||
commrx,
|
|
||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
||||||
@ -599,14 +537,15 @@ impl StreamMgr {
|
|||||||
let (tx, rx)= unbounded();
|
let (tx, rx)= unbounded();
|
||||||
let stream = self.cpal_api.startDefaultOutputStream(rx)?;
|
let stream = self.cpal_api.startDefaultOutputStream(rx)?;
|
||||||
let meta = stream.metadata();
|
let meta = stream.metadata();
|
||||||
let (threadhandle, commtx, commrx) = self.startOuputStreamThread(meta, tx);
|
let (threadhandle, commtx) = self.startOuputStreamThread(meta, tx);
|
||||||
|
// Inform all listeners of new stream data
|
||||||
|
|
||||||
|
|
||||||
self.output_stream = Some(StreamInfo {
|
self.output_stream = Some(StreamInfo {
|
||||||
streamtype: StreamType::Input,
|
streamtype: StreamType::Input,
|
||||||
stream,
|
stream,
|
||||||
threadhandle,
|
threadhandle,
|
||||||
commtx,
|
comm: commtx,
|
||||||
commrx,
|
|
||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
||||||
@ -624,22 +563,19 @@ impl StreamMgr {
|
|||||||
streamtype: _, // Ignored here
|
streamtype: _, // Ignored here
|
||||||
stream: _,
|
stream: _,
|
||||||
threadhandle,
|
threadhandle,
|
||||||
commtx,
|
comm,
|
||||||
commrx,
|
|
||||||
}) = self.input_stream.take()
|
}) = self.input_stream.take()
|
||||||
{
|
{
|
||||||
// println!("Stopping existing stream..");
|
// println!("Stopping existing stream..");
|
||||||
// Send thread to stop
|
// Send thread to stop
|
||||||
commtx.send(StreamCommand::StopThread).unwrap();
|
comm.send(StreamCommand::StopThread).unwrap();
|
||||||
|
|
||||||
// Store stream queues back into StreamMgr
|
// Store stream queues back into StreamMgr
|
||||||
self.instreamqueues = Some(threadhandle.join().expect("Stream thread panicked!"));
|
self.instreamqueues = Some(threadhandle.join().expect("Stream thread panicked!"));
|
||||||
|
|
||||||
let res = commrx.recv().unwrap();
|
|
||||||
return res;
|
|
||||||
} else {
|
} else {
|
||||||
bail!("Stream is not running.")
|
bail!("Stream is not running.")
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
/// Stop existing output stream
|
/// Stop existing output stream
|
||||||
pub fn stopOutputStream(&mut self) -> Result<()> {
|
pub fn stopOutputStream(&mut self) -> Result<()> {
|
||||||
@ -647,17 +583,21 @@ impl StreamMgr {
|
|||||||
streamtype: _, // Ignored here
|
streamtype: _, // Ignored here
|
||||||
stream: _,
|
stream: _,
|
||||||
threadhandle,
|
threadhandle,
|
||||||
commtx,
|
comm,
|
||||||
commrx,
|
|
||||||
}) = self.output_stream.take()
|
}) = self.output_stream.take()
|
||||||
{
|
{
|
||||||
commtx.send(StreamCommand::StopThread).unwrap();
|
if comm.send(StreamCommand::StopThread).is_err() {
|
||||||
// eprintln!("Wainting for threadhandle to join...");
|
// Failed to send command over channel. This means the thread is
|
||||||
|
// already finished due to some other reason.
|
||||||
|
assert!(threadhandle.is_finished());
|
||||||
|
}
|
||||||
|
// println!("Wainting for threadhandle to join...");
|
||||||
self.siggen = Some(threadhandle.join().expect("Output thread panicked!"));
|
self.siggen = Some(threadhandle.join().expect("Output thread panicked!"));
|
||||||
commrx.recv().unwrap()
|
// println!("Threadhandle joined!");
|
||||||
} else {
|
} else {
|
||||||
bail!("Stream is not running.");
|
bail!("Stream is not running.");
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
/// Stop existing running stream.
|
/// Stop existing running stream.
|
||||||
///
|
///
|
||||||
@ -670,40 +610,6 @@ impl StreamMgr {
|
|||||||
StreamType::Output => self.stopOutputStream(),
|
StreamType::Output => self.stopOutputStream(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply a signal generator command to control the output stream's signal
|
|
||||||
/// generator. see [SiggenCommand] for types of commands. Muting, setting
|
|
||||||
/// gain etc. A result code is given back and should be checked for errors.
|
|
||||||
pub fn siggenCommand(&mut self, cmd: SiggenCommand) -> Result<()> {
|
|
||||||
if let Some(stream) = self.output_stream.as_ref() {
|
|
||||||
stream
|
|
||||||
.commtx
|
|
||||||
.send(StreamCommand::SiggenCommand(cmd))
|
|
||||||
.unwrap();
|
|
||||||
return stream.commrx.recv().unwrap();
|
|
||||||
} else if let Some(stream) = self.input_stream.as_ref() {
|
|
||||||
// When its duplex, it should have a signal generator
|
|
||||||
if matches!(stream.streamtype, StreamType::Duplex) {
|
|
||||||
stream
|
|
||||||
.commtx
|
|
||||||
.send(StreamCommand::SiggenCommand(cmd))
|
|
||||||
.unwrap();
|
|
||||||
stream.commrx.recv().unwrap()
|
|
||||||
} else {
|
|
||||||
return self
|
|
||||||
.siggen
|
|
||||||
.as_mut()
|
|
||||||
.expect("siggen should be in rest pos")
|
|
||||||
.applyCommand(cmd);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return self
|
|
||||||
.siggen
|
|
||||||
.as_mut()
|
|
||||||
.expect("siggen should be in rest pos")
|
|
||||||
.applyCommand(cmd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // impl StreamMgr
|
} // impl StreamMgr
|
||||||
impl Drop for StreamMgr {
|
impl Drop for StreamMgr {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
|
@ -2,7 +2,7 @@ use super::*;
|
|||||||
use super::seriesbiquad::*;
|
use super::seriesbiquad::*;
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
#[cfg_attr(feature = "python-bindings", pyclass)]
|
#[cfg_attr(feature = "python-bindings", pyclass)]
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone)]
|
||||||
/// Multiple biquad filter that operate in parallel on a signal, and can apply a gain value to each
|
/// Multiple biquad filter that operate in parallel on a signal, and can apply a gain value to each
|
||||||
/// of the returned values. The BiquadBank can be used to decompose a signal by running it through
|
/// of the returned values. The BiquadBank can be used to decompose a signal by running it through
|
||||||
/// parallel filters, or it can directly be used to eq a signal. For the latter process, also a
|
/// parallel filters, or it can directly be used to eq a signal. For the latter process, also a
|
||||||
|
@ -4,8 +4,6 @@
|
|||||||
//! Contains [Biquad], [SeriesBiquad], and [BiquadBank]. These are all constructs that work on
|
//! Contains [Biquad], [SeriesBiquad], and [BiquadBank]. These are all constructs that work on
|
||||||
//! blocks of input data, and apply filters on it. TODO: implement FIR filter.
|
//! blocks of input data, and apply filters on it. TODO: implement FIR filter.
|
||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
use std::fmt::Debug;
|
|
||||||
|
|
||||||
use super::config::*;
|
use super::config::*;
|
||||||
|
|
||||||
mod biquad;
|
mod biquad;
|
||||||
@ -25,7 +23,7 @@ pub use seriesbiquad::SeriesBiquad;
|
|||||||
pub use zpkmodel::{PoleOrZero, ZPKModel, FilterSpec};
|
pub use zpkmodel::{PoleOrZero, ZPKModel, FilterSpec};
|
||||||
|
|
||||||
/// Implementations of this trait are able to DSP-filter input data.
|
/// Implementations of this trait are able to DSP-filter input data.
|
||||||
pub trait Filter: Send + Debug {
|
pub trait Filter: Send {
|
||||||
//! The filter trait is implemented by, for example, [Biquad], [SeriesBiquad], and [BiquadBank].
|
//! The filter trait is implemented by, for example, [Biquad], [SeriesBiquad], and [BiquadBank].
|
||||||
|
|
||||||
/// Filter input to generate output. A vector of output floats is generated with the same
|
/// Filter input to generate output. A vector of output floats is generated with the same
|
||||||
@ -41,6 +39,7 @@ pub trait Filter: Send + Debug {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Implementations are able to generate transfer functions of itself
|
/// Implementations are able to generate transfer functions of itself
|
||||||
|
|
||||||
pub trait TransferFunction<'a, T>: Send
|
pub trait TransferFunction<'a, T>: Send
|
||||||
where
|
where
|
||||||
T: AsArray<'a, Flt>,
|
T: AsArray<'a, Flt>,
|
||||||
|
@ -33,7 +33,6 @@
|
|||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
use config::*;
|
use config::*;
|
||||||
use filter::*;
|
|
||||||
|
|
||||||
pub use config::Flt;
|
pub use config::Flt;
|
||||||
pub mod daq;
|
pub mod daq;
|
||||||
@ -41,6 +40,7 @@ pub mod filter;
|
|||||||
pub mod ps;
|
pub mod ps;
|
||||||
mod math;
|
mod math;
|
||||||
pub mod siggen;
|
pub mod siggen;
|
||||||
|
use filter::*;
|
||||||
pub mod rt;
|
pub mod rt;
|
||||||
pub mod slm;
|
pub mod slm;
|
||||||
|
|
||||||
@ -61,10 +61,6 @@ fn lasprs(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|||||||
|
|
||||||
// Signal generator
|
// Signal generator
|
||||||
m.add_class::<siggen::Siggen>()?;
|
m.add_class::<siggen::Siggen>()?;
|
||||||
m.add_class::<siggen::SiggenCommand>()?;
|
|
||||||
m.add_class::<siggen::SweepType>()?;
|
|
||||||
m.add_class::<siggen::SiggenCommand>()?;
|
|
||||||
m.add_class::<siggen::Source>()?;
|
|
||||||
|
|
||||||
// SLM
|
// SLM
|
||||||
m.add_class::<slm::TimeWeighting>()?;
|
m.add_class::<slm::TimeWeighting>()?;
|
||||||
|
@ -2,10 +2,7 @@
|
|||||||
//!
|
//!
|
||||||
//!
|
//!
|
||||||
use crate::config::*;
|
use crate::config::*;
|
||||||
use ndarray::{ArrayView1, IntoDimension};
|
use ndarray::ArrayView1;
|
||||||
use rand::{rngs::SmallRng, thread_rng, Rng, SeedableRng};
|
|
||||||
use rand_distr::StandardNormal;
|
|
||||||
use smallvec::Array;
|
|
||||||
|
|
||||||
/// Compute maximum value of an array of float values
|
/// Compute maximum value of an array of float values
|
||||||
pub fn max<T>(arr: ArrayView<Flt, T>) -> Flt
|
pub fn max<T>(arr: ArrayView<Flt, T>) -> Flt
|
||||||
@ -36,22 +33,3 @@ where
|
|||||||
{
|
{
|
||||||
arr.fold(0., |acc, new| if new.abs() > acc { new.abs() } else { acc })
|
arr.fold(0., |acc, new| if new.abs() > acc { new.abs() } else { acc })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate an array of *PSEUDO* random numbers from the standard normal
|
|
||||||
/// distribution. Typically used for white noise generation. To be used for
|
|
||||||
/// noise signals, not for anything that needs to be truly random.
|
|
||||||
///
|
|
||||||
/// # Args
|
|
||||||
///
|
|
||||||
/// - `shape` - Shape of the returned array
|
|
||||||
///
|
|
||||||
pub fn randNormal<Sh, D>(shape: Sh) -> ndarray::Array<Flt,D>
|
|
||||||
where
|
|
||||||
Sh: ShapeBuilder<Dim=D>,
|
|
||||||
D: Dimension
|
|
||||||
{
|
|
||||||
// Explicit conversion to Fortran order
|
|
||||||
let mut rng = SmallRng::from_entropy();
|
|
||||||
let shape = shape.f().into_shape_with_order();
|
|
||||||
ArrayBase::from_shape_simple_fn(shape, || rng.sample(StandardNormal))
|
|
||||||
}
|
|
||||||
|
@ -308,9 +308,11 @@ impl AvPowerSpectra {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use approx::assert_abs_diff_eq;
|
use approx::assert_abs_diff_eq;
|
||||||
|
use ndarray_rand::rand_distr::Normal;
|
||||||
|
use ndarray_rand::RandomExt;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{config::*, math::randNormal};
|
use crate::config::*;
|
||||||
|
|
||||||
use super::{ApsMode, AvPowerSpectra, CPSResult, Overlap, WindowType};
|
use super::{ApsMode, AvPowerSpectra, CPSResult, Overlap, WindowType};
|
||||||
use Overlap::Percentage;
|
use Overlap::Percentage;
|
||||||
@ -357,7 +359,8 @@ mod test {
|
|||||||
let mut aps = AvPowerSpectra::new(settings);
|
let mut aps = AvPowerSpectra::new(settings);
|
||||||
assert_eq!(aps.overlap_keep, 0);
|
assert_eq!(aps.overlap_keep, 0);
|
||||||
|
|
||||||
let timedata_some = randNormal((nfft,1));
|
let distr = Normal::new(1.0, 1.0).expect("Distribution cannot be built");
|
||||||
|
let timedata_some = Dmat::random((nfft, 1), distr);
|
||||||
let timedata_zeros = Dmat::zeros((nfft, 1));
|
let timedata_zeros = Dmat::zeros((nfft, 1));
|
||||||
|
|
||||||
// Clone here, as first_result reference is overwritten by subsequent
|
// Clone here, as first_result reference is overwritten by subsequent
|
||||||
@ -379,7 +382,8 @@ mod test {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_tf1() {
|
fn test_tf1() {
|
||||||
let nfft = 4800;
|
let nfft = 4800;
|
||||||
let mut timedata = randNormal((nfft, 1));
|
let distr = Normal::new(1.0, 1.0).unwrap();
|
||||||
|
let mut timedata = Dmat::random((nfft, 1), distr);
|
||||||
timedata
|
timedata
|
||||||
.push_column(timedata.column(0).mapv(|a| 2. * a).view())
|
.push_column(timedata.column(0).mapv(|a| 2. * a).view())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@ -401,7 +405,8 @@ mod test {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_tf2() {
|
fn test_tf2() {
|
||||||
let nfft = 4800;
|
let nfft = 4800;
|
||||||
let mut timedata = randNormal((nfft, 1));
|
let distr = Normal::new(1.0, 1.0).unwrap();
|
||||||
|
let mut timedata = Dmat::random((nfft, 1), distr);
|
||||||
timedata
|
timedata
|
||||||
.push_column(timedata.column(0).mapv(|a| 2. * a).view())
|
.push_column(timedata.column(0).mapv(|a| 2. * a).view())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@ -426,7 +431,8 @@ mod test {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_ap() {
|
fn test_ap() {
|
||||||
let nfft = 1024;
|
let nfft = 1024;
|
||||||
let timedata = randNormal((150 * nfft, 1));
|
let distr = Normal::new(1.0, 1.0).unwrap();
|
||||||
|
let timedata = Dmat::random((150 * nfft, 1), distr);
|
||||||
let timedata_mean_square = (&timedata * &timedata).sum() / (timedata.len() as Flt);
|
let timedata_mean_square = (&timedata * &timedata).sum() / (timedata.len() as Flt);
|
||||||
|
|
||||||
for wt in [
|
for wt in [
|
||||||
|
@ -2,7 +2,11 @@ use crate::config::*;
|
|||||||
use strum::IntoEnumIterator;
|
use strum::IntoEnumIterator;
|
||||||
use strum_macros::{Display, EnumIter, EnumMessage};
|
use strum_macros::{Display, EnumIter, EnumMessage};
|
||||||
/// Sound level frequency weighting type (A, C, Z)
|
/// Sound level frequency weighting type (A, C, Z)
|
||||||
#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
|
|
||||||
|
// Do the following when Pyo3 0.22 can finally be used combined with rust-numpy:
|
||||||
|
// #[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
|
||||||
|
// For now:
|
||||||
|
#[cfg_attr(feature = "python-bindings", pyclass)]
|
||||||
#[derive(Copy, Display, Debug, EnumMessage, Default, Clone, PartialEq, EnumIter)]
|
#[derive(Copy, Display, Debug, EnumMessage, Default, Clone, PartialEq, EnumIter)]
|
||||||
pub enum FreqWeighting {
|
pub enum FreqWeighting {
|
||||||
/// A-weighting
|
/// A-weighting
|
||||||
|
@ -252,6 +252,7 @@ mod test {
|
|||||||
use approx::{abs_diff_eq, assert_relative_eq, assert_ulps_eq, ulps_eq};
|
use approx::{abs_diff_eq, assert_relative_eq, assert_ulps_eq, ulps_eq};
|
||||||
// For absolute value
|
// For absolute value
|
||||||
use num::complex::ComplexFloat;
|
use num::complex::ComplexFloat;
|
||||||
|
use rand_distr::StandardNormal;
|
||||||
|
|
||||||
/// Generate a sine wave at the order i
|
/// Generate a sine wave at the order i
|
||||||
fn generate_sinewave(nfft: usize, order: usize) -> Dcol {
|
fn generate_sinewave(nfft: usize, order: usize) -> Dcol {
|
||||||
@ -266,8 +267,6 @@ mod test {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::math::randNormal;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
#[test]
|
#[test]
|
||||||
/// Test whether DC part of single-sided FFT has right properties
|
/// Test whether DC part of single-sided FFT has right properties
|
||||||
@ -350,6 +349,7 @@ mod test {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use ndarray_rand::RandomExt;
|
||||||
// Test parseval's theorem for some random data
|
// Test parseval's theorem for some random data
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parseval() {
|
fn test_parseval() {
|
||||||
@ -358,7 +358,7 @@ mod test {
|
|||||||
let mut ps = PowerSpectra::newFromWindow(rect);
|
let mut ps = PowerSpectra::newFromWindow(rect);
|
||||||
|
|
||||||
// Start with a time signal
|
// Start with a time signal
|
||||||
let t: Dmat = randNormal((nfft,1));
|
let t: Dmat = Dmat::random((nfft, 1), StandardNormal);
|
||||||
|
|
||||||
let tavg = t.sum() / (nfft as Flt);
|
let tavg = t.sum() / (nfft as Flt);
|
||||||
let t_dc_power = tavg.powi(2);
|
let t_dc_power = tavg.powi(2);
|
||||||
@ -394,7 +394,7 @@ mod test {
|
|||||||
let mut ps = PowerSpectra::newFromWindow(window);
|
let mut ps = PowerSpectra::newFromWindow(window);
|
||||||
|
|
||||||
// Start with a time signal
|
// Start with a time signal
|
||||||
let t: Dmat = randNormal((nfft,1));
|
let t: Dmat = 2. * Dmat::random((nfft, 1), StandardNormal);
|
||||||
|
|
||||||
let tavg = t.sum() / (nfft as Flt);
|
let tavg = t.sum() / (nfft as Flt);
|
||||||
let t_dc_power = tavg.powi(2);
|
let t_dc_power = tavg.powi(2);
|
||||||
|
@ -8,7 +8,7 @@ use std::collections::VecDeque;
|
|||||||
/// TimeBuffer, storage to add blocks of data in a ring buffer, that can be
|
/// TimeBuffer, storage to add blocks of data in a ring buffer, that can be
|
||||||
/// extracted by blocks of other size. Also, we can keep samples in a buffer to
|
/// extracted by blocks of other size. Also, we can keep samples in a buffer to
|
||||||
/// create, for example, overlapping windows of time data.
|
/// create, for example, overlapping windows of time data.
|
||||||
#[derive(Default, Debug, Clone)]
|
#[derive(Default, Debug)]
|
||||||
pub struct TimeBuffer {
|
pub struct TimeBuffer {
|
||||||
data: Vec<VecDeque<Flt>>,
|
data: Vec<VecDeque<Flt>>,
|
||||||
}
|
}
|
||||||
|
@ -73,8 +73,11 @@ fn hamming(N: usize) -> Dcol {
|
|||||||
/// * Blackman
|
/// * Blackman
|
||||||
///
|
///
|
||||||
/// The [WindowType::default] is [WindowType::Hann].
|
/// The [WindowType::default] is [WindowType::Hann].
|
||||||
#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
|
|
||||||
#[derive(Display, Default, Copy, Clone, Debug, PartialEq, EnumMessage, EnumIter)]
|
#[derive(Display, Default, Copy, Clone, Debug, PartialEq, EnumMessage, EnumIter)]
|
||||||
|
// Do the following when Pyo3 0.22 can finally be used combined with rust-numpy:
|
||||||
|
// #[cfg_attr(feature = "python-bindings", pyclass(eq))]
|
||||||
|
// For now:
|
||||||
|
#[cfg_attr(feature = "python-bindings", pyclass)]
|
||||||
pub enum WindowType {
|
pub enum WindowType {
|
||||||
/// Von Hann window
|
/// Von Hann window
|
||||||
#[default]
|
#[default]
|
||||||
|
@ -192,8 +192,8 @@ impl Drop for PPM {
|
|||||||
|
|
||||||
/// Enumerator denoting, for each channel what the level approximately is. Low,
|
/// Enumerator denoting, for each channel what the level approximately is. Low,
|
||||||
/// fine, high or clipped.
|
/// fine, high or clipped.
|
||||||
#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
|
#[cfg_attr(feature = "python-bindings", pyclass)]
|
||||||
#[derive(Copy, Debug, PartialEq, Clone, Default)]
|
#[derive(Copy, Debug, Clone, Default)]
|
||||||
pub enum ClipState {
|
pub enum ClipState {
|
||||||
/// Level is rather low
|
/// Level is rather low
|
||||||
#[default]
|
#[default]
|
||||||
|
@ -136,11 +136,6 @@ impl RtAps {
|
|||||||
let mut lck = self.status.lock();
|
let mut lck = self.status.lock();
|
||||||
lck.take()
|
lck.take()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reset power spectra estimator, start with a clean sleeve
|
|
||||||
pub fn reset(&self) {
|
|
||||||
self.sender.send(RtApsMessage::ResetStatus).unwrap();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
impl Drop for RtAps {
|
impl Drop for RtAps {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
@ -163,11 +158,6 @@ impl RtAps {
|
|||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pyo3(name = "reset")]
|
|
||||||
fn reset_py(&self) {
|
|
||||||
self.reset()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
468
src/siggen.rs
Normal file
468
src/siggen.rs
Normal file
@ -0,0 +1,468 @@
|
|||||||
|
//! This module provide signal generators. The import struct defined here is
|
||||||
|
//! [Siggen], which has several creation methods.
|
||||||
|
//!
|
||||||
|
//! # Examples
|
||||||
|
//!
|
||||||
|
//! ## Create some white noise and print it.
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! use lasprs::siggen::Siggen;
|
||||||
|
//! let mut wn = Siggen::newWhiteNoise(1);
|
||||||
|
//! // Set gains for all channels
|
||||||
|
//! wn.setAllGains(0.1);
|
||||||
|
//! // Unmute all channels
|
||||||
|
//! wn.setAllMute(false);
|
||||||
|
//! // Create a slice where data is stored.
|
||||||
|
//! let mut sig = [0. ; 1024];
|
||||||
|
//! // Fill `sig` with the signal data.
|
||||||
|
//! wn.genSignal(&mut sig);
|
||||||
|
//! // Print data.
|
||||||
|
//! println!("{:?}", &sig);
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
use super::config::*;
|
||||||
|
use super::filter::Filter;
|
||||||
|
use dasp_sample::{FromSample, Sample};
|
||||||
|
use rayon::prelude::*;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use std::iter::ExactSizeIterator;
|
||||||
|
use std::slice::IterMut;
|
||||||
|
|
||||||
|
use rand::prelude::*;
|
||||||
|
use rand::rngs::ThreadRng;
|
||||||
|
use rand_distr::StandardNormal;
|
||||||
|
|
||||||
|
/// Ratio between circumference and radius of a circle
|
||||||
|
const twopi: Flt = 2.0 * pi;
|
||||||
|
|
||||||
|
/// Source for the signal generator. Implementations are sine waves, sweeps, noise.
|
||||||
|
pub trait Source: Send {
|
||||||
|
/// Generate the 'pure' source signal. Output is placed inside the `sig` argument.
|
||||||
|
fn genSignal_unscaled(&mut self, sig: &mut dyn ExactSizeIterator<Item = &mut Flt>);
|
||||||
|
/// Reset the source state, i.e. set phase to 0, etc
|
||||||
|
fn reset(&mut self, fs: Flt);
|
||||||
|
/// Used to make the Siggen struct cloneable
|
||||||
|
fn clone_dyn(&self) -> Box<dyn Source>;
|
||||||
|
}
|
||||||
|
impl Clone for Box<dyn Source> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
self.clone_dyn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Silence {}
|
||||||
|
|
||||||
|
impl Source for Silence {
|
||||||
|
fn genSignal_unscaled(&mut self, sig: &mut dyn ExactSizeIterator<Item = &mut Flt>) {
|
||||||
|
sig.for_each(|s| {
|
||||||
|
*s = 0.0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
fn reset(&mut self, _fs: Flt) {}
|
||||||
|
fn clone_dyn(&self) -> Box<dyn Source> {
|
||||||
|
Box::new(self.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// White noise source
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct WhiteNoise {}
|
||||||
|
impl WhiteNoise {
|
||||||
|
/// Generate new WhiteNoise generator
|
||||||
|
fn new() -> WhiteNoise {
|
||||||
|
WhiteNoise {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Source for WhiteNoise {
|
||||||
|
fn genSignal_unscaled(&mut self, sig: &mut dyn ExactSizeIterator<Item = &mut Flt>) {
|
||||||
|
sig.for_each(|s| {
|
||||||
|
*s = thread_rng().sample(StandardNormal);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
fn reset(&mut self, _fs: Flt) {}
|
||||||
|
fn clone_dyn(&self) -> Box<dyn Source> {
|
||||||
|
Box::new(self.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sine wave, with configurable frequency
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Sine {
|
||||||
|
// Sampling freq [Hz]
|
||||||
|
fs: Flt,
|
||||||
|
// current stored phase
|
||||||
|
phase: Flt,
|
||||||
|
// Signal frequency [rad/s]
|
||||||
|
omg: Flt,
|
||||||
|
}
|
||||||
|
impl Sine {
|
||||||
|
/// Create new sine source signal
|
||||||
|
///
|
||||||
|
/// Args:
|
||||||
|
///
|
||||||
|
/// * fs: Sampling freq [Hz]
|
||||||
|
/// *
|
||||||
|
fn new(freq: Flt) -> Sine {
|
||||||
|
Sine {
|
||||||
|
fs: -1.0,
|
||||||
|
phase: 0.0,
|
||||||
|
omg: 2.0 * pi * freq,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Source for Sine {
|
||||||
|
fn genSignal_unscaled(&mut self, sig: &mut dyn ExactSizeIterator<Item = &mut Flt>) {
|
||||||
|
if self.fs <= 0.0 {
|
||||||
|
sig.for_each(|s| {
|
||||||
|
*s = 0.0;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sig.for_each(|s| {
|
||||||
|
*s = Flt::sin(self.phase);
|
||||||
|
self.phase += self.omg / self.fs;
|
||||||
|
self.phase %= twopi;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
fn reset(&mut self, fs: Flt) {
|
||||||
|
self.fs = fs;
|
||||||
|
self.phase = 0.0;
|
||||||
|
}
|
||||||
|
fn clone_dyn(&self) -> Box<dyn Source> {
|
||||||
|
Box::new(self.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signal generator. Able to create acoustic output signals. See above example on how to use.
|
||||||
|
/// Typical signal that can be created are:
|
||||||
|
///
|
||||||
|
/// * (Siggen::newWhiteNoise)
|
||||||
|
/// * (Siggen::newSine)
|
||||||
|
///
|
||||||
|
#[derive(Clone)]
|
||||||
|
#[cfg_attr(feature = "python-bindings", pyclass)]
|
||||||
|
pub struct Siggen {
|
||||||
|
// The source dynamic signal. Noise, a sine wave, sweep, etc
|
||||||
|
source: Box<dyn Source>,
|
||||||
|
// Filter applied to the source signal
|
||||||
|
channels: Vec<SiggenChannelConfig>,
|
||||||
|
|
||||||
|
// Temporary source signal buffer
|
||||||
|
source_buf: Vec<Flt>,
|
||||||
|
|
||||||
|
// Output buffers (for filtered source signal)
|
||||||
|
chout_buf: Vec<Vec<Flt>>,
|
||||||
|
}
|
||||||
|
#[cfg(feature = "python-bindings")]
|
||||||
|
#[cfg_attr(feature = "python-bindings", pymethods)]
|
||||||
|
impl Siggen {
|
||||||
|
#[pyo3(name = "newWhiteNoise")]
|
||||||
|
#[staticmethod]
|
||||||
|
fn newWhiteNoise_py() -> Siggen {
|
||||||
|
Siggen::newWhiteNoise(0)
|
||||||
|
}
|
||||||
|
#[pyo3(name = "newSine")]
|
||||||
|
#[staticmethod]
|
||||||
|
fn newSine_py(freq: Flt) -> Siggen {
|
||||||
|
Siggen::newSine(0, freq)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Multiple channel signal generator. Can use a single source (coherent) to provide multiple signals
|
||||||
|
/// that can be sent out through different EQ's
|
||||||
|
impl Siggen {
|
||||||
|
/// Returns the number of channels this signal generator is generating for.
|
||||||
|
pub fn nchannels(&self) -> usize {
|
||||||
|
self.channels.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Silence: create a signal generator that does not output any dynamic
|
||||||
|
/// signal at all.
|
||||||
|
pub fn newSilence(nchannels: usize) -> Siggen {
|
||||||
|
Siggen {
|
||||||
|
channels: vec![SiggenChannelConfig::new(); nchannels],
|
||||||
|
source: Box::new(Silence {}),
|
||||||
|
source_buf: vec![],
|
||||||
|
chout_buf: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a white noise signal generator.
|
||||||
|
pub fn newWhiteNoise(nchannels: usize) -> Siggen {
|
||||||
|
Siggen::new(nchannels, Box::new(WhiteNoise::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set gains of all channels in signal generator to the same value
|
||||||
|
///
|
||||||
|
/// # Args
|
||||||
|
///
|
||||||
|
/// * g: New gain value
|
||||||
|
pub fn setAllGains(&mut self, g: Flt) {
|
||||||
|
self.channels.iter_mut().for_each(|set| set.setGain(g))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the number of channels to generate a signal for. Truncates the
|
||||||
|
/// output in case the value before calling this method is too little.
|
||||||
|
/// Appends new channel configs in case to little is available.
|
||||||
|
///
|
||||||
|
/// * nch: The new required number of channels
|
||||||
|
pub fn setNChannels(&mut self, nch: usize) {
|
||||||
|
self.channels.truncate(nch);
|
||||||
|
|
||||||
|
while self.channels.len() < nch {
|
||||||
|
self.channels.push(SiggenChannelConfig::new());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the DC offset for all channels
|
||||||
|
pub fn setDCOffset(&mut self, dc: &[Flt]) {
|
||||||
|
self.channels.iter_mut().zip(dc).for_each(|(ch, dc)| {
|
||||||
|
ch.DCOffset = *dc;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a sine wave signal generator
|
||||||
|
///
|
||||||
|
/// * freq: Frequency of the sine wave in \[Hz\]
|
||||||
|
pub fn newSine(nchannels: usize, freq: Flt) -> Siggen {
|
||||||
|
Siggen::new(nchannels, Box::new(Sine::new(freq)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new signal generator wiht an arbitrary source.
|
||||||
|
pub fn new(nchannels: usize, source: Box<dyn Source>) -> Siggen {
|
||||||
|
Siggen {
|
||||||
|
source,
|
||||||
|
channels: vec![SiggenChannelConfig::new(); nchannels],
|
||||||
|
source_buf: vec![],
|
||||||
|
chout_buf: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates *interleaved* output signal
|
||||||
|
pub fn genSignal<T>(&mut self, out: &mut [T])
|
||||||
|
where
|
||||||
|
T: Sample + FromSample<Flt> + Debug,
|
||||||
|
Flt: Sample,
|
||||||
|
{
|
||||||
|
let nch = self.nchannels();
|
||||||
|
let nsamples: usize = out.len() / nch;
|
||||||
|
assert!(out.len() % self.nchannels() == 0);
|
||||||
|
|
||||||
|
// Create source signal
|
||||||
|
self.source_buf.resize(nsamples, 0.0);
|
||||||
|
self.source
|
||||||
|
.genSignal_unscaled(&mut self.source_buf.iter_mut());
|
||||||
|
// println!("Source signal: {:?}", self.source_buf);
|
||||||
|
|
||||||
|
// Write output while casted to the correct type
|
||||||
|
// Iterate over each channel, and counter
|
||||||
|
self.chout_buf.resize(nch, vec![]);
|
||||||
|
|
||||||
|
for (channelno, (channel, chout)) in self
|
||||||
|
.channels
|
||||||
|
.iter_mut()
|
||||||
|
.zip(self.chout_buf.iter_mut())
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
chout.resize(nsamples, 0.0);
|
||||||
|
|
||||||
|
// Create output signal, overwrite chout
|
||||||
|
channel.genSignal(&self.source_buf, chout);
|
||||||
|
// println!("Channel: {}, {:?}", channelno, chout);
|
||||||
|
|
||||||
|
let out_iterator = out.iter_mut().skip(channelno).step_by(nch);
|
||||||
|
out_iterator.zip(chout).for_each(|(out, chin)| {
|
||||||
|
*out = chin.to_sample();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// println!("{:?}", out);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset signal generator. Applies any kind of cleanup necessary.
|
||||||
|
///
|
||||||
|
/// Args
|
||||||
|
///
|
||||||
|
/// * fs: (New) Sampling frequency \[Hz\]
|
||||||
|
///
|
||||||
|
pub fn reset(&mut self, fs: Flt) {
|
||||||
|
self.source.reset(fs);
|
||||||
|
self.channels.iter_mut().for_each(|x| x.reset(fs))
|
||||||
|
}
|
||||||
|
/// Mute / unmute all channels at once
|
||||||
|
pub fn setAllMute(&mut self, mute: bool) {
|
||||||
|
self.channels.iter_mut().for_each(|s| {
|
||||||
|
s.setMute(mute);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mute / unmute individual channels. Array of bools should have same size
|
||||||
|
/// as number of channels in signal generator.
|
||||||
|
pub fn setMute(&mut self, mute: &[bool]) {
|
||||||
|
assert!(mute.len() == self.nchannels());
|
||||||
|
self.channels.iter_mut().zip(mute).for_each(|(s, m)| {
|
||||||
|
s.setMute(*m);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signal generator config for a certain channel
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct SiggenChannelConfig {
|
||||||
|
muted: bool,
|
||||||
|
prefilter: Option<Box<dyn Filter>>,
|
||||||
|
gain: Flt,
|
||||||
|
DCOffset: Flt,
|
||||||
|
}
|
||||||
|
unsafe impl Send for SiggenChannelConfig {}
|
||||||
|
impl SiggenChannelConfig {
|
||||||
|
/// Set new pre-filter that filters the source signal
|
||||||
|
pub fn setPreFilter(&mut self, pref: Option<Box<dyn Filter>>) {
|
||||||
|
self.prefilter = pref;
|
||||||
|
}
|
||||||
|
/// Set the gain applied to the source signal
|
||||||
|
///
|
||||||
|
/// * g: Gain value. Can be any float. If set to 0.0, the source is effectively muted. Only
|
||||||
|
/// using (setMute) is a more efficient way to do this.
|
||||||
|
pub fn setGain(&mut self, g: Flt) {
|
||||||
|
self.gain = g;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset signal channel config. Only resets the prefilter state
|
||||||
|
pub fn reset(&mut self, _fs: Flt) {
|
||||||
|
if let Some(f) = &mut self.prefilter {
|
||||||
|
f.reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Generate new channel configuration using 'arbitrary' initial config: muted false, gain 1.0, DC offset 0.
|
||||||
|
/// and no prefilter
|
||||||
|
pub fn new() -> SiggenChannelConfig {
|
||||||
|
SiggenChannelConfig {
|
||||||
|
muted: false,
|
||||||
|
prefilter: None,
|
||||||
|
gain: 1.0,
|
||||||
|
DCOffset: 0.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set mute on channel. If true, only DC signal offset is outputed from (SiggenChannelConfig::transform).
|
||||||
|
pub fn setMute(&mut self, mute: bool) {
|
||||||
|
self.muted = mute;
|
||||||
|
}
|
||||||
|
/// Generate new signal data, given input source data.
|
||||||
|
///
|
||||||
|
/// # Args
|
||||||
|
///
|
||||||
|
/// source: Input source signal.
|
||||||
|
/// result: Reference of array of float values to be filled with signal data.
|
||||||
|
///
|
||||||
|
/// # Details
|
||||||
|
///
|
||||||
|
/// - When muted, the DC offset is still applied
|
||||||
|
/// - The order of the generation is:
|
||||||
|
/// - If a prefilter is installed, this pre-filter is applied to the source signal.
|
||||||
|
/// - Gain is applied.
|
||||||
|
/// - Offset is applied (thus, no gain is applied to the DC offset).
|
||||||
|
///
|
||||||
|
pub fn genSignal(&mut self, source: &[Flt], result: &mut [Flt]) {
|
||||||
|
if self.muted {
|
||||||
|
result.iter_mut().for_each(|x| {
|
||||||
|
*x = 0.0;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
result.copy_from_slice(source);
|
||||||
|
if let Some(f) = &mut self.prefilter {
|
||||||
|
f.filter(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.iter_mut().for_each(|x| {
|
||||||
|
// First apply gain, then offset
|
||||||
|
*x *= self.gain;
|
||||||
|
*x += self.DCOffset;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use approx::assert_abs_diff_eq;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::Flt;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_whitenoise() {
|
||||||
|
// This code is just to check syntax. We should really be listening to these outputs.
|
||||||
|
let mut t = [0.0; 10];
|
||||||
|
Siggen::newWhiteNoise(1).genSignal(&mut t);
|
||||||
|
// println!("{:?}", &t);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sine() {
|
||||||
|
// This code is just to check syntax. We should really be listening to
|
||||||
|
// these outputs.
|
||||||
|
const N: usize = 10000;
|
||||||
|
let mut s1 = [0.0; N];
|
||||||
|
let mut s2 = [0.0; N];
|
||||||
|
let mut siggen = Siggen::newSine(1, 1.0);
|
||||||
|
|
||||||
|
siggen.reset(10.0);
|
||||||
|
siggen.setAllMute(false);
|
||||||
|
siggen.genSignal(&mut s1);
|
||||||
|
siggen.reset(10.0);
|
||||||
|
siggen.genSignal(&mut s2);
|
||||||
|
|
||||||
|
let absdiff = s1
|
||||||
|
.iter()
|
||||||
|
.zip(s2.iter())
|
||||||
|
.map(|(s1, s2)| Flt::abs(*s1 - *s2))
|
||||||
|
.sum::<Flt>();
|
||||||
|
assert_abs_diff_eq!(absdiff, 0., epsilon = Flt::EPSILON * 100.);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sine2() {
|
||||||
|
// Test if channels are properly separated etc. Check if RMS is correct
|
||||||
|
// for amplitude = 1.0.
|
||||||
|
const fs: Flt = 10.0;
|
||||||
|
// Number of samples per channel
|
||||||
|
const Nframes: usize = 10000;
|
||||||
|
const Nch: usize = 2;
|
||||||
|
let mut signal = [0.0; Nch * Nframes];
|
||||||
|
let mut siggen = Siggen::newSine(Nch, 1.0);
|
||||||
|
|
||||||
|
siggen.reset(fs);
|
||||||
|
siggen.setMute(&[false, true]);
|
||||||
|
// siggen.channels[0].DCOffset = 0.1;
|
||||||
|
|
||||||
|
// Split off in two terms, see if this works properly
|
||||||
|
siggen.genSignal(&mut signal[..Nframes / 2]);
|
||||||
|
siggen.genSignal(&mut signal[Nframes / 2..]);
|
||||||
|
|
||||||
|
// Mean square of the signal
|
||||||
|
let ms1 = signal.iter().step_by(2).map(|s1| *s1 * *s1).sum::<Flt>() / (Nframes as Flt);
|
||||||
|
println!("ms1: {}", ms1);
|
||||||
|
|
||||||
|
let ms2 = signal
|
||||||
|
.iter()
|
||||||
|
.skip(1)
|
||||||
|
.step_by(2)
|
||||||
|
.map(|s1| *s1 * *s1)
|
||||||
|
.sum::<Flt>()
|
||||||
|
/ (Nframes as Flt);
|
||||||
|
|
||||||
|
assert_abs_diff_eq!(Flt::abs(ms1 - 0.5) , 0., epsilon= Flt::EPSILON * 1e3);
|
||||||
|
assert_eq!(ms2, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A small test to learn a bit about sample types and conversion. This
|
||||||
|
// is the thing we want.
|
||||||
|
#[test]
|
||||||
|
fn test_sample() {
|
||||||
|
assert_eq!((0.5f32).to_sample::<i8>(), 64);
|
||||||
|
assert_eq!((1.0f32).to_sample::<i8>(), 127);
|
||||||
|
assert_eq!(-(1.0f32).to_sample::<i8>(), -127);
|
||||||
|
assert_eq!((1.0f32).to_sample::<i16>(), i16::MAX);
|
||||||
|
}
|
||||||
|
}
|
@ -1,31 +0,0 @@
|
|||||||
//! This module provide signal generators. The import struct defined here is
|
|
||||||
//! [Siggen], which has several creation methods.
|
|
||||||
//!
|
|
||||||
//! # Examples
|
|
||||||
//!
|
|
||||||
//! ## Create some white noise and print it.
|
|
||||||
//!
|
|
||||||
//! ```
|
|
||||||
//! use lasprs::siggen::Siggen;
|
|
||||||
//! let mut wn = Siggen::newWhiteNoise(1);
|
|
||||||
//! // Set gains for all channels
|
|
||||||
//! wn.setAllGains(0.1);
|
|
||||||
//! // Unmute all channels
|
|
||||||
//! wn.setAllMute(false);
|
|
||||||
//! // Create a slice where data is stored.
|
|
||||||
//! let mut sig = [0. ; 1024];
|
|
||||||
//! // Fill `sig` with the signal data.
|
|
||||||
//! wn.genSignal(&mut sig);
|
|
||||||
//! // Print data.
|
|
||||||
//! println!("{:?}", &sig);
|
|
||||||
//!
|
|
||||||
//! ```
|
|
||||||
mod siggen;
|
|
||||||
mod siggenchannel;
|
|
||||||
mod source;
|
|
||||||
mod siggencmd;
|
|
||||||
mod sweep;
|
|
||||||
pub use source::Source;
|
|
||||||
pub use siggen::Siggen;
|
|
||||||
pub use sweep::SweepType;
|
|
||||||
pub use siggencmd::SiggenCommand;
|
|
@ -1,345 +0,0 @@
|
|||||||
use super::siggenchannel::SiggenChannelConfig;
|
|
||||||
use super::source::{self, *};
|
|
||||||
use super::sweep::SweepType;
|
|
||||||
use super::SiggenCommand;
|
|
||||||
use crate::config::*;
|
|
||||||
use crate::filter::Filter;
|
|
||||||
use anyhow::{bail, Result};
|
|
||||||
use dasp_sample::{FromSample, Sample};
|
|
||||||
use rayon::prelude::*;
|
|
||||||
use std::fmt::Debug;
|
|
||||||
use std::iter::ExactSizeIterator;
|
|
||||||
use std::slice::IterMut;
|
|
||||||
|
|
||||||
/// Multiple channel signal generator. Able to create (acoustic) output signals. See above example on how to use.
|
|
||||||
/// Typical signal that can be created are:
|
|
||||||
///
|
|
||||||
/// * [Siggen::newWhiteNoise]
|
|
||||||
/// * [Siggen::newSine]
|
|
||||||
/// * [Siggen::newSilence]
|
|
||||||
///
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
#[cfg_attr(feature = "python-bindings", pyclass)]
|
|
||||||
pub struct Siggen {
|
|
||||||
// The source dynamic signal. Noise, a sine wave, sweep, etc
|
|
||||||
source: Source,
|
|
||||||
|
|
||||||
// Channel configuration for each output channel
|
|
||||||
channels: Vec<SiggenChannelConfig>,
|
|
||||||
|
|
||||||
// Temporary source signal buffer
|
|
||||||
source_buf: Vec<Flt>,
|
|
||||||
|
|
||||||
// Output buffers (for filtered source signal)
|
|
||||||
chout_buf: Vec<Vec<Flt>>,
|
|
||||||
}
|
|
||||||
#[cfg(feature = "python-bindings")]
|
|
||||||
#[cfg_attr(feature = "python-bindings", pymethods)]
|
|
||||||
impl Siggen {
|
|
||||||
#[pyo3(name = "newWhiteNoise")]
|
|
||||||
#[staticmethod]
|
|
||||||
fn newWhiteNoise_py(fs: Flt) -> Siggen {
|
|
||||||
Siggen::newWhiteNoise(fs, 0)
|
|
||||||
}
|
|
||||||
#[pyo3(name = "newSine")]
|
|
||||||
#[staticmethod]
|
|
||||||
fn newSine_py(fs: Flt, freq: Flt, nchannels: usize) -> PyResult<Siggen> {
|
|
||||||
Ok(Siggen::newSine(fs, nchannels, freq)?)
|
|
||||||
}
|
|
||||||
#[pyo3(name = "newSweep")]
|
|
||||||
#[staticmethod]
|
|
||||||
fn newSweep_py(
|
|
||||||
fs: Flt,
|
|
||||||
nchannels: usize,
|
|
||||||
fl: Flt,
|
|
||||||
fu: Flt,
|
|
||||||
sweep_time: Flt,
|
|
||||||
quiet_time: Flt,
|
|
||||||
sweep_type: SweepType,
|
|
||||||
) -> Result<Self> {
|
|
||||||
Ok(Siggen::newSweep(
|
|
||||||
fs, nchannels, fl, fu, sweep_time, quiet_time, sweep_type,
|
|
||||||
)?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Siggen {
|
|
||||||
/// Create a new signal generator with an arbitrary source.
|
|
||||||
/// # Args
|
|
||||||
///
|
|
||||||
/// - `nchannels` - The number of channels to output
|
|
||||||
/// - `source` - Source function
|
|
||||||
pub fn new(nchannels: usize, source: Source) -> Siggen {
|
|
||||||
Siggen {
|
|
||||||
source,
|
|
||||||
channels: vec![SiggenChannelConfig::new(); nchannels],
|
|
||||||
source_buf: vec![],
|
|
||||||
chout_buf: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Create sine sweep signal generator
|
|
||||||
///
|
|
||||||
/// # Args
|
|
||||||
///
|
|
||||||
/// - `fs` - Sample rate \[Hz\]
|
|
||||||
/// - `nchannels`: The number of channels to output
|
|
||||||
/// - `fl` - Lower frequency \[Hz\]
|
|
||||||
/// - `fu` - Upper frequency \[Hz\]
|
|
||||||
/// - `sweep_time` - The duration of a single sweep \[s\]
|
|
||||||
/// - `quiet_time` - Time of silence after one sweep and start of the next \[s\]
|
|
||||||
/// - `sweep_type` - The type of the sweep, see [SweepType].
|
|
||||||
pub fn newSweep(
|
|
||||||
fs: Flt,
|
|
||||||
nchannels: usize,
|
|
||||||
fl: Flt,
|
|
||||||
fu: Flt,
|
|
||||||
sweep_time: Flt,
|
|
||||||
quiet_time: Flt,
|
|
||||||
sweep_type: SweepType,
|
|
||||||
) -> Result<Self> {
|
|
||||||
let source = Source::newSweep(fs, fl, fu, sweep_time, quiet_time, sweep_type)?;
|
|
||||||
Ok(Self::new(nchannels, source))
|
|
||||||
}
|
|
||||||
/// Create a sine wave signal generator
|
|
||||||
///
|
|
||||||
/// # Args
|
|
||||||
///
|
|
||||||
/// - `fs` - Sampling frequency \[Hz\]
|
|
||||||
/// - `nchannels`: The number of channels to output
|
|
||||||
/// * `freq` - Frequency of the sine wave in \[Hz\]
|
|
||||||
pub fn newSine(fs: Flt, nchannels: usize, freq: Flt) -> Result<Siggen> {
|
|
||||||
Ok(Siggen::new(nchannels, Source::newSine(fs, freq)?))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Silence: create a signal generator that does not output any dynamic
|
|
||||||
/// signal at all.
|
|
||||||
/// # Args
|
|
||||||
///
|
|
||||||
/// - `fs` - Sampling frequency \[Hz\]
|
|
||||||
/// - `nchannels` - The number of channels to output
|
|
||||||
pub fn newSilence(_fs: Flt, nchannels: usize) -> Siggen {
|
|
||||||
Siggen::new(nchannels, Source::newSilence())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a white noise signal generator.
|
|
||||||
///
|
|
||||||
/// # Args
|
|
||||||
///
|
|
||||||
/// - `fs` - Sampling frequency \[Hz\]
|
|
||||||
/// - `nchannels` - The number of channels to output
|
|
||||||
pub fn newWhiteNoise(_fs: Flt, nchannels: usize) -> Siggen {
|
|
||||||
Siggen::new(nchannels, Source::newWhiteNoise())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the number of channels this signal generator is generating for.
|
|
||||||
pub fn nchannels(&self) -> usize {
|
|
||||||
self.channels.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Apply command to current signal generator to change its state.
|
|
||||||
pub fn applyCommand(&mut self, msg: SiggenCommand) -> Result<()> {
|
|
||||||
match msg {
|
|
||||||
SiggenCommand::ChangeSource { src } => {
|
|
||||||
self.source = src;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
SiggenCommand::ResetSiggen { fs } => {
|
|
||||||
self.reset(fs);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
SiggenCommand::SetMuteAllChannels { mute } => {
|
|
||||||
self.setAllMute(mute);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
SiggenCommand::SetMuteChannel { ch, mute } => {
|
|
||||||
if ch > self.channels.len() {
|
|
||||||
bail!("Invalid channel index: {ch}");
|
|
||||||
}
|
|
||||||
self.channels[ch].setMute(mute);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
SiggenCommand::SetAllGains { g } => {
|
|
||||||
self.setAllGains(g);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Set gains of all channels in signal generator to the same value
|
|
||||||
///
|
|
||||||
/// # Args
|
|
||||||
///
|
|
||||||
/// * g: New gain value
|
|
||||||
pub fn setAllGains(&mut self, g: Flt) {
|
|
||||||
self.channels.iter_mut().for_each(|set| set.setGain(g))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the number of channels to generate a signal for. Truncates the
|
|
||||||
/// output in case the value before calling this method is too little.
|
|
||||||
/// Appends new channel configs in case to little is available.
|
|
||||||
///
|
|
||||||
/// * nch: The new required number of channels
|
|
||||||
pub fn setNChannels(&mut self, nch: usize) {
|
|
||||||
self.channels.truncate(nch);
|
|
||||||
|
|
||||||
while self.channels.len() < nch {
|
|
||||||
self.channels.push(SiggenChannelConfig::new());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the DC offset for all channels
|
|
||||||
pub fn setDCOffset(&mut self, dc: &[Flt]) {
|
|
||||||
self.channels.iter_mut().zip(dc).for_each(|(ch, dc)| {
|
|
||||||
ch.DCOffset = *dc;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates *interleaved* output signal
|
|
||||||
pub fn genSignal<T>(&mut self, out: &mut [T])
|
|
||||||
where
|
|
||||||
T: Sample + FromSample<Flt> + Debug,
|
|
||||||
Flt: Sample,
|
|
||||||
{
|
|
||||||
let nch = self.nchannels();
|
|
||||||
let nsamples: usize = out.len() / nch;
|
|
||||||
assert!(out.len() % self.nchannels() == 0);
|
|
||||||
|
|
||||||
// Create source signal
|
|
||||||
self.source_buf.resize(nsamples, 0.0);
|
|
||||||
self.source
|
|
||||||
.genSignal_unscaled(&mut self.source_buf.iter_mut());
|
|
||||||
// println!("Source signal: {:?}", self.source_buf);
|
|
||||||
|
|
||||||
// Write output while casted to the correct type
|
|
||||||
// Iterate over each channel, and counter
|
|
||||||
self.chout_buf.resize(nch, vec![]);
|
|
||||||
|
|
||||||
for (channelno, (channel, chout)) in self
|
|
||||||
.channels
|
|
||||||
.iter_mut()
|
|
||||||
.zip(self.chout_buf.iter_mut())
|
|
||||||
.enumerate()
|
|
||||||
{
|
|
||||||
chout.resize(nsamples, 0.0);
|
|
||||||
|
|
||||||
// Create output signal, overwrite chout
|
|
||||||
channel.genSignal(&self.source_buf, chout);
|
|
||||||
// println!("Channel: {}, {:?}", channelno, chout);
|
|
||||||
|
|
||||||
let out_iterator = out.iter_mut().skip(channelno).step_by(nch);
|
|
||||||
out_iterator.zip(chout).for_each(|(out, chin)| {
|
|
||||||
*out = chin.to_sample();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// println!("{:?}", out);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reset signal generator. Applies any kind of cleanup necessary.
|
|
||||||
///
|
|
||||||
/// Args
|
|
||||||
///
|
|
||||||
/// * fs: (New) Sampling frequency \[Hz\]
|
|
||||||
///
|
|
||||||
pub fn reset(&mut self, fs: Flt) {
|
|
||||||
self.source.reset(fs);
|
|
||||||
self.channels.iter_mut().for_each(|x| x.reset(fs))
|
|
||||||
}
|
|
||||||
/// Mute / unmute all channels at once
|
|
||||||
pub fn setAllMute(&mut self, mute: bool) {
|
|
||||||
self.channels.iter_mut().for_each(|s| {
|
|
||||||
s.setMute(mute);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mute / unmute individual channels. Array of bools should have same size
|
|
||||||
/// as number of channels in signal generator.
|
|
||||||
pub fn setMute(&mut self, mute: &[bool]) {
|
|
||||||
assert!(mute.len() == self.nchannels());
|
|
||||||
self.channels.iter_mut().zip(mute).for_each(|(s, m)| {
|
|
||||||
s.setMute(*m);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use approx::assert_abs_diff_eq;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::Flt;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_whitenoise() {
|
|
||||||
// This code is just to check syntax. We should really be listening to these outputs.
|
|
||||||
let mut t = [0.0; 10];
|
|
||||||
Siggen::newWhiteNoise(1., 1).genSignal(&mut t);
|
|
||||||
// println!("{:?}", &t);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sine() {
|
|
||||||
// This code is just to check syntax. We should really be listening to
|
|
||||||
// these outputs.
|
|
||||||
const N: usize = 10000;
|
|
||||||
let mut s1 = [0.0; N];
|
|
||||||
let mut s2 = [0.0; N];
|
|
||||||
let mut siggen = Siggen::newSine(1., 1, 1.0).unwrap();
|
|
||||||
|
|
||||||
siggen.reset(10.0);
|
|
||||||
siggen.setAllMute(false);
|
|
||||||
siggen.genSignal(&mut s1);
|
|
||||||
siggen.reset(10.0);
|
|
||||||
siggen.genSignal(&mut s2);
|
|
||||||
|
|
||||||
let absdiff = s1
|
|
||||||
.iter()
|
|
||||||
.zip(s2.iter())
|
|
||||||
.map(|(s1, s2)| Flt::abs(*s1 - *s2))
|
|
||||||
.sum::<Flt>();
|
|
||||||
assert_abs_diff_eq!(absdiff, 0., epsilon = Flt::EPSILON * 100.);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sine2() {
|
|
||||||
// Test if channels are properly separated etc. Check if RMS is correct
|
|
||||||
// for amplitude = 1.0.
|
|
||||||
const fs: Flt = 10.0;
|
|
||||||
// Number of samples per channel
|
|
||||||
const Nframes: usize = 10000;
|
|
||||||
const Nch: usize = 2;
|
|
||||||
let mut signal = [0.0; Nch * Nframes];
|
|
||||||
let mut siggen = Siggen::newSine(fs, Nch, 1.0).unwrap();
|
|
||||||
|
|
||||||
siggen.reset(fs);
|
|
||||||
siggen.setMute(&[false, true]);
|
|
||||||
// siggen.channels[0].DCOffset = 0.1;
|
|
||||||
|
|
||||||
// Split off in two terms, see if this works properly
|
|
||||||
siggen.genSignal(&mut signal[..Nframes / 2]);
|
|
||||||
siggen.genSignal(&mut signal[Nframes / 2..]);
|
|
||||||
|
|
||||||
// Mean square of the signal
|
|
||||||
let ms1 = signal.iter().step_by(2).map(|s1| *s1 * *s1).sum::<Flt>() / (Nframes as Flt);
|
|
||||||
println!("ms1: {}", ms1);
|
|
||||||
|
|
||||||
let ms2 = signal
|
|
||||||
.iter()
|
|
||||||
.skip(1)
|
|
||||||
.step_by(2)
|
|
||||||
.map(|s1| *s1 * *s1)
|
|
||||||
.sum::<Flt>()
|
|
||||||
/ (Nframes as Flt);
|
|
||||||
|
|
||||||
assert_abs_diff_eq!(Flt::abs(ms1 - 0.5), 0., epsilon = Flt::EPSILON * 1e3);
|
|
||||||
assert_eq!(ms2, 0.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A small test to learn a bit about sample types and conversion. This
|
|
||||||
// is the thing we want.
|
|
||||||
#[test]
|
|
||||||
fn test_sample() {
|
|
||||||
assert_eq!((0.5f32).to_sample::<i8>(), 64);
|
|
||||||
assert_eq!((1.0f32).to_sample::<i8>(), 127);
|
|
||||||
assert_eq!(-(1.0f32).to_sample::<i8>(), -127);
|
|
||||||
assert_eq!((1.0f32).to_sample::<i16>(), i16::MAX);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,78 +0,0 @@
|
|||||||
use crate::config::*;
|
|
||||||
use crate::filter::Filter;
|
|
||||||
/// Signal generator config for a certain channel
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct SiggenChannelConfig {
|
|
||||||
muted: bool,
|
|
||||||
prefilter: Option<Box<dyn Filter>>,
|
|
||||||
gain: Flt,
|
|
||||||
pub DCOffset: Flt,
|
|
||||||
}
|
|
||||||
unsafe impl Send for SiggenChannelConfig {}
|
|
||||||
impl SiggenChannelConfig {
|
|
||||||
/// Set new pre-filter that filters the source signal
|
|
||||||
pub fn setPreFilter(&mut self, pref: Option<Box<dyn Filter>>) {
|
|
||||||
self.prefilter = pref;
|
|
||||||
}
|
|
||||||
/// Set the gain applied to the source signal
|
|
||||||
///
|
|
||||||
/// * g: Gain value. Can be any float. If set to 0.0, the source is effectively muted. Only
|
|
||||||
/// using (setMute) is a more efficient way to do this.
|
|
||||||
pub fn setGain(&mut self, g: Flt) {
|
|
||||||
self.gain = g;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reset signal channel config. Only resets the prefilter state
|
|
||||||
pub fn reset(&mut self, _fs: Flt) {
|
|
||||||
if let Some(f) = &mut self.prefilter {
|
|
||||||
f.reset()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Generate new channel configuration using 'arbitrary' initial config: muted false, gain 1.0, DC offset 0.
|
|
||||||
/// and no prefilter
|
|
||||||
pub fn new() -> SiggenChannelConfig {
|
|
||||||
SiggenChannelConfig {
|
|
||||||
muted: false,
|
|
||||||
prefilter: None,
|
|
||||||
gain: 1.0,
|
|
||||||
DCOffset: 0.0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set mute on channel. If true, only DC signal offset is outputed from (SiggenChannelConfig::transform).
|
|
||||||
pub fn setMute(&mut self, mute: bool) {
|
|
||||||
self.muted = mute;
|
|
||||||
}
|
|
||||||
/// Generate new signal data, given input source data.
|
|
||||||
///
|
|
||||||
/// # Args
|
|
||||||
///
|
|
||||||
/// source: Input source signal.
|
|
||||||
/// result: Reference of array of float values to be filled with signal data.
|
|
||||||
///
|
|
||||||
/// # Details
|
|
||||||
///
|
|
||||||
/// - When muted, the DC offset is still applied
|
|
||||||
/// - The order of the generation is:
|
|
||||||
/// - If a prefilter is installed, this pre-filter is applied to the source signal.
|
|
||||||
/// - Gain is applied.
|
|
||||||
/// - Offset is applied (thus, no gain is applied to the DC offset).
|
|
||||||
///
|
|
||||||
pub fn genSignal(&mut self, source: &[Flt], result: &mut [Flt]) {
|
|
||||||
if self.muted {
|
|
||||||
result.iter_mut().for_each(|x| {
|
|
||||||
*x = 0.0;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
result.copy_from_slice(source);
|
|
||||||
if let Some(f) = &mut self.prefilter {
|
|
||||||
f.filter(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result.iter_mut().for_each(|x| {
|
|
||||||
// First apply gain, then offset
|
|
||||||
*x *= self.gain;
|
|
||||||
*x += self.DCOffset;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
use super::source::*;
|
|
||||||
use crate::config::*;
|
|
||||||
|
|
||||||
/// Messages that can be send to a given signal generator [Siggen], to change its behaviour
|
|
||||||
|
|
||||||
#[cfg_attr(feature = "python-bindings", pyclass)]
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub enum SiggenCommand {
|
|
||||||
/// Change the source to a sine wave with given frequency.
|
|
||||||
ChangeSource{
|
|
||||||
/// New signal source to apply for signal generator
|
|
||||||
src: Source,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Reset the signal generator state
|
|
||||||
ResetSiggen {
|
|
||||||
/// Sampling frequency \[Hz\]
|
|
||||||
fs: Flt,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Set all gains to value g
|
|
||||||
SetAllGains {
|
|
||||||
/// Linear gain level to apply to all channels
|
|
||||||
g: Flt,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Change the mute state for a certain channel
|
|
||||||
SetMuteChannel {
|
|
||||||
/// channel index
|
|
||||||
ch: usize,
|
|
||||||
/// mute state
|
|
||||||
mute: bool,
|
|
||||||
},
|
|
||||||
/// Change the mute state for all channels
|
|
||||||
SetMuteAllChannels {
|
|
||||||
/// mute state
|
|
||||||
mute: bool,
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,265 +0,0 @@
|
|||||||
//! All sources for a signal generator. Sine waves, sweeps, noise, etc.
|
|
||||||
use super::sweep::{SweepParams, SweepType};
|
|
||||||
use crate::config::*;
|
|
||||||
use std::fmt::Debug;
|
|
||||||
use std::ops::{Deref, DerefMut};
|
|
||||||
|
|
||||||
/// Ratio between circumference and radius of a circle
|
|
||||||
const twopi: Flt = 2.0 * pi;
|
|
||||||
use crate::config::*;
|
|
||||||
use anyhow::{bail, Result};
|
|
||||||
use rand::prelude::*;
|
|
||||||
use rand::rngs::ThreadRng;
|
|
||||||
use rand_distr::StandardNormal;
|
|
||||||
|
|
||||||
/// Signal source for a signal generator. A signal source is capable of creating
|
|
||||||
/// new signal data.
|
|
||||||
#[cfg_attr(feature = "python-bindings", pyclass)]
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Source {
|
|
||||||
src: Box<dyn SourceImpl>,
|
|
||||||
}
|
|
||||||
impl Source {
|
|
||||||
/// Create a sine wave signal source
|
|
||||||
///
|
|
||||||
/// # Args
|
|
||||||
///
|
|
||||||
/// - `fs` - Sampling frequency \[Hz\]
|
|
||||||
/// * `freq` - Frequency of the sine wave in \[Hz\]
|
|
||||||
pub fn newSine(fs: Flt, freq: Flt) -> Result<Source> {
|
|
||||||
Ok(Source {
|
|
||||||
src: Box::new(Sine::new(fs, freq)?),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/// Silence: create a signal source that does not output any dynamic
|
|
||||||
/// signal at all.
|
|
||||||
pub fn newSilence() -> Source {
|
|
||||||
Source {
|
|
||||||
src: Box::new(Silence {}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a white noise signal source
|
|
||||||
pub fn newWhiteNoise() -> Source {
|
|
||||||
Source {
|
|
||||||
src: Box::new(WhiteNoise { rng: SmallRng::from_entropy()}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sine sweep source
|
|
||||||
///
|
|
||||||
/// # Args
|
|
||||||
///
|
|
||||||
/// - `fs` - Sample rate \[Hz\]
|
|
||||||
/// - `fl` - Lower frequency \[Hz\]
|
|
||||||
/// - `fu` - Upper frequency \[Hz\]
|
|
||||||
/// - `sweep_time` - The duration of a single sweep \[s\]
|
|
||||||
/// - `quiet_time` - Time of silence after one sweep and start of the next \[s\]
|
|
||||||
/// - `sweep_type` - The type of the sweep, see [SweepType].
|
|
||||||
pub fn newSweep(
|
|
||||||
fs: Flt,
|
|
||||||
fl: Flt,
|
|
||||||
fu: Flt,
|
|
||||||
sweep_time: Flt,
|
|
||||||
quiet_time: Flt,
|
|
||||||
sweep_type: SweepType,
|
|
||||||
) -> Result<Source> {
|
|
||||||
Ok(Source {
|
|
||||||
src: Box::new(Sweep::new(fs, fl, fu, sweep_time, quiet_time, sweep_type)?),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "python-bindings")]
|
|
||||||
#[cfg_attr(feature = "python-bindings", pymethods)]
|
|
||||||
impl Source {
|
|
||||||
#[staticmethod]
|
|
||||||
#[pyo3(name = "newSine")]
|
|
||||||
fn newSine_py(fs: Flt, freq: Flt) -> PyResult<Source> {
|
|
||||||
Ok(Self::newSine(fs, freq)?)
|
|
||||||
}
|
|
||||||
#[pyo3(name = "newSilence")]
|
|
||||||
#[staticmethod]
|
|
||||||
fn newSilence_py() -> Source {
|
|
||||||
Self::newSilence()
|
|
||||||
}
|
|
||||||
#[staticmethod]
|
|
||||||
#[pyo3(name = "newWhiteNoise")]
|
|
||||||
fn newWhiteNoise_py() -> Source {
|
|
||||||
Self::newWhiteNoise()
|
|
||||||
}
|
|
||||||
#[staticmethod]
|
|
||||||
#[pyo3(name = "newSweep")]
|
|
||||||
fn newSweep_py(
|
|
||||||
fs: Flt,
|
|
||||||
fl: Flt,
|
|
||||||
fu: Flt,
|
|
||||||
sweep_time: Flt,
|
|
||||||
quiet_time: Flt,
|
|
||||||
sweep_type: SweepType,
|
|
||||||
) -> PyResult<Source> {
|
|
||||||
Ok(Self::newSweep(
|
|
||||||
fs, fl, fu, sweep_time, quiet_time, sweep_type,
|
|
||||||
)?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
/// Silence source. Most simple one does only send out a 0.
|
|
||||||
struct Silence {}
|
|
||||||
|
|
||||||
impl SourceImpl for Silence {
|
|
||||||
fn genSignal_unscaled(&mut self, sig: &mut dyn ExactSizeIterator<Item = &mut Flt>) {
|
|
||||||
sig.for_each(|s| {
|
|
||||||
*s = 0.0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
fn reset(&mut self, _fs: Flt) {}
|
|
||||||
fn clone_dyn(&self) -> Box<dyn SourceImpl> {
|
|
||||||
Box::new(self.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// White noise source. Can be colored by applying a color filter to the source
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
struct WhiteNoise {
|
|
||||||
// SmallRng is a cheap random number generator
|
|
||||||
rng: SmallRng
|
|
||||||
}
|
|
||||||
impl SourceImpl for WhiteNoise {
|
|
||||||
fn genSignal_unscaled(&mut self, sig: &mut dyn ExactSizeIterator<Item = &mut Flt>) {
|
|
||||||
sig.for_each(|s| {
|
|
||||||
*s = self.rng.sample(StandardNormal);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
fn reset(&mut self, _fs: Flt) {}
|
|
||||||
fn clone_dyn(&self) -> Box<dyn SourceImpl> {
|
|
||||||
Box::new(self.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sine wave, with configurable frequency
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
struct Sine {
|
|
||||||
// Sampling freq \[Hz\]
|
|
||||||
fs: Flt,
|
|
||||||
// current stored phase
|
|
||||||
phase: Flt,
|
|
||||||
// Signal frequency \[rad/s\]
|
|
||||||
omg: Flt,
|
|
||||||
}
|
|
||||||
impl Sine {
|
|
||||||
/// Create new sine source signal
|
|
||||||
///
|
|
||||||
/// Args:
|
|
||||||
///
|
|
||||||
/// * fs: Sampling freq [Hz]
|
|
||||||
/// *
|
|
||||||
pub fn new(fs: Flt, freq: Flt) -> Result<Sine> {
|
|
||||||
if fs <= 0. {
|
|
||||||
bail!("Invalid sampling frequency");
|
|
||||||
}
|
|
||||||
if freq >= fs / 2. {
|
|
||||||
bail!("Frequency of sine wave should be smaller than Nyquist frequency");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Sine {
|
|
||||||
fs,
|
|
||||||
phase: 0.0,
|
|
||||||
omg: 2.0 * pi * freq,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl SourceImpl for Sine {
|
|
||||||
fn genSignal_unscaled(&mut self, sig: &mut dyn ExactSizeIterator<Item = &mut Flt>) {
|
|
||||||
if self.fs <= 0.0 {
|
|
||||||
sig.for_each(|s| {
|
|
||||||
*s = 0.0;
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sig.for_each(|s| {
|
|
||||||
*s = Flt::sin(self.phase);
|
|
||||||
self.phase += self.omg / self.fs;
|
|
||||||
self.phase %= twopi;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
fn reset(&mut self, fs: Flt) {
|
|
||||||
self.fs = fs;
|
|
||||||
self.phase = 0.0;
|
|
||||||
}
|
|
||||||
fn clone_dyn(&self) -> Box<dyn SourceImpl> {
|
|
||||||
Box::new(self.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg_attr(feature = "python-bindings", pyclass)]
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct Sweep {
|
|
||||||
params: SweepParams,
|
|
||||||
// Generated time-periodic buffer
|
|
||||||
gen: Dcol,
|
|
||||||
N: usize,
|
|
||||||
}
|
|
||||||
impl Sweep {
|
|
||||||
fn new(
|
|
||||||
fs: Flt,
|
|
||||||
fl_: Flt,
|
|
||||||
fu_: Flt,
|
|
||||||
sweep_time: Flt,
|
|
||||||
quiet_time: Flt,
|
|
||||||
sweeptype: SweepType,
|
|
||||||
) -> Result<Self> {
|
|
||||||
let params = SweepParams::new(fs, fl_, fu_, sweep_time, quiet_time, sweeptype)?;
|
|
||||||
let gen = params.getSignal();
|
|
||||||
|
|
||||||
Ok(Sweep { params, gen, N: 0 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Linear forward or backward sweep phase
|
|
||||||
impl SourceImpl for Sweep {
|
|
||||||
fn genSignal_unscaled(&mut self, sig: &mut dyn ExactSizeIterator<Item = &mut Flt>) {
|
|
||||||
let sweep_iter = self.gen.as_slice().unwrap().iter().cycle().skip(self.N);
|
|
||||||
for (sig, sweep_sample) in sig.zip(sweep_iter) {
|
|
||||||
*sig = *sweep_sample;
|
|
||||||
self.N += 1;
|
|
||||||
}
|
|
||||||
// Modulo number of samples in generator
|
|
||||||
self.N %= self.gen.len();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reset(&mut self, fs: Flt) {
|
|
||||||
self.gen = self.params.reset(fs);
|
|
||||||
self.N = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clone_dyn(&self) -> Box<dyn SourceImpl> {
|
|
||||||
Box::new(self.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for Source {
|
|
||||||
type Target = Box<dyn SourceImpl>;
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.src
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl DerefMut for Source {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
||||||
&mut self.src
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Source for the signal generator. Implementations are sine waves, sweeps, noise.
|
|
||||||
pub trait SourceImpl: Send + Debug {
|
|
||||||
/// Generate the 'pure' source signal. Output is placed inside the `sig` argument.
|
|
||||||
fn genSignal_unscaled(&mut self, sig: &mut dyn ExactSizeIterator<Item = &mut Flt>);
|
|
||||||
/// Reset the source state, i.e. set phase to 0, etc
|
|
||||||
fn reset(&mut self, fs: Flt);
|
|
||||||
/// Used to make the Siggen struct cloneable
|
|
||||||
fn clone_dyn(&self) -> Box<dyn SourceImpl>;
|
|
||||||
}
|
|
||||||
impl Clone for Box<dyn SourceImpl> {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
self.clone_dyn()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,295 +0,0 @@
|
|||||||
//! Sweep signal generation code
|
|
||||||
use strum::EnumMessage;
|
|
||||||
use strum_macros::{Display, EnumMessage};
|
|
||||||
use {
|
|
||||||
crate::config::*,
|
|
||||||
anyhow::{bail, Result},
|
|
||||||
};
|
|
||||||
const NITER_NEWTON: usize = 20;
|
|
||||||
const twopi: Flt = 2. * pi;
|
|
||||||
|
|
||||||
/// Enumerator representing the type of sweep source to create. Used as
|
|
||||||
/// parameter in [Siggen::newSweep].
|
|
||||||
#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
|
|
||||||
#[derive(Debug, PartialEq, Clone, Display, EnumMessage)]
|
|
||||||
pub enum SweepType {
|
|
||||||
/// Forward only logarithmic sweep, repeats itself
|
|
||||||
#[strum(message = "Forward logarithmic")]
|
|
||||||
ForwardLog,
|
|
||||||
/// Reverse only logarithmic sweep, repeats itself
|
|
||||||
#[strum(message = "Backward logarithmic")]
|
|
||||||
BackwardLog,
|
|
||||||
/// Continuous logarithmic sweep, repeats itself
|
|
||||||
#[strum(message = "Continuous logarithmic")]
|
|
||||||
ContinuousLog,
|
|
||||||
|
|
||||||
/// Forward only linear sweep, repeats itself
|
|
||||||
#[strum(message = "Forward linear")]
|
|
||||||
ForwardLin,
|
|
||||||
/// Reverse only linear sweep, repeats itself
|
|
||||||
#[strum(message = "Backward linear")]
|
|
||||||
BackwardLin,
|
|
||||||
/// Continuous linear sweep, repeats itself
|
|
||||||
#[strum(message = "Continuous linear")]
|
|
||||||
ContinuousLin,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "python-bindings")]
|
|
||||||
#[cfg_attr(feature = "python-bindings", pymethods)]
|
|
||||||
impl SweepType {
|
|
||||||
#[staticmethod]
|
|
||||||
fn all() -> Vec<SweepType> {
|
|
||||||
use SweepType::*;
|
|
||||||
vec![
|
|
||||||
ForwardLin,
|
|
||||||
ForwardLog,
|
|
||||||
BackwardLin,
|
|
||||||
BackwardLog,
|
|
||||||
ContinuousLin,
|
|
||||||
ContinuousLog,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
fn __str__(&self) -> String {
|
|
||||||
self.get_message().unwrap().into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct SweepParams {
|
|
||||||
// These parameters are described at [Source::newSweep]
|
|
||||||
fs: Flt,
|
|
||||||
fl: Flt,
|
|
||||||
fu: Flt,
|
|
||||||
sweep_time: Flt,
|
|
||||||
quiet_time: Flt,
|
|
||||||
sweeptype: SweepType,
|
|
||||||
}
|
|
||||||
impl SweepParams {
|
|
||||||
pub fn new(
|
|
||||||
fs: Flt,
|
|
||||||
fl: Flt,
|
|
||||||
fu: Flt,
|
|
||||||
sweep_time: Flt,
|
|
||||||
quiet_time: Flt,
|
|
||||||
sweeptype: SweepType,
|
|
||||||
) -> Result<Self> {
|
|
||||||
if fs <= 0. {
|
|
||||||
bail!("Invalid sampling frequency: {} Hz", fs);
|
|
||||||
}
|
|
||||||
if fl > fu {
|
|
||||||
bail!("Lower frequency should be smaller than upper frequency");
|
|
||||||
}
|
|
||||||
if fu >= fs / 2. {
|
|
||||||
bail!("Upper frequency should be smaller than sampling frequency");
|
|
||||||
}
|
|
||||||
if sweep_time <= 0. {
|
|
||||||
bail!("Invalid sweep time, should be > 0.");
|
|
||||||
}
|
|
||||||
if 1. / sweep_time > fs / 2. {
|
|
||||||
bail!("Invalid sweep time: too short");
|
|
||||||
}
|
|
||||||
|
|
||||||
// For backward sweeps, we just reverse the start and stop frequency.
|
|
||||||
let (fl, fu) = if matches!(sweeptype, SweepType::BackwardLin | SweepType::BackwardLog) {
|
|
||||||
(fu, fl)
|
|
||||||
} else {
|
|
||||||
(fl, fu)
|
|
||||||
};
|
|
||||||
Ok(SweepParams {
|
|
||||||
fs,
|
|
||||||
fl,
|
|
||||||
fu,
|
|
||||||
sweep_time,
|
|
||||||
quiet_time,
|
|
||||||
sweeptype,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
pub fn reset(&mut self, fs: Flt) -> Dcol {
|
|
||||||
self.fs = fs;
|
|
||||||
self.getSignal()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn Ns(&self) -> usize {
|
|
||||||
(self.sweep_time * self.fs) as usize
|
|
||||||
}
|
|
||||||
/// Returns the phase as a function of time
|
|
||||||
fn getPhase(&self) -> Dcol {
|
|
||||||
match self.sweeptype {
|
|
||||||
SweepType::BackwardLin | SweepType::ForwardLin => self.getLinSweepFBPhase(),
|
|
||||||
SweepType::BackwardLog | SweepType::ForwardLog => self.getLogSweepFBPhase(),
|
|
||||||
SweepType::ContinuousLin => self.getLinSweepContPhase(),
|
|
||||||
SweepType::ContinuousLog => self.getLogSweepContPhase(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn getSignal(&self) -> Dcol {
|
|
||||||
let fs = self.fs;
|
|
||||||
// Number of samples in sweep
|
|
||||||
let Ns = (self.sweep_time * fs) as usize;
|
|
||||||
// Number of samples in quiet time
|
|
||||||
let Nq = (self.quiet_time * fs) as usize;
|
|
||||||
|
|
||||||
// Total number of samples
|
|
||||||
let N = Ns + Nq;
|
|
||||||
|
|
||||||
let phase = self.getPhase();
|
|
||||||
Dcol::from_iter((0..N).map(|i| if i < Ns { Flt::sin(phase[i]) } else { 0. }))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Linear forward or backward sweep phase
|
|
||||||
fn getLinSweepFBPhase(&self) -> Dcol {
|
|
||||||
assert!(matches!(
|
|
||||||
self.sweeptype,
|
|
||||||
SweepType::BackwardLin | SweepType::ForwardLin
|
|
||||||
));
|
|
||||||
let (Ns, fl, fu, fs) = (self.Ns(), self.fl, self.fu, self.fs);
|
|
||||||
|
|
||||||
// Time step
|
|
||||||
let Dt = 1. / fs;
|
|
||||||
let Nsf = Ns as Flt;
|
|
||||||
let K = (Dt * (fl * Nsf + 0.5 * (Nsf - 1.) * (fu - fl))).floor();
|
|
||||||
let eps_num = K / Dt - fl * Nsf - 0.5 * (Nsf - 1.) * (fu - fl);
|
|
||||||
let eps = eps_num / (0.5 * (Nsf - 1.));
|
|
||||||
let mut phase = 0.;
|
|
||||||
Dcol::from_iter((0..Ns).map(|n| {
|
|
||||||
let freq = fl + (n as Flt - 1.) / (Ns as Flt) * (fu + eps - fl);
|
|
||||||
let phase_out = phase;
|
|
||||||
phase += twopi * Dt * freq;
|
|
||||||
phase_out
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logarithmic forward or backward sweep phase
|
|
||||||
fn getLogSweepFBPhase(&self) -> Dcol {
|
|
||||||
assert!(matches!(
|
|
||||||
self.sweeptype,
|
|
||||||
SweepType::BackwardLog | SweepType::ForwardLog
|
|
||||||
));
|
|
||||||
|
|
||||||
let (Ns, fl, fu, fs) = (self.Ns(), self.fl, self.fu, self.fs);
|
|
||||||
// // Time step
|
|
||||||
let Dt = 1. / fs;
|
|
||||||
let Nsf = Ns as Flt;
|
|
||||||
let mut k = fu / fl;
|
|
||||||
let K = (Dt * fl * (k - 1.) / ((k.powf(1.0 / Nsf)) - 1.)).floor();
|
|
||||||
|
|
||||||
/* Iterate k to the right solution */
|
|
||||||
(0..10).for_each(|_| {
|
|
||||||
let E = 1. + K / (Dt * fl) * (k.powf(1.0 / Nsf) - 1.) - k;
|
|
||||||
let dEdk = K / (Dt * fl) * k.powf(1.0 / Nsf) / (Nsf * k) - 1.;
|
|
||||||
k -= E / dEdk;
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut phase = 0.;
|
|
||||||
Dcol::from_iter((0..Ns).map(|n| {
|
|
||||||
let nf = n as Flt;
|
|
||||||
let fnn = fl * k.powf(nf / Nsf);
|
|
||||||
let phase_old = phase;
|
|
||||||
phase += twopi * Dt * fnn;
|
|
||||||
phase_old
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Continuous log sweep phase
|
|
||||||
fn getLogSweepContPhase(&self) -> Dcol {
|
|
||||||
assert!(matches!(self.sweeptype, SweepType::ContinuousLog));
|
|
||||||
|
|
||||||
let (Ns, fl, fu, fs) = (self.Ns(), self.fl, self.fu, self.fs);
|
|
||||||
// // Time step
|
|
||||||
let Dt = 1. / fs;
|
|
||||||
let Nf = Ns / 2;
|
|
||||||
let Nff = Nf as Flt;
|
|
||||||
let Nb = Ns - Nf;
|
|
||||||
let Nbf = Nb as Flt;
|
|
||||||
let k1 = fu / fl;
|
|
||||||
let phif1 = twopi * Dt * fl * (k1 - 1.) / (k1.powf(1.0 / Nff) - 1.);
|
|
||||||
|
|
||||||
let K =
|
|
||||||
(phif1 / twopi + Dt * fu * (1. / k1 - 1.) / ((1. / k1).powf(1.0 / Nbf) - 1.)).floor();
|
|
||||||
let mut k = k1;
|
|
||||||
|
|
||||||
/* Newton iterations to converge k to the value such that the sweep is
|
|
||||||
* continuous */
|
|
||||||
(0..NITER_NEWTON).for_each(|_| {
|
|
||||||
let E = (k - 1.) / (k.powf(1.0 / Nff) - 1.) + (k - 1.) / (1. - k.powf(-1.0 / Nbf))
|
|
||||||
- K / Dt / fl;
|
|
||||||
|
|
||||||
// /* All parts of the derivative of above error E to k */
|
|
||||||
let dEdk1 = 1. / (k.powf(1.0 / Nff) - 1.);
|
|
||||||
let dEdk2 = (1. / k - 1.) / (k.powf(-1.0 / Nbf) - 1.);
|
|
||||||
let dEdk3 = -1. / (k * (k.powf(-1.0 / Nbf) - 1.));
|
|
||||||
let dEdk4 = k.powf(-1.0 / Nbf) * (1. / k - 1.)
|
|
||||||
/ (Nbf * Flt::powi(Flt::powf(k, -1.0 / Nbf) - 1., 2));
|
|
||||||
|
|
||||||
let dEdk5 = -Flt::powf(k, 1.0 / Nff) * (k - 1.)
|
|
||||||
/ (Nff * k * Flt::powi(Flt::powf(k, 1.0 / Nff) - 1., 2));
|
|
||||||
|
|
||||||
let dEdk = dEdk1 + dEdk2 + dEdk3 + dEdk4 + dEdk5;
|
|
||||||
k -= E / dEdk;
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut phase = 0.;
|
|
||||||
Dcol::from_iter((0..Ns).map(|n| {
|
|
||||||
let nf = n as Flt;
|
|
||||||
let fnn = if n <= Nf {
|
|
||||||
fl * k.powf(nf / Nff)
|
|
||||||
} else {
|
|
||||||
fl * k * (1. / k).powf((nf - Nff) / Nbf)
|
|
||||||
};
|
|
||||||
let phase_old = phase;
|
|
||||||
phase += twopi * Dt * fnn;
|
|
||||||
|
|
||||||
phase_old
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Continuous linear sweep phase
|
|
||||||
fn getLinSweepContPhase(&self) -> Dcol {
|
|
||||||
assert!(matches!(self.sweeptype, SweepType::ContinuousLin));
|
|
||||||
|
|
||||||
let (Ns, fl, fu, fs) = (self.Ns(), self.fl, self.fu, self.fs);
|
|
||||||
let Dt = 1. / fs;
|
|
||||||
let Nf = Ns / 2;
|
|
||||||
let Nb = Ns - Nf;
|
|
||||||
let Nff = Nf as Flt;
|
|
||||||
let Nbf = Nb as Flt;
|
|
||||||
/* Phi halfway */
|
|
||||||
let phih = twopi * Dt * (fl * Nff + 0.5 * (Nff - 1.) * (fu - fl));
|
|
||||||
let K = (phih / twopi + Dt * (fu * Nbf - (Nb as Flt - 1.) * (fu - fl))).floor();
|
|
||||||
|
|
||||||
let eps_num1 = (K - phih / twopi) / Dt;
|
|
||||||
let eps_num2 = -fu * Nbf + (Nbf - 1.) * (fu - fl);
|
|
||||||
|
|
||||||
let eps = (eps_num1 + eps_num2) / (0.5 * (Nbf + 1.));
|
|
||||||
let mut phase = 0.;
|
|
||||||
Dcol::from_iter((0..Ns).map(|n| {
|
|
||||||
let nf = n as Flt;
|
|
||||||
let freq = if n < Nf {
|
|
||||||
fl + nf / Nff * (fu - fl)
|
|
||||||
} else {
|
|
||||||
fu - (nf - Nff) / Nbf * (fu + eps - fl)
|
|
||||||
};
|
|
||||||
let phase_out = phase;
|
|
||||||
phase += twopi * Dt * freq;
|
|
||||||
phase_out
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use approx::assert_abs_diff_eq;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_phase_linsweep1() {
|
|
||||||
let fs = 10.;
|
|
||||||
let fl = 1.;
|
|
||||||
let fu = 1.;
|
|
||||||
let phase = SweepParams::new(fs, fl, fu, 10., 0., SweepType::ForwardLin)
|
|
||||||
.unwrap()
|
|
||||||
.getLinSweepFBPhase();
|
|
||||||
|
|
||||||
assert_abs_diff_eq!(phase[10], &(twopi));
|
|
||||||
}
|
|
||||||
}
|
|
@ -285,7 +285,7 @@ mod test {
|
|||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let mut siggen = Siggen::newSine(1., 1, 1000.).unwrap();
|
let mut siggen = Siggen::newSine(1, 1000.);
|
||||||
siggen.setAllMute(false);
|
siggen.setAllMute(false);
|
||||||
siggen.reset(fs);
|
siggen.reset(fs);
|
||||||
let mut data = vec![0.; N];
|
let mut data = vec![0.; N];
|
||||||
|
@ -3,7 +3,11 @@ use crate::config::*;
|
|||||||
use strum::EnumMessage;
|
use strum::EnumMessage;
|
||||||
use strum_macros::Display;
|
use strum_macros::Display;
|
||||||
/// Time weighting to use in level detection of Sound Level Meter.
|
/// Time weighting to use in level detection of Sound Level Meter.
|
||||||
#[cfg_attr(feature = "python-bindings", pyclass(eq))]
|
///
|
||||||
|
// Do the following when Pyo3 0.22 can finally be used combined with rust-numpy:
|
||||||
|
// #[cfg_attr(feature = "python-bindings", pyclass(eq))]
|
||||||
|
// For now:
|
||||||
|
#[cfg_attr(feature = "python-bindings", pyclass)]
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Display)]
|
#[derive(Clone, Copy, Debug, PartialEq, Display)]
|
||||||
pub enum TimeWeighting {
|
pub enum TimeWeighting {
|
||||||
// I know that the curly braces here are not required and add some
|
// I know that the curly braces here are not required and add some
|
||||||
@ -33,6 +37,10 @@ pub enum TimeWeighting {
|
|||||||
#[cfg(feature = "python-bindings")]
|
#[cfg(feature = "python-bindings")]
|
||||||
#[cfg_attr(feature = "python-bindings", pymethods)]
|
#[cfg_attr(feature = "python-bindings", pymethods)]
|
||||||
impl TimeWeighting {
|
impl TimeWeighting {
|
||||||
|
// This method is still required in Pyo3 0.21, not anymore in 0.22
|
||||||
|
fn __eq__(&self, other: &Self) -> bool {
|
||||||
|
self == other
|
||||||
|
}
|
||||||
fn __str__(&self) -> String {
|
fn __str__(&self) -> String {
|
||||||
format!("{self}")
|
format!("{self}")
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user