Added loose gain value to SeriesBiquad. Implemented pink noise. Bup to 0.6.5

This commit is contained in:
Anne de Jong 2024-10-27 21:33:45 +01:00
parent 636213c2b7
commit 9e4ce617ae
7 changed files with 177 additions and 32 deletions

View File

@ -1,6 +1,6 @@
[package]
name = "lasprs"
version = "0.6.4"
version = "0.6.5"
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)"

View File

@ -24,13 +24,14 @@ pub use dummy::DummyFilter;
pub use seriesbiquad::SeriesBiquad;
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. The filter
/// trait is implemented by, for example, [Biquad], [SeriesBiquad], and
/// [BiquadBank].
pub trait Filter: Send + Debug {
//! 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
/// length as input.
fn filter(&mut self, input: &[Flt]) -> Vd;
/// Reset the filter state(s). In essence, this makes sure that all memory of the past is
/// forgotten.
fn reset(&mut self);

View File

@ -13,6 +13,8 @@ use anyhow::{bail, Result};
/// See (tests)
///
pub struct SeriesBiquad {
// A loose gain value
gain: Flt,
biqs: Vec<Biquad>,
}
@ -59,7 +61,12 @@ impl SeriesBiquad {
///
pub fn newFromBiqs(biqs: Vec<Biquad>) -> SeriesBiquad {
assert!(biqs.len() > 0);
SeriesBiquad { biqs }
SeriesBiquad { biqs, gain: 1.0 }
}
/// Set an overall gain value to amplify / attenuate overall output
pub fn setGain(&mut self, g: Flt) {
self.gain = g;
}
/// Return reference to internally stored biquads
@ -94,7 +101,7 @@ impl SeriesBiquad {
bail!("No filter coefficients given!");
}
Ok(SeriesBiquad { biqs })
Ok(SeriesBiquad { biqs, gain: 1.0 })
}
/// Unit impulse response series biquad. Input = output
@ -107,6 +114,14 @@ impl SeriesBiquad {
fn clone_dyn(&self) -> Box<dyn Filter> {
Box::new(self.clone())
}
/// Filter input signal in place
pub fn filter_inout(&mut self, inout: &mut [Flt]) {
for biq in self.biqs.iter_mut() {
biq.filter_inout(inout);
}
inout.iter_mut().for_each(|io| *io *= self.gain);
}
}
impl Filter for SeriesBiquad {
@ -115,9 +130,7 @@ impl Filter for SeriesBiquad {
//!
fn filter(&mut self, input: &[Flt]) -> Vd {
let mut inout = input.to_vec();
for biq in self.biqs.iter_mut() {
biq.filter_inout(&mut inout);
}
self.filter_inout(&mut inout);
inout
}
fn reset(&mut self) {
@ -130,7 +143,7 @@ impl Filter for SeriesBiquad {
impl<'a, T: AsArray<'a, Flt>> TransferFunction<'a, T> for SeriesBiquad {
fn tf(&self, fs: Flt, freq: T) -> Ccol {
let freq = freq.into();
let mut res = self.biqs.first().unwrap().tf(fs, freq);
let mut res = self.biqs.first().unwrap().tf(fs, freq) * self.gain;
for biq in self.biqs.iter().skip(1) {
res = &res * biq.tf(fs, freq);
}

View File

@ -21,6 +21,7 @@
//!
//! ```
mod siggen;
mod noise;
mod siggenchannel;
mod source;
mod siggencmd;

130
src/siggen/noise.rs Normal file
View File

@ -0,0 +1,130 @@
//! (Colored) noise Source implementations. Uses bilinear transform for a fixe and
//! pre-calculated set of analogue poles and zeros.
//!
use super::source::SourceImpl;
use crate::{config::*, PoleOrZero, SeriesBiquad, TransferFunction, ZPKModel};
use rand::{rngs::SmallRng, Rng, SeedableRng};
use rand_distr::StandardNormal;
/// The number of poles in the filter that turns white noise into pink noise.
const PINKNOISE_ANALOG_ORDER: usize = 10;
/// White noise source. Can be colored by applying a color filter to the source
#[derive(Clone, Debug)]
pub struct WhiteNoise {
// SmallRng is a cheap random number generator
rng: SmallRng,
}
impl WhiteNoise {
pub fn new() -> Self {
WhiteNoise {
rng: SmallRng::from_entropy(),
}
}
}
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())
}
}
#[derive(Debug, Clone)]
pub struct ColoredNoise {
// White noise generator
wn: WhiteNoise,
tmp: Vec<Flt>,
analogue_blueprint: ZPKModel,
filter: SeriesBiquad,
}
impl ColoredNoise {
/// Generate a colored noise signal source that outputs pink noise (-3 dB /
/// octave ) from 20 Hz to 20 kHz.
pub fn newPinkNoise() -> Self {
let twopi = 2. * pi;
let fl = 10.;
let fu = 20e3;
// Exponentially spread poles
let fpoles_real: Vec<Flt> = (0..PINKNOISE_ANALOG_ORDER)
.map(|i| fl * (fu / fl).powf((i as Flt) / (PINKNOISE_ANALOG_ORDER as Flt - 1.)))
.collect();
// Zeros as geometric means inbetween
let fzeros_real: Vec<Flt> = (0..PINKNOISE_ANALOG_ORDER - 1)
.map(|i| (fpoles_real[i] * fpoles_real[i + 1]).sqrt())
.collect();
let poles: Vec<PoleOrZero> = fpoles_real
.into_iter()
.map(|f| PoleOrZero::Real1(-twopi * f))
.collect();
let zeros: Vec<PoleOrZero> = fzeros_real
.into_iter()
.map(|f| PoleOrZero::Real1(-twopi * f))
.collect();
let gain = 1.;
let analogue_blueprint = ZPKModel::new(zeros, poles, gain);
let filter = analogue_blueprint.bilinear(480000.);
Self {
wn: WhiteNoise::new(),
tmp: vec![],
analogue_blueprint,
filter,
}
}
}
impl SourceImpl for ColoredNoise {
fn genSignal_unscaled(&mut self, sig: &mut dyn ExactSizeIterator<Item = &mut Flt>) {
// Make temporary array of same size as output iterator
self.tmp.resize(sig.len(), 0.);
// Generate white noise signal in place
self.wn.genSignal_unscaled(&mut self.tmp.iter_mut());
// Then, filter it with noise shaping filter
self.filter.filter_inout(&mut self.tmp);
// And copy over to output iterator
sig.zip(&self.tmp).for_each(|(sig, src)| {
*sig = *src;
});
}
fn reset(&mut self, fs: Flt) {
// Pre-computed analog poles
self.filter = self.analogue_blueprint.bilinear(fs);
let fnyq = fs / 2.;
// We set the gain such that the signal power is the same as that for
// the white noise that enters it. We do this by calculating the sum of
// the power in each bin up to the Nyquist frequency, and calculating a
// gain that is the inverse of this value.
// Choose a number of points. As long as its fine enough over the whole
// range (no peaks / dips are missing) At 48 kHz this means we sample
// the tf with a resolution of 10 Hz. Fine enough for slowly varying
// filter gains.
let N = 2400;
let freq = Array1::linspace(20., fnyq, 2000);
let tf_power_sum = self
.filter
.tf(fs, freq.view())
.mapv(|t| t.abs().powi(2))
.sum();
// The above is sum of squares. The white noise signal is unit
// variance, meaning also unit linear level. So to compute gain, we
// have to take the square root of this ratio.
let gain = ((N as Flt) / tf_power_sum).sqrt();
// dbg!(gain);
self.filter.setGain(gain);
}
fn clone_dyn(&self) -> Box<dyn SourceImpl> {
Box::new(self.clone())
}
}

View File

@ -21,6 +21,9 @@ use std::slice::IterMut;
#[derive(Clone, Debug)]
#[cfg_attr(feature = "python-bindings", pyclass)]
pub struct Siggen {
// Sampling frequency in Hz
fs: Option<Flt>,
// The source dynamic signal. Noise, a sine wave, sweep, etc
source: Source,
@ -71,6 +74,7 @@ impl Siggen {
/// - `source` - Source function
pub fn new(nchannels: usize, source: Source) -> Siggen {
Siggen {
fs: None,
source,
channels: vec![SiggenChannelConfig::new(); nchannels],
source_buf: vec![],
@ -141,6 +145,8 @@ impl Siggen {
match msg {
SiggenCommand::ChangeSource { src } => {
self.source = src;
self.reset(self.fs.unwrap_or(48e3));
Ok(())
}
SiggenCommand::ResetSiggen { fs } => {
@ -240,6 +246,7 @@ impl Siggen {
/// * fs: (New) Sampling frequency \[Hz\]
///
pub fn reset(&mut self, fs: Flt) {
self.fs = Some(fs);
self.source.reset(fs);
self.channels.iter_mut().for_each(|x| x.reset(fs))
}

View File

@ -1,5 +1,6 @@
//! All sources for a signal generator. Sine waves, sweeps, noise, etc.
use super::sweep::{SweepParams, SweepType};
use super::noise::{ColoredNoise, WhiteNoise};
use crate::config::*;
use std::fmt::Debug;
use std::ops::{Deref, DerefMut};
@ -9,8 +10,6 @@ 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.
@ -42,7 +41,13 @@ impl Source {
/// Create a white noise signal source
pub fn newWhiteNoise() -> Source {
Source {
src: Box::new(WhiteNoise { rng: SmallRng::from_entropy()}),
src: Box::new(WhiteNoise::new()),
}
}
/// Create a pink noise signal source
pub fn newPinkNoise() -> Source {
Source {
src: Box::new(ColoredNoise::newPinkNoise()),
}
}
@ -89,6 +94,11 @@ impl Source {
Self::newWhiteNoise()
}
#[staticmethod]
#[pyo3(name = "newPinkNoise")]
fn newPinkNoise_py() -> Source {
Self::newPinkNoise()
}
#[staticmethod]
#[pyo3(name = "newSweep")]
fn newSweep_py(
fs: Flt,
@ -119,23 +129,6 @@ impl SourceImpl for Silence {
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)]