Compare commits

..

2 Commits

15 changed files with 101 additions and 46 deletions

View File

@ -1,6 +1,6 @@
[package]
name = "lasprs"
version = "0.6.0"
version = "0.6.1"
edition = "2021"
authors = ["J.A. de Jong <j.a.dejong@ascee.nl>"]
description = "Library for Acoustic Signal Processing (Rust edition, with optional Python bindings via pyo3)"
@ -12,7 +12,7 @@ categories = ["multimedia::audio", "science", "mathematics"]
[lib]
name = "lasprs"
crate-type = ["cdylib", "rlib",]
crate-type = ["cdylib", "rlib"]
[dependencies]
# Error handling
@ -29,8 +29,12 @@ num = "0.4.3"
rayon = "1.10.0"
# Python bindings
pyo3 = { version = "0.22.2", optional = true, features = ["extension-module", "anyhow"]}
numpy = { git = "https://github.com/JRRudy1/rust-numpy", branch = "pyo3-0.22.0" , optional = true}
pyo3 = { version = "0.21.2", optional = true, features = [
"extension-module",
"anyhow",
] }
# Python bindings for Numpy arrays
numpy = { version = "0.21.0", optional = true }
# White noise etc
rand = "0.8.5"
@ -79,7 +83,12 @@ chrono = {version = "0.4.38", optional = true}
uuid = { version = "1.10.0", features = ["v4"], optional = true }
# 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
realfft = "3.3.0"
@ -104,7 +113,20 @@ default = ["f64", "cpal-api", "record"]
# Use this to test if everything works well in f32
# 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"]
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).
## Python bindings
## Python bindings and examples
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
#[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)]
#[allow(dead_code)]
pub enum StreamApiDescr {

View File

@ -7,8 +7,11 @@ use crate::config::*;
/// Data type description for samples coming from a stream
#[derive(strum_macros::EnumMessage, PartialEq, Copy, Debug, Clone, Serialize, Deserialize)]
// Do the following when Pyo3 0.22 can finally be used combined with rust-numpy:
//#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
// For now:
#[cfg_attr(feature = "python-bindings", pyclass)]
#[allow(dead_code)]
#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
pub enum DataType {
/// 32-bit floats
#[strum(message = "F32", detailed_message = "32-bits floating points")]

View File

@ -50,7 +50,10 @@ use api::*;
use crate::config::*;
/// 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)]
pub enum StreamType {
/// Input-only stream

View File

@ -7,8 +7,10 @@ use strum_macros;
use serde::{Serialize, Deserialize};
/// Physical quantities that are I/O of a Daq device.
// Do the following when Pyo3 0.22 can finally be used combined with rust-numpy:
// #[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
#[cfg_attr(feature = "python-bindings", pyclass)]
#[derive(PartialEq, Serialize, Deserialize, strum_macros::EnumMessage, Debug, Clone, Copy)]
#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))]
#[allow(dead_code)]
pub enum Qty {
/// Number

View File

@ -3,7 +3,12 @@ use crate::config::*;
/// Errors that happen in a stream
#[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 {
/// Input overrun
#[strum(

View File

@ -478,6 +478,7 @@ impl BandDescriptor for ThirdOctaveBandDescriptor {
}
}
#[cfg(feature = "python-bindings")]
#[cfg_attr(feature = "python-bindings", pymethods)]
impl StandardFilterDescriptor {
#[pyo3(name = "genFilter")]

View File

@ -34,12 +34,14 @@ impl FFT {
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
T: Into<ArrayViewMut<'a, Cflt, Ix1>>,
T: Into<ArrayView<'a, Flt, Ix1>>,
U: Into<ArrayViewMut<'a, Cflt, Ix1>>,
{
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
.fft
.process(&mut self.timescratch, freq.as_slice_mut().unwrap());

View File

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

View File

@ -7,8 +7,8 @@ use std::usize;
use crate::Dcol;
use super::window::*;
use super::fft::FFT;
use super::window::*;
use std::mem::MaybeUninit;
use realfft::{RealFftPlanner, RealToComplex};
@ -46,7 +46,6 @@ pub trait CrossPowerSpecra {
fn tf(&self, chi: usize, chj: usize, chRef: Option<usize>) -> Array1<Cflt>;
}
impl CrossPowerSpecra for CPSResult {
fn ap(&self, ch: usize) -> Array1<Flt> {
// Slice out one value for all frequencies, map to only real part, and
@ -87,10 +86,10 @@ impl CrossPowerSpecra for CPSResult {
/// estimation.
///
pub struct PowerSpectra {
/// Window used in estimator
pub window: Window,
/// The window power, is corrected for in power spectra estimants
pub sqrt_win_pwr: Flt,
/// Window used in estimator. The actual Window in here is normalized with
/// the square root of the Window power. This safes one division when
/// processing time data.
pub window_normalized: Window,
ffts: Vec<FFT>,
@ -103,7 +102,7 @@ pub struct PowerSpectra {
impl PowerSpectra {
/// Returns the FFT length used in power spectra computations
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
///
@ -116,9 +115,12 @@ impl PowerSpectra {
///
/// - `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 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 % 2 == 0);
@ -128,8 +130,7 @@ impl PowerSpectra {
let Fft = FFT::new(fft);
PowerSpectra {
window,
sqrt_win_pwr: Flt::sqrt(win_pwr),
window_normalized: window,
ffts: vec![Fft],
timedata: Array2::zeros((nfft, 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
/// 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 nfft = self.nfft();
assert!(n == nfft);
@ -153,25 +154,27 @@ impl PowerSpectra {
}
assert!(n == self.nfft());
assert!(n == self.window.win.len());
let sqrt_win_pwr = self.sqrt_win_pwr;
assert!(n == self.window_normalized.win.len());
// Multiply signals with window function, and compute fft's for each channel
Zip::from(timedata.axis_iter(Axis(1)))
.and(self.timedata.axis_iter_mut(Axis(1)))
.and(&mut self.ffts)
.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
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();
let fslice = freq.as_slice_mut().unwrap();
fft.process(tslice, fslice);
fft.process(&time_tmp_storage, &mut freq);
freq[0] = DC + 0. * I;
});
&self.freqdata
self.freqdata.view()
}
/// Compute cross power spectra from input time data. First axis is

View File

@ -24,7 +24,7 @@
//! let settings = SLMSettingsBuilder::default()
//! .fs(48e3)
//! .freqWeighting(FreqWeighting::A)
//! .timeWeighting(TimeWeighting::Fast)
//! .timeWeighting(TimeWeighting::Fast{})
//! .filterDescriptors(&[desc]).build().unwrap();
//!
//! let mut slm = SLM::new(settings);
@ -33,7 +33,7 @@
//! data[0] = 1.;
//!
//! // 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
//! assert_eq!(res.len(), 1);

View File

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

View File

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

View File

@ -1,6 +1,10 @@
use crate::config::*;
/// 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)]
pub enum TimeWeighting {
// I know that the curly braces here are not required and add some