webaudio: Implement IIRFilterNode (#33001)

* Basic IIRFIlterNode bindings

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Add constructor to BaseAudioContext

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Update IDL and use statements

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Update non-crashing test expectations

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Tidy

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Add missing spec link

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Optimize error checks

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Pass context channel count to servo-media

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Update test expectations

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Update legacy expectations

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Add IIRFilterNode in interfaces.html

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Update MANIFEST

Signed-off-by: Daniel Adams <msub2official@gmail.com>

---------

Signed-off-by: Daniel Adams <msub2official@gmail.com>
This commit is contained in:
Daniel Adams 2024-08-11 14:27:54 -10:00 committed by GitHub
parent 1af3ad8a74
commit 5520a9eb50
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 206 additions and 175 deletions

24
Cargo.lock generated
View file

@ -5926,7 +5926,7 @@ dependencies = [
[[package]]
name = "servo-media"
version = "0.1.0"
source = "git+https://github.com/servo/media#45756bef67037ade0f4f0125d579fdc3f3d457c8"
source = "git+https://github.com/servo/media#adfb5467abadcc42a0c047fcfd043e5be10818e6"
dependencies = [
"once_cell",
"servo-media-audio",
@ -5939,7 +5939,7 @@ dependencies = [
[[package]]
name = "servo-media-audio"
version = "0.2.0"
source = "git+https://github.com/servo/media#45756bef67037ade0f4f0125d579fdc3f3d457c8"
source = "git+https://github.com/servo/media#adfb5467abadcc42a0c047fcfd043e5be10818e6"
dependencies = [
"byte-slice-cast",
"euclid",
@ -5960,7 +5960,7 @@ dependencies = [
[[package]]
name = "servo-media-derive"
version = "0.1.0"
source = "git+https://github.com/servo/media#45756bef67037ade0f4f0125d579fdc3f3d457c8"
source = "git+https://github.com/servo/media#adfb5467abadcc42a0c047fcfd043e5be10818e6"
dependencies = [
"proc-macro2",
"quote",
@ -5970,7 +5970,7 @@ dependencies = [
[[package]]
name = "servo-media-dummy"
version = "0.1.0"
source = "git+https://github.com/servo/media#45756bef67037ade0f4f0125d579fdc3f3d457c8"
source = "git+https://github.com/servo/media#adfb5467abadcc42a0c047fcfd043e5be10818e6"
dependencies = [
"ipc-channel",
"servo-media",
@ -5984,7 +5984,7 @@ dependencies = [
[[package]]
name = "servo-media-gstreamer"
version = "0.1.0"
source = "git+https://github.com/servo/media#45756bef67037ade0f4f0125d579fdc3f3d457c8"
source = "git+https://github.com/servo/media#adfb5467abadcc42a0c047fcfd043e5be10818e6"
dependencies = [
"byte-slice-cast",
"glib",
@ -6018,7 +6018,7 @@ dependencies = [
[[package]]
name = "servo-media-gstreamer-render"
version = "0.1.0"
source = "git+https://github.com/servo/media#45756bef67037ade0f4f0125d579fdc3f3d457c8"
source = "git+https://github.com/servo/media#adfb5467abadcc42a0c047fcfd043e5be10818e6"
dependencies = [
"gstreamer",
"gstreamer-video",
@ -6028,7 +6028,7 @@ dependencies = [
[[package]]
name = "servo-media-gstreamer-render-android"
version = "0.1.0"
source = "git+https://github.com/servo/media#45756bef67037ade0f4f0125d579fdc3f3d457c8"
source = "git+https://github.com/servo/media#adfb5467abadcc42a0c047fcfd043e5be10818e6"
dependencies = [
"glib",
"gstreamer",
@ -6042,7 +6042,7 @@ dependencies = [
[[package]]
name = "servo-media-gstreamer-render-unix"
version = "0.1.0"
source = "git+https://github.com/servo/media#45756bef67037ade0f4f0125d579fdc3f3d457c8"
source = "git+https://github.com/servo/media#adfb5467abadcc42a0c047fcfd043e5be10818e6"
dependencies = [
"glib",
"gstreamer",
@ -6057,7 +6057,7 @@ dependencies = [
[[package]]
name = "servo-media-player"
version = "0.1.0"
source = "git+https://github.com/servo/media#45756bef67037ade0f4f0125d579fdc3f3d457c8"
source = "git+https://github.com/servo/media#adfb5467abadcc42a0c047fcfd043e5be10818e6"
dependencies = [
"ipc-channel",
"serde",
@ -6069,7 +6069,7 @@ dependencies = [
[[package]]
name = "servo-media-streams"
version = "0.1.0"
source = "git+https://github.com/servo/media#45756bef67037ade0f4f0125d579fdc3f3d457c8"
source = "git+https://github.com/servo/media#adfb5467abadcc42a0c047fcfd043e5be10818e6"
dependencies = [
"lazy_static",
"uuid",
@ -6078,12 +6078,12 @@ dependencies = [
[[package]]
name = "servo-media-traits"
version = "0.1.0"
source = "git+https://github.com/servo/media#45756bef67037ade0f4f0125d579fdc3f3d457c8"
source = "git+https://github.com/servo/media#adfb5467abadcc42a0c047fcfd043e5be10818e6"
[[package]]
name = "servo-media-webrtc"
version = "0.1.0"
source = "git+https://github.com/servo/media#45756bef67037ade0f4f0125d579fdc3f3d457c8"
source = "git+https://github.com/servo/media#adfb5467abadcc42a0c047fcfd043e5be10818e6"
dependencies = [
"lazy_static",
"log",

View file

@ -58,6 +58,7 @@ impl AudioNode {
count: options.count as u8,
mode: options.mode.into(),
interpretation: options.interpretation.into(),
context_channel_count: context.channel_count() as u8,
};
let node_id = context
.audio_context_impl()

View file

@ -42,6 +42,7 @@ use crate::dom::bindings::codegen::Bindings::ChannelMergerNodeBinding::ChannelMe
use crate::dom::bindings::codegen::Bindings::ChannelSplitterNodeBinding::ChannelSplitterOptions;
use crate::dom::bindings::codegen::Bindings::ConstantSourceNodeBinding::ConstantSourceOptions;
use crate::dom::bindings::codegen::Bindings::GainNodeBinding::GainOptions;
use crate::dom::bindings::codegen::Bindings::IIRFilterNodeBinding::IIRFilterOptions;
use crate::dom::bindings::codegen::Bindings::OscillatorNodeBinding::OscillatorOptions;
use crate::dom::bindings::codegen::Bindings::PannerNodeBinding::PannerOptions;
use crate::dom::bindings::codegen::Bindings::StereoPannerNodeBinding::StereoPannerOptions;
@ -58,6 +59,7 @@ use crate::dom::constantsourcenode::ConstantSourceNode;
use crate::dom::domexception::{DOMErrorName, DOMException};
use crate::dom::eventtarget::EventTarget;
use crate::dom::gainnode::GainNode;
use crate::dom::iirfilternode::IIRFilterNode;
use crate::dom::oscillatornode::OscillatorNode;
use crate::dom::pannernode::PannerNode;
use crate::dom::promise::Promise;
@ -264,6 +266,10 @@ impl BaseAudioContext {
},
}
}
pub fn channel_count(&self) -> u32 {
self.channel_count
}
}
impl BaseAudioContextMethods for BaseAudioContext {
@ -550,6 +556,20 @@ impl BaseAudioContextMethods for BaseAudioContext {
// Step 4.
promise
}
/// <https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-createiirfilter>
fn CreateIIRFilter(
&self,
feedforward: Vec<Finite<f64>>,
feedback: Vec<Finite<f64>>,
) -> Fallible<DomRoot<IIRFilterNode>> {
let opts = IIRFilterOptions {
parent: AudioNodeOptions::empty(),
feedback,
feedforward,
};
IIRFilterNode::new(self.global().as_window(), self, &opts)
}
}
impl From<BaseAudioContextOptions> for AudioContextOptions {

View file

@ -0,0 +1,145 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::sync::Arc;
use dom_struct::dom_struct;
use itertools::Itertools;
use js::gc::CustomAutoRooterGuard;
use js::rust::HandleObject;
use js::typedarray::Float32Array;
use servo_media::audio::iir_filter_node::{IIRFilterNode as IIRFilter, IIRFilterNodeOptions};
use servo_media::audio::node::AudioNodeInit;
use crate::dom::audionode::AudioNode;
use crate::dom::baseaudiocontext::BaseAudioContext;
use crate::dom::bindings::codegen::Bindings::AudioNodeBinding::{
ChannelCountMode, ChannelInterpretation,
};
use crate::dom::bindings::codegen::Bindings::IIRFilterNodeBinding::{
IIRFilterNodeMethods, IIRFilterOptions,
};
use crate::dom::bindings::error::{Error, Fallible};
use crate::dom::bindings::num::Finite;
use crate::dom::bindings::reflector::reflect_dom_object_with_proto;
use crate::dom::bindings::root::DomRoot;
use crate::dom::window::Window;
#[dom_struct]
pub struct IIRFilterNode {
node: AudioNode,
feedforward: Vec<Finite<f64>>,
feedback: Vec<Finite<f64>>,
}
impl IIRFilterNode {
#[allow(crown::unrooted_must_root)]
pub fn new_inherited(
window: &Window,
context: &BaseAudioContext,
options: &IIRFilterOptions,
) -> Fallible<IIRFilterNode> {
if !(1..=20).contains(&options.feedforward.len()) ||
!(1..=20).contains(&options.feedback.len())
{
return Err(Error::NotSupported);
}
if options.feedforward.iter().all(|v| **v == 0.0) || *options.feedback[0] == 0.0 {
return Err(Error::InvalidState);
}
let node_options =
options
.parent
.unwrap_or(2, ChannelCountMode::Max, ChannelInterpretation::Speakers);
let init_options = options.into();
let node = AudioNode::new_inherited(
AudioNodeInit::IIRFilterNode(init_options),
context,
node_options,
1, // inputs
1, // outputs
)?;
Ok(IIRFilterNode {
node,
feedforward: (*options.feedforward).to_vec(),
feedback: (*options.feedback).to_vec(),
})
}
pub fn new(
window: &Window,
context: &BaseAudioContext,
options: &IIRFilterOptions,
) -> Fallible<DomRoot<IIRFilterNode>> {
Self::new_with_proto(window, None, context, options)
}
#[allow(crown::unrooted_must_root)]
fn new_with_proto(
window: &Window,
proto: Option<HandleObject>,
context: &BaseAudioContext,
options: &IIRFilterOptions,
) -> Fallible<DomRoot<IIRFilterNode>> {
let node = IIRFilterNode::new_inherited(window, context, options)?;
Ok(reflect_dom_object_with_proto(Box::new(node), window, proto))
}
#[allow(non_snake_case)]
pub fn Constructor(
window: &Window,
proto: Option<HandleObject>,
context: &BaseAudioContext,
options: &IIRFilterOptions,
) -> Fallible<DomRoot<IIRFilterNode>> {
IIRFilterNode::new_with_proto(window, proto, context, options)
}
}
impl IIRFilterNodeMethods for IIRFilterNode {
#[allow(unsafe_code)]
/// <https://webaudio.github.io/web-audio-api/#dom-iirfilternode-getfrequencyresponse>
fn GetFrequencyResponse(
&self,
frequency_hz: CustomAutoRooterGuard<Float32Array>,
mut mag_response: CustomAutoRooterGuard<Float32Array>,
mut phase_response: CustomAutoRooterGuard<Float32Array>,
) -> Result<(), Error> {
let len = frequency_hz.len();
if len != mag_response.len() || len != phase_response.len() {
return Err(Error::InvalidAccess);
}
let feedforward: Vec<f64> = (self.feedforward.iter().map(|v| **v).collect_vec()).to_vec();
let feedback: Vec<f64> = (self.feedback.iter().map(|v| **v).collect_vec()).to_vec();
let frequency_hz_vec = frequency_hz.to_vec();
let mut mag_response_vec = mag_response.to_vec();
let mut phase_response_vec = phase_response.to_vec();
IIRFilter::get_frequency_response(
&feedforward,
&feedback,
&frequency_hz_vec,
&mut mag_response_vec,
&mut phase_response_vec,
);
unsafe {
mag_response.update(&mag_response_vec);
phase_response.update(&phase_response_vec);
}
Ok(())
}
}
impl<'a> From<&'a IIRFilterOptions> for IIRFilterNodeOptions {
fn from(options: &'a IIRFilterOptions) -> Self {
let feedforward: Vec<f64> =
(*options.feedforward.iter().map(|v| **v).collect_vec()).to_vec();
let feedback: Vec<f64> = (*options.feedback.iter().map(|v| **v).collect_vec()).to_vec();
Self {
feedforward: Arc::new(feedforward),
feedback: Arc::new(feedback),
}
}
}

View file

@ -440,6 +440,7 @@ pub mod htmlulistelement;
pub mod htmlunknownelement;
pub mod htmlvideoelement;
pub mod identityhub;
pub mod iirfilternode;
pub mod imagebitmap;
pub mod imagedata;
pub mod inputevent;

View file

@ -39,8 +39,8 @@ interface BaseAudioContext : EventTarget {
[Throws] GainNode createGain();
// DelayNode createDelay(optional double maxDelayTime = 1);
[Throws] BiquadFilterNode createBiquadFilter();
// IIRFilterNode createIIRFilter(sequence<double> feedforward,
// sequence<double> feedback);
[Throws] IIRFilterNode createIIRFilter(sequence<double> feedforward,
sequence<double> feedback);
// WaveShaperNode createWaveShaper();
[Throws] PannerNode createPanner();
[Throws] StereoPannerNode createStereoPanner();

View file

@ -0,0 +1,22 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
/*
* The origin of this IDL file is
* https://webaudio.github.io/web-audio-api/#IIRFilterNode
*/
[Exposed=Window]
interface IIRFilterNode : AudioNode {
[Throws] constructor (BaseAudioContext context, IIRFilterOptions options);
[Throws] undefined getFrequencyResponse (
Float32Array frequencyHz,
Float32Array magResponse,
Float32Array phaseResponse
);
};
dictionary IIRFilterOptions : AudioNodeOptions {
required sequence<double> feedforward;
required sequence<double> feedback;
};

View file

@ -5,9 +5,6 @@
[X node = new IIRFilterNode(c, {"feedforward":[1,0.5\]}) threw "ReferenceError" instead of EcmaScript error TypeError.]
expected: FAIL
[Executing "functionality"]
expected: FAIL
[X node = new IIRFilterNode(, {"feedback":[1,0.5\]}) threw "ReferenceError" instead of EcmaScript error TypeError.]
expected: FAIL
@ -17,15 +14,9 @@
[< [default constructor\] 1 out of 1 assertions were failed.]
expected: FAIL
[Executing "test AudioNodeOptions"]
expected: FAIL
[< [test AudioNodeOptions\] 1 out of 1 assertions were failed.]
expected: FAIL
[Executing "default constructor"]
expected: FAIL
[< [constructor options\] 2 out of 2 assertions were failed.]
expected: FAIL

View file

@ -34,10 +34,3 @@
[X createIIRFilter([\], [1\]) threw "TypeError" instead of NotSupportedError.]
expected: FAIL
[Executing "parameters"]
expected: FAIL
[Executing "exceptions-getFrequencyData"]
expected: FAIL

View file

@ -1,10 +1,3 @@
[iirfilter-getFrequencyResponse.html]
[Executing "compare IIR and biquad"]
expected: FAIL
[Executing "1-pole IIR"]
expected: FAIL
[Executing "getFrequencyResponse"]
expected: FAIL

View file

@ -5,48 +5,9 @@
[X createIIRFilter with normalized coefficients incorrectly threw TypeError: "context.createIIRFilter is not a function".]
expected: FAIL
[Executing "coefficient-normalization"]
expected: FAIL
[Executing "7: peaking"]
expected: FAIL
[Executing "0: lowpass"]
expected: FAIL
[Executing "one-zero"]
expected: FAIL
[Executing "6: highshelf"]
expected: FAIL
[< [coefficient-normalization\] 2 out of 2 assertions were failed.]
expected: FAIL
[Executing "5: lowshelf"]
expected: FAIL
[Executing "one-pole"]
expected: FAIL
[Executing "1: highpass"]
expected: FAIL
[Executing "4: allpass"]
expected: FAIL
[Executing "4th-order-iir"]
expected: FAIL
[Executing "multi-channel"]
expected: FAIL
[Executing "2: bandpass"]
expected: FAIL
[Executing "3: notch"]
expected: FAIL
[# AUDIT TASK RUNNER FINISHED: 1 out of 13 tasks were failed.]
expected: FAIL

View file

@ -1,22 +1,3 @@
[test-iirfilternode.html]
[the first feedback coefficient must be non-zero]
expected: FAIL
[feedback coefficients can not be empty]
expected: FAIL
[IIRFilterNode getFrequencyResponse handles invalid frequencies properly]
expected: FAIL
[more than 20 feedback coefficients can not be used]
expected: FAIL
[feedforward coefficients can not be empty]
expected: FAIL
[at least one feedforward coefficient must be non-zero]
expected: FAIL
[more than 20 feedforward coefficients can not be used]
expected: FAIL

View file

@ -1,13 +1,4 @@
[ctor-iirfilter.html]
[Executing "default constructor"]
expected: FAIL
[Executing "test AudioNodeOptions"]
expected: FAIL
[Executing "functionality"]
expected: FAIL
[X node0 = new IIRFilterNode(context, {"feedforward":[1\],"feedback":[1,-0.9\]}) incorrectly threw TypeError: "window[name\] is not a constructor".]
expected: FAIL

View file

@ -1,10 +1,4 @@
[iirfilter-basic.html]
[Executing "parameters"]
expected: FAIL
[Executing "exceptions-getFrequencyData"]
expected: FAIL
[X context.createIIRFilter does not exist. Got undefined.]
expected: FAIL

View file

@ -1,9 +1,3 @@
[iirfilter-getFrequencyResponse.html]
[Executing "1-pole IIR"]
expected: FAIL
[Executing "compare IIR and biquad"]
expected: FAIL
[Executing "getFrequencyResponse"]
expected: FAIL
expected: FAIL

View file

@ -1,43 +1,4 @@
[iirfilter.html]
[Executing "coefficient-normalization"]
expected: FAIL
[Executing "one-zero"]
expected: FAIL
[Executing "one-pole"]
expected: FAIL
[Executing "0: lowpass"]
expected: FAIL
[Executing "1: highpass"]
expected: FAIL
[Executing "2: bandpass"]
expected: FAIL
[Executing "3: notch"]
expected: FAIL
[Executing "4: allpass"]
expected: FAIL
[Executing "5: lowshelf"]
expected: FAIL
[Executing "6: highshelf"]
expected: FAIL
[Executing "7: peaking"]
expected: FAIL
[Executing "multi-channel"]
expected: FAIL
[Executing "4th-order-iir"]
expected: FAIL
[X createIIRFilter with normalized coefficients incorrectly threw TypeError: "context.createIIRFilter is not a function".]
expected: FAIL

View file

@ -1,21 +1,3 @@
[test-iirfilternode.html]
[feedforward coefficients can not be empty]
expected: FAIL
[feedback coefficients can not be empty]
expected: FAIL
[more than 20 feedforward coefficients can not be used]
expected: FAIL
[more than 20 feedback coefficients can not be used]
expected: FAIL
[at least one feedforward coefficient must be non-zero]
expected: FAIL
[the first feedback coefficient must be non-zero]
expected: FAIL
[IIRFilterNode getFrequencyResponse handles invalid frequencies properly]
expected: FAIL

View file

@ -13459,7 +13459,7 @@
]
],
"interfaces.html": [
"2ab9214e53c431e4a599254d4cb498fd75eef4ed",
"ee31ffa62be1623831a6379ace46a4784df7608c",
[
null,
{}

View file

@ -165,6 +165,7 @@ test_interfaces([
"HTMLUListElement",
"HTMLUnknownElement",
"HTMLVideoElement",
"IIRFilterNode",
"ImageData",
"Image",
"InputEvent",