Compare commits

..

No commits in common. "eb94785a89c12b7507aef3d9bc604c725a34bfb3" and "7662ff176c25a14de732d4e5090cf4cb934ee236" have entirely different histories.

15 changed files with 46 additions and 101 deletions

View File

@ -1,6 +1,6 @@
[package] [package]
name = "lasprs" name = "lasprs"
version = "0.6.1" version = "0.6.0"
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)"
@ -12,7 +12,7 @@ categories = ["multimedia::audio", "science", "mathematics"]
[lib] [lib]
name = "lasprs" name = "lasprs"
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib",]
[dependencies] [dependencies]
# Error handling # Error handling
@ -29,12 +29,8 @@ 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.2", optional = true, features = ["extension-module", "anyhow"]}
"extension-module", numpy = { git = "https://github.com/JRRudy1/rust-numpy", branch = "pyo3-0.22.0" , optional = true}
"anyhow",
] }
# Python bindings for Numpy arrays
numpy = { version = "0.21.0", optional = true }
# White noise etc # White noise etc
rand = "0.8.5" rand = "0.8.5"
@ -78,17 +74,12 @@ itertools = "0.13.0"
approx = "0.5.1" approx = "0.5.1"
# For getting timestamps. Only useful when recording. # For getting timestamps. Only useful when recording.
chrono = { version = "0.4.38", optional = true } chrono = {version = "0.4.38", optional = true}
# For getting UUIDs in recording # For getting UUIDs in recording
uuid = { version = "1.10.0", features = ["v4"], optional = true } uuid = { version = "1.10.0", features = ["v4"] , optional = true}
# Command line argument parser, for CLI apps # Command line argument parser, for CLI apps
clap = { version = "4.5.13", features = [ clap = { version = "4.5.13", features = ["derive", "color", "help", "suggestions"] }
"derive",
"color",
"help",
"suggestions",
] }
# FFT's # FFT's
realfft = "3.3.0" realfft = "3.3.0"
@ -113,21 +104,8 @@ 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
# default = ["f32", "cpal-api", "record"] # default = ["f32", "cpal-api", "record"]
# When building Python bindings of Lasprs, you should enable the feature flag # Use this for debugging extensions
# "python-bindings". When debugging these, to safe the flag specification you # default = ["f64", "python-bindings", "record", "cpal-api"]
# could just uncomment the line below here and comment the one above. If only
# testing the implementations, or building the extension module, you should
# call:
# $ maturin develop -F python-bindings
# Or:
# $ maturin develop --release -F python-bindings
# For faster code
#default = ["f64", "python-bindings", "record", "cpal-api"]
pulse-api = [] pulse-api = []
cpal-api = ["dep:cpal"] cpal-api = ["dep:cpal"]

View File

