diff --git a/audio/src/biquad_filter_node.rs b/audio/src/biquad_filter_node.rs new file mode 100644 index 00000000..c5cd46ab --- /dev/null +++ b/audio/src/biquad_filter_node.rs @@ -0,0 +1,318 @@ +use block::Chunk; +use block::Tick; +use node::AudioNodeEngine; +use node::BlockInfo; +use node::{AudioNodeMessage, AudioNodeType, ChannelInfo}; +use param::{Param, ParamType}; +use smallvec::SmallVec; +use std::f32::consts::{PI, SQRT_2}; + +#[derive(Copy, Clone, Debug)] +pub struct BiquadFilterNodeOptions { + pub filter: FilterType, + pub frequency: f32, + pub detune: f32, + pub q: f32, + pub gain: f32, +} + +#[derive(Copy, Clone, Debug)] +pub enum FilterType { + LowPass, + HighPass, + BandPass, + LowShelf, + HighShelf, + Peaking, + Notch, + AllPass +} + +impl Default for BiquadFilterNodeOptions { + fn default() -> Self { + BiquadFilterNodeOptions { + filter: FilterType::LowPass, + frequency: 350., + detune: 0., + q: 1., + gain: 0., + } + } +} + +#[derive(Copy, Clone, Debug)] +pub enum BiquadFilterNodeMessage { + SetFilterType(FilterType) +} + +/// The last two input and output values, per-channel +// Default sets all fields to zero +#[derive(Default, Copy, Clone, PartialEq)] +struct BiquadState { + /// The input value from last frame + x1: f32, + /// The input value from two frames ago + x2: f32, + /// The output value from last frame + y1: f32, + /// The output value from two frames ago + y2: f32, +} + +impl BiquadState { + /// Update with new input/output values from this frame + fn update(&mut self, x: f32, y: f32) { + self.x2 = self.x1; + self.x1 = x; + self.y2 = self.y1; + self.y1 = y; + } +} + +/// https://webaudio.github.io/web-audio-api/#biquadfilternode +#[derive(AudioNodeCommon)] +pub(crate) struct BiquadFilterNode { + channel_info: ChannelInfo, + filter: FilterType, + frequency: Param, + detune: Param, + q: Param, + gain: Param, + /// The computed filter parameter b0 + /// This is actually b0 / a0, we pre-divide + /// for efficiency + b0: f32, + /// The computed filter parameter b1 + /// This is actually b1 / a0, we pre-divide + /// for efficiency + b1: f32, + /// The computed filter parameter b2 + /// This is actually b2 / a0, we pre-divide + /// for efficiency + b2: f32, + /// The computed filter parameter a1 + /// This is actually a1 / a0, we pre-divide + /// for efficiency + a1: f32, + /// The computed filter parameter a2 + /// This is actually a2 / a0, we pre-divide + /// for efficiency + a2: f32, + /// Stored filter state, this contains the last two + /// frames of input and output values for every + /// channel + state: SmallVec<[BiquadState; 2]>, +} + +impl BiquadFilterNode { + pub fn new(options: BiquadFilterNodeOptions, + channel_info: ChannelInfo, + sample_rate: f32) -> Self { + let mut ret = Self { + channel_info, + filter: options.filter, + frequency: Param::new(options.frequency), + gain: Param::new(options.gain), + q: Param::new(options.q), + detune: Param::new(options.detune), + b0: 0., b1: 0., b2: 0., a1: 0., a2: 0., + state: SmallVec::new(), + }; + ret.update_coefficients(sample_rate); + ret + } + + pub fn update_parameters(&mut self, info: &BlockInfo, tick: Tick) -> bool { + let mut changed = self.frequency.update(info, tick); + changed |= self.detune.update(info, tick); + changed |= self.q.update(info, tick); + changed |= self.gain.update(info, tick); + + if changed { + self.update_coefficients(info.sample_rate); + } + changed + } + + /// Update the coefficients a1, a2, b0, b1, b2, given the sample_rate + /// + /// See https://webaudio.github.io/web-audio-api/#filters-characteristics + fn update_coefficients(&mut self, fs: f32) { + let g = self.gain.value(); + let q = self.q.value(); + let f0 = self.frequency.value() * (2.0_f32).powf(self.detune.value() / 1200.); + + // clamp to nominal range + // https://webaudio.github.io/web-audio-api/#biquadfilternode + let f0 = if f0 > fs / 2. || !f0.is_finite() { + fs / 2. + } else if f0 < 0. { + 0. + } else { + f0 + }; + + let a = 10.0_f32.powf(g / 40.); + let omega0 = 2. * PI * f0 / fs; + let sin_omega = omega0.sin(); + let cos_omega = omega0.cos(); + let alpha_q = sin_omega / (2. * q); + let alpha_q_db = sin_omega / (2. * 10.0_f32.powf(q / 20.)); + let alpha_s = sin_omega / SQRT_2; + + // we predivide by a0 + let a0; + + match self.filter { + FilterType::LowPass => { + self.b0 = (1. - cos_omega) / 2.; + self.b1 = 1. - cos_omega; + self.b2 = self.b1 / 2.; + a0 = 1. + alpha_q_db; + self.a1 = -2. * cos_omega; + self.a2 = 1. - alpha_q_db; + } + FilterType::HighPass => { + self.b0 = (1. + cos_omega) / 2.; + self.b1 = - (1. + cos_omega); + self.b2 = - self.b1 / 2.; + a0 = 1. + alpha_q_db; + self.a1 = -2. * cos_omega; + self.a2 = 1. - alpha_q_db; + } + FilterType::BandPass => { + self.b0 = alpha_q; + self.b1 = 0.; + self.b2 = -alpha_q; + a0 = 1. + alpha_q; + self.a1 = - 2. * cos_omega; + self.a2 = 1. - alpha_q; + } + FilterType::Notch => { + self.b0 = 1.; + self.b1 = -2. * cos_omega; + self.b2 = 1.; + a0 = 1. + alpha_q; + self.a1 = -2. * cos_omega; + self.a2 = 1. - alpha_q; + } + FilterType::AllPass => { + self.b0 = 1. - alpha_q; + self.b1 = -2. * cos_omega; + self.b2 = 1. + alpha_q; + a0 = 1. + alpha_q; + self.a1 = -2. * cos_omega; + self.a2 = 1. - alpha_q; + } + FilterType::Peaking => { + self.b0 = 1. + alpha_q * a; + self.b1 = -2. * cos_omega; + self.b2 = 1. - alpha_q * a; + a0 = 1. + alpha_q / a; + self.a1 = -2. * cos_omega; + self.a2 = 1. - alpha_q / a; + } + FilterType::LowShelf => { + let alpha_rt_a = 2. * alpha_s * a.sqrt(); + self.b0 = a * ((a + 1.) - (a - 1.) * cos_omega + alpha_rt_a); + self.b1 = 2. * a * ((a - 1.) - (a + 1.) * cos_omega); + self.b2 = a * ((a + 1.) - (a - 1.) * cos_omega - alpha_rt_a); + a0 = (a + 1.) + (a - 1.) * cos_omega + alpha_rt_a; + self.a1 = -2. * ((a - 1.) + (a + 1.) * cos_omega); + self.a2 = (a + 1.) + (a - 1.) * cos_omega - alpha_rt_a; + } + FilterType::HighShelf => { + let alpha_rt_a = 2. * alpha_s * a.sqrt(); + self.b0 = a * ((a + 1.) + (a - 1.) * cos_omega + alpha_rt_a); + self.b1 = -2. * a * ((a - 1.) + (a + 1.) * cos_omega); + self.b2 = a * ((a + 1.) + (a - 1.) * cos_omega - alpha_rt_a); + a0 = (a + 1.) - (a - 1.) * cos_omega + alpha_rt_a; + self.a1 = 2. * ((a - 1.) - (a + 1.) * cos_omega); + self.a2 = (a + 1.) - (a - 1.) * cos_omega - alpha_rt_a; + } + } + self.b0 = self.b0 / a0; + self.b1 = self.b1 / a0; + self.b2 = self.b2 / a0; + self.a1 = self.a1 / a0; + self.a2 = self.a2 / a0; + } +} + +impl AudioNodeEngine for BiquadFilterNode { + fn node_type(&self) -> AudioNodeType { + AudioNodeType::BiquadFilterNode + } + + fn process(&mut self, mut inputs: Chunk, info: &BlockInfo) -> Chunk { + debug_assert!(inputs.len() == 1); + self.state.resize(inputs.blocks[0].chan_count() as usize, Default::default()); + self.update_parameters(info, info.frame); + + // XXXManishearth this node has tail time, so even if the block is silence + // we must still compute things on it. However, it is possible to become + // a dumb passthrough as long as we reach a quiescent state + // + // see https://dxr.mozilla.org/mozilla-central/rev/87a95e1b7ec691bef7b938e722fe1b01cce68664/dom/media/webaudio/blink/Biquad.cpp#81-91 + + let repeat_or_silence = inputs.blocks[0].is_silence() || inputs.blocks[0].is_repeat(); + + + if repeat_or_silence && !self.state.iter().all(|s| *s == self.state[0]) { + // In case our input is repeat/silence but our states are not identical, we must + // explicitly duplicate, since mutate_with will otherwise only operate + // on the first channel, ignoring the states of the later ones + inputs.blocks[0].explicit_repeat(); + } else { + // In case the states are identical, just make any silence explicit, + // since mutate_with can't handle silent blocks + inputs.blocks[0].explicit_silence(); + } + + { + let mut iter = inputs.blocks[0].iter(); + while let Some(mut frame) = iter.next() { + self.update_parameters(info, frame.tick()); + frame.mutate_with(|sample, chan| { + let state = &mut self.state[chan as usize]; + let x0 = *sample; + *sample = self.b0 * x0 + self.b1 * state.x1 + self.b2 * state.x2 + - self.a1 * state.y1 - self.a2 * state.y2; + state.update(x0, *sample); + }); + } + + } + + if inputs.blocks[0].is_repeat() { + let state = self.state[0]; + self.state.iter_mut().for_each(|s| *s = state); + } + + inputs + } + + fn get_param(&mut self, id: ParamType) -> &mut Param { + match id { + ParamType::Frequency => &mut self.frequency, + ParamType::Detune => &mut self.detune, + ParamType::Q => &mut self.q, + ParamType::Gain => &mut self.gain, + _ => panic!("Unknown param {:?} for BiquadFilterNode", id), + } + } + + fn message_specific(&mut self, message: AudioNodeMessage, sample_rate: f32) { + match message { + AudioNodeMessage::BiquadFilterNode(m) => { + match m { + BiquadFilterNodeMessage::SetFilterType(f) => { + self.filter = f; + self.update_coefficients(sample_rate); + } + } + } + _ => () + } + } +} diff --git a/audio/src/block.rs b/audio/src/block.rs index 6f41fd56..6a0adb3d 100644 --- a/audio/src/block.rs +++ b/audio/src/block.rs @@ -182,6 +182,10 @@ impl Block { self.buffer.is_empty() } + pub fn is_repeat(&self) -> bool { + self.repeat + } + pub fn data_chan_frame(&self, frame: usize, chan: u8) -> f32 { if self.is_silence() { 0. @@ -519,10 +523,12 @@ impl<'a> FrameRef<'a> { /// (Helpers for the other cases will eventually exist) /// /// Block must not be silence + /// + /// The second parameter to f is the channel number, 0 in case of a repeat() #[inline] - pub fn mutate_with(&mut self, f: F) + pub fn mutate_with(&mut self, mut f: F) where - F: Fn(&mut f32), + F: FnMut(&mut f32, u8), { debug_assert!( !self.block.is_silence(), @@ -530,11 +536,12 @@ impl<'a> FrameRef<'a> { call .explicit_silence() if you wish to use this" ); if self.block.repeat { - f(&mut self.block.buffer[self.frame.0 as usize]) + f(&mut self.block.buffer[self.frame.0 as usize], 0) } else { for chan in 0..self.block.channels { f(&mut self.block.buffer - [chan as usize * FRAMES_PER_BLOCK_USIZE + self.frame.0 as usize]) + [chan as usize * FRAMES_PER_BLOCK_USIZE + self.frame.0 as usize], + chan) } } } diff --git a/audio/src/gain_node.rs b/audio/src/gain_node.rs index 23eca4d6..a5697f77 100644 --- a/audio/src/gain_node.rs +++ b/audio/src/gain_node.rs @@ -55,7 +55,7 @@ impl AudioNodeEngine for GainNode { if self.update_parameters(info, frame.tick()) { gain = self.gain.value(); } - frame.mutate_with(|sample| *sample = *sample * gain); + frame.mutate_with(|sample, _| *sample = *sample * gain); } } inputs diff --git a/audio/src/lib.rs b/audio/src/lib.rs index 20d2d2c0..a6f98134 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -14,6 +14,7 @@ extern crate smallvec; pub mod macros; pub mod analyser_node; +pub mod biquad_filter_node; pub mod block; pub mod buffer_source_node; pub mod channel_node; diff --git a/audio/src/node.rs b/audio/src/node.rs index 1b9b3908..aa33d737 100644 --- a/audio/src/node.rs +++ b/audio/src/node.rs @@ -1,3 +1,4 @@ +use biquad_filter_node::{BiquadFilterNodeMessage, BiquadFilterNodeOptions}; use boxfnonce::SendBoxFnOnce; use block::{Block, Chunk, Tick}; use buffer_source_node::{AudioBufferSourceNodeMessage, AudioBufferSourceNodeOptions}; @@ -11,7 +12,7 @@ use std::sync::mpsc::Sender; /// Information required to construct an audio node pub enum AudioNodeInit { AnalyserNode(Box), - BiquadFilterNode, + BiquadFilterNode(BiquadFilterNodeOptions), AudioBuffer, AudioBufferSourceNode(AudioBufferSourceNodeOptions), ChannelMergerNode(ChannelNodeOptions), @@ -178,6 +179,7 @@ pub(crate) trait AudioNodeEngine: Send + AudioNodeCommon { pub enum AudioNodeMessage { AudioBufferSourceNode(AudioBufferSourceNodeMessage), AudioScheduledSourceNode(AudioScheduledSourceNodeMessage), + BiquadFilterNode(BiquadFilterNodeMessage), PannerNode(PannerNodeMessage), GetParamValue(ParamType, Sender), SetChannelCount(u8), diff --git a/audio/src/oscillator_node.rs b/audio/src/oscillator_node.rs index afd08bd2..9249f754 100644 --- a/audio/src/oscillator_node.rs +++ b/audio/src/oscillator_node.rs @@ -118,7 +118,7 @@ impl AudioNodeEngine for OscillatorNode { step = two_pi * self.frequency.value() as f64 / sample_rate; } let value = vol * f32::sin(NumCast::from(self.phase).unwrap()); - frame.mutate_with(|sample| *sample = value); + frame.mutate_with(|sample, _| *sample = value); self.phase += step; if self.phase >= two_pi { diff --git a/audio/src/param.rs b/audio/src/param.rs index 2e7af915..5806abba 100644 --- a/audio/src/param.rs +++ b/audio/src/param.rs @@ -8,6 +8,7 @@ pub enum ParamType { Frequency, Detune, Gain, + Q, PlaybackRate, Position(ParamDir), Forward(ParamDir), diff --git a/audio/src/render_thread.rs b/audio/src/render_thread.rs index 357de814..5e7a1b41 100644 --- a/audio/src/render_thread.rs +++ b/audio/src/render_thread.rs @@ -1,4 +1,5 @@ use analyser_node::AnalyserNode; +use biquad_filter_node::BiquadFilterNode; use block::{Chunk, Tick, FRAMES_PER_BLOCK}; use buffer_source_node::AudioBufferSourceNode; use channel_node::{ChannelMergerNode, ChannelSplitterNode}; @@ -134,6 +135,9 @@ impl AudioRenderThread { AudioNodeInit::AudioBufferSourceNode(options) => { Box::new(AudioBufferSourceNode::new(options, ch)) } + AudioNodeInit::BiquadFilterNode(options) => { + Box::new(BiquadFilterNode::new(options, ch, self.sample_rate)) + } AudioNodeInit::GainNode(options) => Box::new(GainNode::new(options, ch)), AudioNodeInit::PannerNode(options) => { needs_listener = true; diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 70675560..3c6ccc7e 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -19,6 +19,10 @@ ipc-channel = "0.11" name = "audio_decoder" path = "audio_decoder.rs" +[[bin]] +name = "biquad" +path = "biquad.rs" + [[bin]] name = "channels" path = "channels.rs" diff --git a/examples/biquad.rs b/examples/biquad.rs new file mode 100644 index 00000000..9691818c --- /dev/null +++ b/examples/biquad.rs @@ -0,0 +1,59 @@ +extern crate servo_media; + +use servo_media::audio::biquad_filter_node::{BiquadFilterNodeMessage, BiquadFilterNodeOptions, FilterType}; +use servo_media::audio::oscillator_node::OscillatorNodeOptions; +use servo_media::audio::node::{AudioNodeInit, AudioNodeMessage, AudioScheduledSourceNodeMessage}; +use servo_media::audio::param::{ParamType, RampKind, UserAutomationEvent}; +use servo_media::ServoMedia; +use std::sync::Arc; +use std::{thread, time}; + +fn run_example(servo_media: Arc) { + let context = servo_media.create_audio_context(Default::default()); + let dest = context.dest_node(); + let mut options = OscillatorNodeOptions::default(); + options.freq = 100.; + let osc1 = context.create_node(AudioNodeInit::OscillatorNode(options), Default::default()); + options.freq = 800.; + let osc2 = context.create_node(AudioNodeInit::OscillatorNode(options), Default::default()); + let mut options = BiquadFilterNodeOptions::default(); + options.frequency = 50.; + options.filter = FilterType::LowPass; + let biquad = context.create_node(AudioNodeInit::BiquadFilterNode(options), Default::default()); + context.connect_ports(osc1.output(0), biquad.input(0)); + context.connect_ports(osc2.output(0), biquad.input(0)); + context.connect_ports(biquad.output(0), dest.input(0)); + let _ = context.resume(); + context.message_node( + osc1, + AudioNodeMessage::AudioScheduledSourceNode(AudioScheduledSourceNodeMessage::Start(0.)), + ); + context.message_node( + osc2, + AudioNodeMessage::AudioScheduledSourceNode(AudioScheduledSourceNodeMessage::Start(0.)), + ); + context.message_node( + biquad, + AudioNodeMessage::SetParam( + ParamType::Frequency, + UserAutomationEvent::RampToValueAtTime(RampKind::Linear, 1000., 2.), + ), + ); + + thread::sleep(time::Duration::from_millis(2200)); + context.message_node( + biquad, + AudioNodeMessage::BiquadFilterNode(BiquadFilterNodeMessage::SetFilterType(FilterType::BandPass)), + ); + + thread::sleep(time::Duration::from_millis(1000)); + +} + +fn main() { + if let Ok(servo_media) = ServoMedia::get() { + run_example(servo_media); + } else { + unreachable!(); + } +}