Removed dependency on ndarray_rand. Crate is not updated. Updated pyo3 to new version and updated enum pyclass derive macros. Switch to SmallRng for white noise random number generation.

This commit is contained in:
Anne de Jong 2024-10-26 21:53:56 +02:00
parent 0567e7fb92
commit 8a573266df
16 changed files with 69 additions and 76 deletions

View File

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

View File

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

View File

@ -7,10 +7,7 @@ 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)]
// 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(eq, eq_int))]
// For now:
#[cfg_attr(feature = "python-bindings", pyclass)]
#[allow(dead_code)] #[allow(dead_code)]
pub enum DataType { pub enum DataType {
/// 32-bit floats /// 32-bit floats

View File

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

View File

@ -7,9 +7,7 @@ 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.
// 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(eq, eq_int))]
#[cfg_attr(feature = "python-bindings", pyclass)]
#[derive(PartialEq, Serialize, Deserialize, strum_macros::EnumMessage, Debug, Clone, Copy)] #[derive(PartialEq, Serialize, Deserialize, strum_macros::EnumMessage, Debug, Clone, Copy)]
#[allow(dead_code)] #[allow(dead_code)]
pub enum Qty { pub enum Qty {

View File

@ -161,28 +161,32 @@ 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 = ndarray::ArrayView2::<i8>::from_shape((framesPerBlock, nchannels), dat)?; let arr = ndarray15p6::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 =
ndarray::ArrayView2::<i16>::from_shape((framesPerBlock, nchannels), dat)?; ndarray15p6::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 =
ndarray::ArrayView2::<i32>::from_shape((framesPerBlock, nchannels), dat)?; ndarray15p6::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 =
ndarray::ArrayView2::<f32>::from_shape((framesPerBlock, nchannels), dat)?; ndarray15p6::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 =
ndarray::ArrayView2::<f64>::from_shape((framesPerBlock, nchannels), dat)?; ndarray15p6::ArrayView2::<f64>::from_shape((framesPerBlock, nchannels), dat)?;
ds.write_slice(arr, (ctr, .., ..))?; ds.write_slice(arr, (ctr, .., ..))?;
} }
} }

View File

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

View File

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

View File

@ -308,11 +308,9 @@ 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::*; use crate::{config::*, math::randNormal};
use super::{ApsMode, AvPowerSpectra, CPSResult, Overlap, WindowType}; use super::{ApsMode, AvPowerSpectra, CPSResult, Overlap, WindowType};
use Overlap::Percentage; use Overlap::Percentage;
@ -359,8 +357,7 @@ 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 distr = Normal::new(1.0, 1.0).expect("Distribution cannot be built"); let timedata_some = randNormal((nfft,1));
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
@ -382,8 +379,7 @@ mod test {
#[test] #[test]
fn test_tf1() { fn test_tf1() {
let nfft = 4800; let nfft = 4800;
let distr = Normal::new(1.0, 1.0).unwrap(); let mut timedata = randNormal((nfft, 1));
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();
@ -405,8 +401,7 @@ mod test {
#[test] #[test]
fn test_tf2() { fn test_tf2() {
let nfft = 4800; let nfft = 4800;
let distr = Normal::new(1.0, 1.0).unwrap(); let mut timedata = randNormal((nfft, 1));
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();
@ -431,8 +426,7 @@ mod test {
#[test] #[test]
fn test_ap() { fn test_ap() {
let nfft = 1024; let nfft = 1024;
let distr = Normal::new(1.0, 1.0).unwrap(); let timedata = randNormal((150 * nfft, 1));
let timedata = Dmat::random((150 * nfft, 1), distr);
let timedata_mean_square = (&timedata * &timedata).sum() / (timedata.len() as Flt); let timedata_mean_square = (&timedata * &timedata).sum() / (timedata.len() as Flt);
for wt in [ for wt in [

View File

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

View File

@ -252,7 +252,6 @@ 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 {
@ -267,6 +266,8 @@ 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
@ -349,7 +350,6 @@ 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 = Dmat::random((nfft, 1), StandardNormal); let t: Dmat = randNormal((nfft,1));
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 = 2. * Dmat::random((nfft, 1), StandardNormal); let t: Dmat = randNormal((nfft,1));
let tavg = t.sum() / (nfft as Flt); let tavg = t.sum() / (nfft as Flt);
let t_dc_power = tavg.powi(2); let t_dc_power = tavg.powi(2);

View File

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

View File

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

View File

@ -42,7 +42,7 @@ impl Source {
/// Create a white noise signal source /// Create a white noise signal source
pub fn newWhiteNoise() -> Source { pub fn newWhiteNoise() -> Source {
Source { Source {
src: Box::new(WhiteNoise {}), src: Box::new(WhiteNoise { rng: SmallRng::from_entropy()}),
} }
} }
@ -121,11 +121,14 @@ impl SourceImpl for Silence {
} }
/// White noise source. Can be colored by applying a color filter to the source /// White noise source. Can be colored by applying a color filter to the source
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
struct WhiteNoise {} struct WhiteNoise {
// SmallRng is a cheap random number generator
rng: SmallRng
}
impl SourceImpl for WhiteNoise { impl SourceImpl for WhiteNoise {
fn genSignal_unscaled(&mut self, sig: &mut dyn ExactSizeIterator<Item = &mut Flt>) { fn genSignal_unscaled(&mut self, sig: &mut dyn ExactSizeIterator<Item = &mut Flt>) {
sig.for_each(|s| { sig.for_each(|s| {
*s = thread_rng().sample(StandardNormal); *s = self.rng.sample(StandardNormal);
}); });
} }
fn reset(&mut self, _fs: Flt) {} fn reset(&mut self, _fs: Flt) {}

View File

@ -1,6 +1,6 @@
//! Sweep signal generation code //! Sweep signal generation code
use strum_macros::{Display, EnumMessage};
use strum::EnumMessage; use strum::EnumMessage;
use strum_macros::{Display, EnumMessage};
use { use {
crate::config::*, crate::config::*,
anyhow::{bail, Result}, anyhow::{bail, Result},
@ -10,8 +10,8 @@ const twopi: Flt = 2. * pi;
/// Enumerator representing the type of sweep source to create. Used as /// Enumerator representing the type of sweep source to create. Used as
/// parameter in [Siggen::newSweep]. /// parameter in [Siggen::newSweep].
#[cfg_attr(feature = "python-bindings", pyclass)] #[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
#[derive(Debug, Clone, Display, EnumMessage)] #[derive(Debug, PartialEq, Clone, Display, EnumMessage)]
pub enum SweepType { pub enum SweepType {
/// Forward only logarithmic sweep, repeats itself /// Forward only logarithmic sweep, repeats itself
#[strum(message = "Forward logarithmic")] #[strum(message = "Forward logarithmic")]

View File

@ -3,11 +3,7 @@ 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
@ -37,10 +33,6 @@ 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}")
} }