@ -12,12 +12,11 @@ and processing of (multi) sensor data in real time on a PC and output results.
Documentation is provided at [doc.rs](https://docs.rs/lasprs/latest/lasprs). Documentation is provided at [doc.rs](https://docs.rs/lasprs/latest/lasprs).
## Python bindings
## Python bindings and examples
The library has Python bindings (via [pyo3](https://pyo3.rs), which can be installed via: The library has Python bindings (via [pyo3](https://pyo3.rs), which can be installed via:
``` ```
$ pip install git+https://code.ascee.nl/ascee/lasprs --install-option "python-bindings" $ pip install git+https://code.ascee.nl/ascee/lasprs --install-option "--all-features"
``` ```

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,11 +7,8 @@ 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))]
// For now:
#[cfg_attr(feature = "python-bindings", pyclass)]
#[allow(dead_code)] #[allow(dead_code)]
#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
pub enum DataType { pub enum DataType {
/// 32-bit floats /// 32-bit floats
#[strum(message = "F32", detailed_message = "32-bits floating points")] #[strum(message = "F32", detailed_message = "32-bits floating points")]

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

View File

@ -3,12 +3,7 @@ use crate::config::*;
/// Errors that happen in a stream /// Errors that happen in a stream
#[derive(strum_macros::EnumMessage, PartialEq, Debug, Clone, Display, Copy)] #[derive(strum_macros::EnumMessage, PartialEq, Debug, Clone, Display, Copy)]
#[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)]
pub enum StreamError { pub enum StreamError {
/// Input overrun /// Input overrun
#[strum( #[strum(

View File

@ -478,7 +478,6 @@ impl BandDescriptor for ThirdOctaveBandDescriptor {
} }
} }
#[cfg(feature = "python-bindings")]
#[cfg_attr(feature = "python-bindings", pymethods)] #[cfg_attr(feature = "python-bindings", pymethods)]
impl StandardFilterDescriptor { impl StandardFilterDescriptor {
#[pyo3(name = "genFilter")] #[pyo3(name = "genFilter")]
@ -512,7 +511,7 @@ impl StandardFilterDescriptor {
} }
fn __repr__(&self) -> String { fn __repr__(&self) -> String {
format! {"{:#?}", self} format!{"{:#?}", self}
} }
fn __str__(&self) -> String { fn __str__(&self) -> String {

View File

@ -34,14 +34,12 @@ impl FFT {
nfftF: nfft as Flt, nfftF: nfft as Flt,
} }
} }
pub fn process<'a, T, U>(&mut self, time: T, freq: U) pub fn process<'a, T>(&mut self, time: &[Flt], freq: T)
where where
T: Into<ArrayView<'a, Flt, Ix1>>, T: Into<ArrayViewMut<'a, Cflt, Ix1>>,
U: Into<ArrayViewMut<'a, Cflt, Ix1>>,
{ {
let mut freq = freq.into(); let mut freq = freq.into();
let time = time.into(); self.timescratch.copy_from_slice(time);
self.timescratch.copy_from_slice(time.as_slice().unwrap());
let _ = self let _ = self
.fft .fft
.process(&mut self.timescratch, freq.as_slice_mut().unwrap()); .process(&mut self.timescratch, freq.as_slice_mut().unwrap());

View File

@ -1,11 +1,7 @@
use crate::config::*; use crate::config::*;
use strum_macros::{Display, EnumMessage}; use strum_macros::{Display, 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(Display, Debug, EnumMessage, Default, Clone, PartialEq)] #[derive(Display, Debug, EnumMessage, Default, Clone, PartialEq)]
pub enum FreqWeighting { pub enum FreqWeighting {
/// A-weighting /// A-weighting

View File

@ -7,8 +7,8 @@ use std::usize;
use crate::Dcol; use crate::Dcol;
use super::fft::FFT;
use super::window::*; use super::window::*;
use super::fft::FFT;
use std::mem::MaybeUninit; use std::mem::MaybeUninit;
use realfft::{RealFftPlanner, RealToComplex}; use realfft::{RealFftPlanner, RealToComplex};
@ -46,6 +46,7 @@ pub trait CrossPowerSpecra {
fn tf(&self, chi: usize, chj: usize, chRef: Option<usize>) -> Array1<Cflt>; fn tf(&self, chi: usize, chj: usize, chRef: Option<usize>) -> Array1<Cflt>;
} }
impl CrossPowerSpecra for CPSResult { impl CrossPowerSpecra for CPSResult {
fn ap(&self, ch: usize) -> Array1<Flt> { fn ap(&self, ch: usize) -> Array1<Flt> {
// Slice out one value for all frequencies, map to only real part, and // Slice out one value for all frequencies, map to only real part, and
@ -86,10 +87,10 @@ impl CrossPowerSpecra for CPSResult {
/// estimation. /// estimation.
/// ///
pub struct PowerSpectra { pub struct PowerSpectra {
/// Window used in estimator. The actual Window in here is normalized with /// Window used in estimator
/// the square root of the Window power. This safes one division when pub window: Window,
/// processing time data. /// The window power, is corrected for in power spectra estimants
pub window_normalized: Window, pub sqrt_win_pwr: Flt,
ffts: Vec<FFT>, ffts: Vec<FFT>,
@ -102,7 +103,7 @@ pub struct PowerSpectra {
impl PowerSpectra { impl PowerSpectra {
/// Returns the FFT length used in power spectra computations /// Returns the FFT length used in power spectra computations
pub fn nfft(&self) -> usize { pub fn nfft(&self) -> usize {
self.window_normalized.win.len() self.window.win.len()
} }
/// Create new power spectra estimator. Uses FFT size from window length /// Create new power spectra estimator. Uses FFT size from window length
/// ///
@ -115,12 +116,9 @@ impl PowerSpectra {
/// ///
/// - `window` - A `Window` struct, from which NFFT is also used. /// - `window` - A `Window` struct, from which NFFT is also used.
/// ///
pub fn newFromWindow(mut window: Window) -> PowerSpectra { pub fn newFromWindow(window: Window) -> PowerSpectra {
let nfft = window.win.len(); let nfft = window.win.len();
let win_pwr = window.win.mapv(|w| w.powi(2)).sum() / (nfft as Flt); let win_pwr = window.win.mapv(|w| w.powi(2)).sum() / (nfft as Flt);
let sqrt_win_pwr = Flt::sqrt(win_pwr);
window.win.mapv_inplace(|v| v / sqrt_win_pwr);
assert!(nfft > 0); assert!(nfft > 0);
assert!(nfft % 2 == 0); assert!(nfft % 2 == 0);
@ -130,7 +128,8 @@ impl PowerSpectra {
let Fft = FFT::new(fft); let Fft = FFT::new(fft);
PowerSpectra { PowerSpectra {
window_normalized: window, window,
sqrt_win_pwr: Flt::sqrt(win_pwr),
ffts: vec![Fft], ffts: vec![Fft],
timedata: Array2::zeros((nfft, 1)), timedata: Array2::zeros((nfft, 1)),
freqdata: Array2::zeros((nfft / 2 + 1, 1)), freqdata: Array2::zeros((nfft / 2 + 1, 1)),
@ -139,7 +138,7 @@ impl PowerSpectra {
/// Compute FFTs of input channel data. Stores the scaled FFT data in /// Compute FFTs of input channel data. Stores the scaled FFT data in
/// self.freqdata. /// self.freqdata.
fn compute_ffts(&mut self, timedata: ArrayView2<Flt>) -> ArrayView2<Cflt> { fn compute_ffts(&mut self, timedata: ArrayView2<Flt>) -> &Array2<Cflt> {
let (n, nch) = timedata.dim(); let (n, nch) = timedata.dim();
let nfft = self.nfft(); let nfft = self.nfft();
assert!(n == nfft); assert!(n == nfft);
@ -154,27 +153,25 @@ impl PowerSpectra {
} }
assert!(n == self.nfft()); assert!(n == self.nfft());
assert!(n == self.window_normalized.win.len()); assert!(n == self.window.win.len());
let sqrt_win_pwr = self.sqrt_win_pwr;
// Multiply signals with window function, and compute fft's for each channel // Multiply signals with window function, and compute fft's for each channel
Zip::from(timedata.axis_iter(Axis(1))) Zip::from(timedata.axis_iter(Axis(1)))
.and(self.timedata.axis_iter_mut(Axis(1))) .and(self.timedata.axis_iter_mut(Axis(1)))
.and(&mut self.ffts) .and(&mut self.ffts)
.and(self.freqdata.axis_iter_mut(Axis(1))) .and(self.freqdata.axis_iter_mut(Axis(1)))
.par_for_each(|time_in, mut time_tmp_storage, fft, mut freq| { .par_for_each(|time_in,mut time, fft, mut freq| {
let DC = time_in.mean().unwrap();
azip!((t in &mut time_tmp_storage, &tin in time_in, &win in &self.window_normalized.win) {
// Substract DC value from time data, as this leaks into
// positive frequencies due to windowing.
// Multiply with window and copy over to local time data buffer // Multiply with window and copy over to local time data buffer
*t=(tin-DC)*win}); azip!((t in &mut time, &tin in time_in, &win in &self.window.win) *t=tin*win/sqrt_win_pwr);
fft.process(&time_tmp_storage, &mut freq); let tslice = time.as_slice().unwrap();
freq[0] = DC + 0. * I; let fslice = freq.as_slice_mut().unwrap();
fft.process(tslice, fslice);
}); });
self.freqdata.view() &self.freqdata
} }
/// Compute cross power spectra from input time data. First axis is /// Compute cross power spectra from input time data. First axis is

View File

@ -24,7 +24,7 @@
//! let settings = SLMSettingsBuilder::default() //! let settings = SLMSettingsBuilder::default()
//! .fs(48e3) //! .fs(48e3)
//! .freqWeighting(FreqWeighting::A) //! .freqWeighting(FreqWeighting::A)
//! .timeWeighting(TimeWeighting::Fast{}) //! .timeWeighting(TimeWeighting::Fast)
//! .filterDescriptors(&[desc]).build().unwrap(); //! .filterDescriptors(&[desc]).build().unwrap();
//! //!
//! let mut slm = SLM::new(settings); //! let mut slm = SLM::new(settings);
@ -33,7 +33,7 @@
//! data[0] = 1.; //! data[0] = 1.;
//! //!
//! // Now apply some data. This is a kind of the SLM-s impulse response //! // Now apply some data. This is a kind of the SLM-s impulse response
//! let res = slm.run(data.as_slice().unwrap(), true).unwrap(); //! let res = slm.run(&data.as_slice().unwrap()).unwrap();
//! //!
//! // Only one channel of result data //! // Only one channel of result data
//! assert_eq!(res.len(), 1); //! assert_eq!(res.len(), 1);

View File

@ -28,7 +28,6 @@ pub struct SLMSettings {
pub filterDescriptors: Vec<StandardFilterDescriptor>, pub filterDescriptors: Vec<StandardFilterDescriptor>,
} }
#[cfg(feature="python-bindings")]
#[cfg_attr(feature = "python-bindings", pymethods)] #[cfg_attr(feature = "python-bindings", pymethods)]
impl SLMSettings { impl SLMSettings {
#[new] #[new]

View File

@ -205,7 +205,6 @@ impl SLM {
} }
} }
#[cfg(feature="python-bindings")]
#[cfg_attr(feature = "python-bindings", pymethods)] #[cfg_attr(feature = "python-bindings", pymethods)]
impl SLM { impl SLM {
#[new] #[new]

View File

@ -1,10 +1,6 @@
use crate::config::*; use crate::config::*;
/// 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, PartialEq)] #[derive(Clone, Copy, PartialEq)]
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