#![warn(
clippy::all,
clippy::pedantic,
clippy::nursery,
clippy::perf,
clippy::missing_docs_in_private_items
)]
use super::AudioNode;
use crate::{
alloc::AudioBuffer,
buffer::{ChannelConfig, ChannelConfigOptions},
context::{AsBaseAudioContext, AudioContextRegistration},
process::{AudioParamValues, AudioProcessor},
SampleRate, MAX_CHANNELS,
};
use num_complex::Complex;
use std::f64::consts::PI;
const MAX_IIR_COEFFS_LEN: usize = 20;
#[allow(clippy::module_name_repetitions)]
pub struct IirFilterOptions {
pub channel_config: ChannelConfigOptions,
pub feedforward: Vec<f64>,
pub feedback: Vec<f64>,
}
#[allow(clippy::module_name_repetitions)]
pub struct IirFilterNode {
sample_rate: f32,
registration: AudioContextRegistration,
channel_config: ChannelConfig,
feedforward: Vec<f64>,
feedback: Vec<f64>,
}
impl AudioNode for IirFilterNode {
fn registration(&self) -> &AudioContextRegistration {
&self.registration
}
fn channel_config_raw(&self) -> &ChannelConfig {
&self.channel_config
}
fn number_of_inputs(&self) -> u32 {
1
}
fn number_of_outputs(&self) -> u32 {
1
}
}
impl IirFilterNode {
pub fn new<C: AsBaseAudioContext>(context: &C, options: IirFilterOptions) -> Self {
context.base().register(move |registration| {
let IirFilterOptions {
feedforward,
feedback,
channel_config,
} = options;
assert!(feedforward.len() <= MAX_IIR_COEFFS_LEN, "NotSupportedError");
assert!(!feedforward.is_empty(), "NotSupportedError");
assert!(!feedforward.iter().all(|&ff| ff == 0.), "InvalidStateError");
assert!(feedback.len() <= MAX_IIR_COEFFS_LEN, "NotSupportedError");
assert!(!feedback.is_empty(), "NotSupportedError");
assert!(!feedback.iter().all(|&ff| ff == 0.), "InvalidStateError");
#[allow(clippy::cast_precision_loss)]
let sample_rate = context.base().sample_rate().0 as f32;
let config = RendererConfig {
feedforward: feedforward.clone(),
feedback: feedback.clone(),
};
let render = IirFilterRenderer::new(config);
let node = Self {
sample_rate,
registration,
channel_config: channel_config.into(),
feedforward,
feedback,
};
(node, Box::new(render))
})
}
#[allow(clippy::cast_possible_truncation)]
pub fn get_frequency_response(
&self,
frequency_hz: &mut [f32],
mag_response: &mut [f32],
phase_response: &mut [f32],
) {
self.validate_inputs(frequency_hz, mag_response, phase_response);
for (i, &f) in frequency_hz.iter().enumerate() {
let mut num: Complex<f64> = Complex::new(0., 0.);
let mut denom: Complex<f64> = Complex::new(0., 0.);
#[allow(clippy::cast_precision_loss)]
for (idx, &ff) in self.feedforward.iter().enumerate() {
num += Complex::from_polar(
ff,
idx as f64 * -2.0 * PI * f64::from(f) / f64::from(self.sample_rate),
);
}
#[allow(clippy::cast_precision_loss)]
for (idx, &fb) in self.feedback.iter().enumerate() {
denom += Complex::from_polar(
fb,
idx as f64 * -2.0 * PI * f64::from(f) / f64::from(self.sample_rate),
);
}
let h_f = num / denom;
mag_response[i] = h_f.norm() as f32;
phase_response[i] = h_f.arg() as f32;
}
}
#[inline]
fn validate_inputs(
&self,
frequency_hz: &mut [f32],
mag_response: &mut [f32],
phase_response: &mut [f32],
) {
assert_eq!(
frequency_hz.len(),
mag_response.len(),
" InvalidAccessError: All paramaters should be the same length"
);
assert_eq!(
mag_response.len(),
phase_response.len(),
" InvalidAccessError: All paramaters should be the same length"
);
let min = 0.;
let max = self.sample_rate / 2.;
for f in frequency_hz.iter_mut() {
*f = f.clamp(min, max);
}
}
}
struct FilterRendererBuilder {
coeffs: Vec<(f64, f64)>,
states: Vec<[f64; MAX_CHANNELS]>,
}
impl FilterRendererBuilder {
#[inline]
fn build(config: RendererConfig) -> Self {
let RendererConfig {
mut feedforward,
mut feedback,
} = config;
match (feedforward.len(), feedback.len()) {
(feedforward_len, feedback_len) if feedforward_len > feedback_len => {
feedforward = feedforward
.into_iter()
.chain(std::iter::repeat(0.))
.take(feedback_len)
.collect();
}
(feedforward_len, feedback_len) if feedforward_len < feedback_len => {
feedback = feedback
.into_iter()
.chain(std::iter::repeat(0.))
.take(feedforward_len)
.collect();
}
_ => (),
};
let coeffs: Vec<(f64, f64)> = feedforward.into_iter().zip(feedback).collect();
let coeffs_len = coeffs.len();
let states = vec![[0.; MAX_CHANNELS]; coeffs_len];
Self { coeffs, states }
}
fn finish(mut self) -> IirFilterRenderer {
let a_0 = self.coeffs[0].1;
for (ff, fb) in &mut self.coeffs {
*ff /= a_0;
*fb /= a_0;
}
IirFilterRenderer {
norm_coeffs: self.coeffs,
states: self.states,
}
}
}
struct RendererConfig {
feedforward: Vec<f64>,
feedback: Vec<f64>,
}
struct IirFilterRenderer {
norm_coeffs: Vec<(f64, f64)>,
states: Vec<[f64; MAX_CHANNELS]>,
}
impl AudioProcessor for IirFilterRenderer {
fn process(
&mut self,
inputs: &[crate::alloc::AudioBuffer],
outputs: &mut [crate::alloc::AudioBuffer],
_params: AudioParamValues,
_timestamp: f64,
_sample_rate: SampleRate,
) {
let input = &inputs[0];
let output = &mut outputs[0];
self.filter(input, output);
}
fn tail_time(&self) -> bool {
true
}
}
impl IirFilterRenderer {
fn new(config: RendererConfig) -> Self {
FilterRendererBuilder::build(config).finish()
}
#[inline]
fn filter(&mut self, input: &AudioBuffer, output: &mut AudioBuffer) {
for (idx, (i_data, o_data)) in input
.channels()
.iter()
.zip(output.channels_mut())
.enumerate()
{
for (&i, o) in i_data.iter().zip(o_data.iter_mut()) {
*o = self.tick(i, idx);
}
}
}
#[inline]
#[allow(clippy::cast_possible_truncation)]
fn tick(&mut self, input: f32, idx: usize) -> f32 {
let input = f64::from(input);
let output = self.norm_coeffs[0].0.mul_add(input, self.states[0][idx]);
for (i, (ff, fb)) in self.norm_coeffs.iter().skip(1).enumerate() {
let state = self.states[i + 1][idx];
self.states[i][idx] = ff * input - fb * output + state;
}
#[cfg(debug_assertions)]
if output.is_nan() || output.is_infinite() {
log::debug!("An unstable filter is processed.");
}
output as f32
}
}
#[cfg(test)]
mod test {
use float_eq::assert_float_eq;
use realfft::num_traits::Zero;
use std::{cmp::min, fs::File};
use crate::{
buffer::ChannelConfigOptions,
context::{AsBaseAudioContext, OfflineAudioContext},
media::{MediaElement, OggVorbisDecoder},
node::{AudioNode, AudioScheduledSourceNode},
snapshot, SampleRate,
};
use super::{IirFilterNode, IirFilterOptions};
const LENGTH: usize = 512;
#[test]
fn build_with_new() {
let context = OfflineAudioContext::new(2, LENGTH, SampleRate(44_100));
let feedforward = vec![
0.000_016_636_797_512_844_526,
0.000_033_273_595_025_689_05,
0.000_016_636_797_512_844_526,
];
let feedback = vec![1.0, -1.988_430_010_622_553_9, 0.988_496_557_812_605_4];
let options = IirFilterOptions {
feedback,
feedforward,
channel_config: ChannelConfigOptions::default(),
};
let _biquad = IirFilterNode::new(&context, options);
}
#[test]
fn build_with_factory_func() {
let context = OfflineAudioContext::new(2, LENGTH, SampleRate(44_100));
let feedforward = vec![
0.000_016_636_797_512_844_526,
0.000_033_273_595_025_689_05,
0.000_016_636_797_512_844_526,
];
let feedback = vec![1.0, -1.988_430_010_622_553_9, 0.988_496_557_812_605_4];
let _biquad = context.create_iir_filter(feedforward, feedback);
}
#[test]
#[should_panic]
fn panics_when_ffs_is_above_max_len() {
let context = OfflineAudioContext::new(2, LENGTH, SampleRate(44_100));
let feedforward = vec![
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
];
let feedback = vec![1.0, -1.988_430_010_622_553_9, 0.988_496_557_812_605_4];
let _biquad = context.create_iir_filter(feedforward, feedback);
}
#[test]
#[should_panic]
fn panics_when_fbs_is_above_max_len() {
let context = OfflineAudioContext::new(2, LENGTH, SampleRate(44_100));
let feedback = vec![
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
];
let feedforward = vec![1.0, -1.988_430_010_622_553_9, 0.988_496_557_812_605_4];
let _biquad = context.create_iir_filter(feedforward, feedback);
}
#[test]
#[should_panic]
fn panics_when_fbs_is_empty() {
let context = OfflineAudioContext::new(2, LENGTH, SampleRate(44_100));
let feedback = vec![];
let feedforward = vec![1.0, -1.988_430_010_622_553_9, 0.988_496_557_812_605_4];
let _biquad = context.create_iir_filter(feedforward, feedback);
}
#[test]
#[should_panic]
fn panics_when_ffs_is_empty() {
let context = OfflineAudioContext::new(2, LENGTH, SampleRate(44_100));
let feedforward = vec![];
let feedback = vec![1.0, -1.988_430_010_622_553_9, 0.988_496_557_812_605_4];
let _biquad = context.create_iir_filter(feedforward, feedback);
}
#[test]
#[should_panic]
fn panics_when_ffs_are_zeros() {
let context = OfflineAudioContext::new(2, LENGTH, SampleRate(44_100));
let feedforward = vec![0., 0.];
let feedback = vec![1.0, -1.988_430_010_622_553_9, 0.988_496_557_812_605_4];
let _biquad = context.create_iir_filter(feedforward, feedback);
}
#[test]
#[should_panic]
fn panics_when_fbs_are_zeros() {
let context = OfflineAudioContext::new(2, LENGTH, SampleRate(44_100));
let feedback = vec![0., 0.];
let feedforward = vec![1.0, -1.988_430_010_622_553_9, 0.988_496_557_812_605_4];
let _biquad = context.create_iir_filter(feedforward, feedback);
}
#[test]
#[should_panic]
fn panics_when_not_the_same_length() {
let context = OfflineAudioContext::new(2, LENGTH, SampleRate(44_100));
let feedforward = vec![
0.000_016_636_797_512_844_526,
0.000_033_273_595_025_689_05,
0.000_016_636_797_512_844_526,
];
let feedback = vec![1.0, -1.988_430_010_622_553_9, 0.988_496_557_812_605_4];
let options = IirFilterOptions {
feedback,
feedforward,
channel_config: ChannelConfigOptions::default(),
};
let biquad = IirFilterNode::new(&context, options);
let mut frequency_hz = [0.];
let mut mag_response = [0., 1.0];
let mut phase_response = [0.];
biquad.get_frequency_response(&mut frequency_hz, &mut mag_response, &mut phase_response);
}
#[test]
#[should_panic]
fn panics_when_not_the_same_length_2() {
let context = OfflineAudioContext::new(2, LENGTH, SampleRate(44_100));
let feedforward = vec![
0.000_016_636_797_512_844_526,
0.000_033_273_595_025_689_05,
0.000_016_636_797_512_844_526,
];
let feedback = vec![1.0, -1.988_430_010_622_553_9, 0.988_496_557_812_605_4];
let options = IirFilterOptions {
feedback,
feedforward,
channel_config: ChannelConfigOptions::default(),
};
let biquad = IirFilterNode::new(&context, options);
let mut frequency_hz = [0.];
let mut mag_response = [0.];
let mut phase_response = [0., 1.0];
biquad.get_frequency_response(&mut frequency_hz, &mut mag_response, &mut phase_response);
}
#[test]
fn frequencies_are_clamped() {
let context = OfflineAudioContext::new(2, LENGTH, SampleRate(44_100));
let feedforward = vec![
0.000_016_636_797_512_844_526,
0.000_033_273_595_025_689_05,
0.000_016_636_797_512_844_526,
];
let feedback = vec![1.0, -1.988_430_010_622_553_9, 0.988_496_557_812_605_4];
let options = IirFilterOptions {
feedback,
feedforward,
channel_config: ChannelConfigOptions::default(),
};
let iir = IirFilterNode::new(&context, options);
#[allow(clippy::cast_precision_loss)]
let niquyst = context.sample_rate().0 as f32 / 2.0;
let mut frequency_hz = [-100., 1_000_000.];
let mut mag_response = [0., 0.];
let mut phase_response = [0., 0.];
iir.get_frequency_response(&mut frequency_hz, &mut mag_response, &mut phase_response);
let ref_arr = [0., niquyst];
assert_float_eq!(frequency_hz, ref_arr, ulps_all <= 0);
}
#[test]
fn check_get_frequency_response() {
let ref_mag = [
1e-3,
4.152_807e-4,
1.460_789_5e-3,
5.051_316e-3,
1.130_323_5e-2,
2.230_340_2e-2,
4.311_698e-2,
8.843_45e-2,
2.146_620_2e-1,
6.802_952e-1,
];
let context = OfflineAudioContext::new(2, LENGTH, SampleRate(44_100));
let feedforward = vec![
0.019_618_022_238_052_212,
-0.036_007_928_102_449_24,
0.019_618_022_238_052_21,
];
let feedback = vec![1., 1.576_436_200_538_313_7, 0.651_680_173_116_867_3];
let options = IirFilterOptions {
feedback,
feedforward,
channel_config: ChannelConfigOptions::default(),
};
let iir = IirFilterNode::new(&context, options);
let mut frequency_hz = [
0., 2205., 4410., 6615., 8820., 11025., 13230., 15435., 17640., 19845.,
];
let mut mag_response = [0.; 10];
let mut phase_response = [0.; 10];
iir.get_frequency_response(&mut frequency_hz, &mut mag_response, &mut phase_response);
assert_float_eq!(mag_response, ref_mag, ulps_all <= 0);
}
#[test]
fn default_periodic_wave_rendering_should_match_snapshot() {
let ref_filtered =
snapshot::read("./snapshots/white_hp.json").expect("Reading snapshot file failed");
let mut context = OfflineAudioContext::new(1, LENGTH, SampleRate(44_100));
let file = File::open("white.ogg").unwrap();
let stream = OggVorbisDecoder::try_new(file).unwrap();
let media = MediaElement::new(stream);
let background = context.create_media_element_source(media);
let feedforward = vec![
0.019_618_022_238_052_212,
-0.036_007_928_102_449_24,
0.019_618_022_238_052_21,
];
let feedback = vec![1., 1.576_436_200_538_313_7, 0.651_680_173_116_867_3];
let options = IirFilterOptions {
feedback,
feedforward,
channel_config: ChannelConfigOptions::default(),
};
let iir = IirFilterNode::new(&context, options);
background.connect(&iir);
iir.connect(&context.destination());
background.start();
let output = context.start_rendering();
let data_ch: Vec<f32> = output
.channel_data(0)
.as_slice()
.iter()
.filter(|x| !x.is_zero())
.copied()
.collect();
let ref_data_ch = ref_filtered.data;
let min_len = min(data_ch.len(), ref_data_ch.len());
assert_float_eq!(data_ch[0..min_len], ref_data_ch[0..min_len], ulps_all <= 0);
}
}