Compare commits

..

2 Commits

15 changed files with 101 additions and 46 deletions

View File

@ -1,6 +1,6 @@
[package] [package]
name = "lasprs" name = "lasprs"
version = "0.6.0" version = "0.6.1"
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,8 +29,12 @@ num = "0.4.3"
rayon = "1.10.0" rayon = "1.10.0"
# Python bindings # Python bindings
pyo3 = { version = "0.22.2", optional = true, features = ["extension-module", "anyhow"]} pyo3 = { version = "0.21.2", optional = true, features = [
numpy = { git = "https://github.com/JRRudy1/rust-numpy", branch = "pyo3-0.22.0" , optional = true} "extension-module",
"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"
@ -79,7 +83,12 @@ chrono = {version = "0.4.38", optional = true}
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 = ["derive", "color", "help", "suggestions"] } clap = { version = "4.5.13", features = [
"derive",
"color",
"help",
"suggestions",
] }
# FFT's # FFT's
realfft = "3.3.0" realfft = "3.3.0"
@ -104,7 +113,20 @@ 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"]
# Use this for debugging extensions # When building Python bindings of Lasprs, you should enable the feature flag
# "python-bindings". When debugging these, to safe the flag specification you
# 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"] #default = ["f64", "python-bindings", "record", "cpal-api"]
pulse-api = [] pulse-api = []

View File

@ -12,11 +12,12 @@ 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 "--all-features" $ pip install git+https://code.ascee.nl/ascee/lasprs --install-option "python-bindings"
``` ```

View File

@ -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 {

View File

@ -7,8 +7,11 @@ 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,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

View File

@ -7,8 +7,10 @@ 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,7 +3,12 @@ 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,6 +478,7 @@ 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")]

View File

@ -34,12 +34,14 @@ impl FFT {
nfftF: nfft as Flt, nfftF: nfft as Flt,
} }
} }
pub fn process<'a, T>(&mut self, time: &[Flt], freq: T) pub fn process<'a, T, U>(&mut self, time: T, freq: U)
where where
T: Into<ArrayViewMut<'a, Cflt, Ix1>>, T: Into<ArrayView<'a, Flt, Ix1>>,
U: Into<ArrayViewMut<'a, Cflt, Ix1>>,
{ {
let mut freq = freq.into(); let mut freq = freq.into();
self.timescratch.copy_from_slice(time); let time = time.into();
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,7 +1,11 @@
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::window::*;
use super::fft::FFT; use super::fft::FFT;
use super::window::*;
use std::mem::MaybeUninit; use std::mem::MaybeUninit;
use realfft::{RealFftPlanner, RealToComplex}; use realfft::{RealFftPlanner, RealToComplex};
@ -46,7 +46,6 @@ 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
@ -87,10 +86,10 @@ impl CrossPowerSpecra for CPSResult {
/// estimation. /// estimation.
/// ///
pub struct PowerSpectra { pub struct PowerSpectra {
/// Window used in estimator /// Window used in estimator. The actual Window in here is normalized with
pub window: Window, /// the square root of the Window power. This safes one division when
/// The window power, is corrected for in power spectra estimants /// processing time data.
pub sqrt_win_pwr: Flt, pub window_normalized: Window,
ffts: Vec<FFT>, ffts: Vec<FFT>,
@ -103,7 +102,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.win.len() self.window_normalized.win.len()
} }
/// Create new power spectra estimator. Uses FFT size from window length /// Create new power spectra estimator. Uses FFT size from window length
/// ///
@ -116,9 +115,12 @@ 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(window: Window) -> PowerSpectra { pub fn newFromWindow(mut 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);
@ -128,8 +130,7 @@ impl PowerSpectra {
let Fft = FFT::new(fft); let Fft = FFT::new(fft);
PowerSpectra { PowerSpectra {
window, window_normalized: 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)),
@ -138,7 +139,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>) -> &Array2<Cflt> { fn compute_ffts(&mut self, timedata: ArrayView2<Flt>) -> ArrayView2<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);
@ -153,25 +154,27 @@ impl PowerSpectra {
} }
assert!(n == self.nfft()); assert!(n == self.nfft());
assert!(n == self.window.win.len()); assert!(n == self.window_normalized.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, fft, mut freq| { .par_for_each(|time_in, mut time_tmp_storage, 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
azip!((t in &mut time, &tin in time_in, &win in &self.window.win) *t=tin*win/sqrt_win_pwr); *t=(tin-DC)*win});
let tslice = time.as_slice().unwrap(); fft.process(&time_tmp_storage, &mut freq);
let fslice = freq.as_slice_mut().unwrap(); freq[0] = DC + 0. * I;
fft.process(tslice, fslice);
}); });
&self.freqdata self.freqdata.view()
} }
/// 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()).unwrap(); //! let res = slm.run(data.as_slice().unwrap(), true).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,6 +28,7 @@ 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,6 +205,7 @@ 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,6 +1,10 @@
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