Skip to main content

script/dom/audio/
audionode.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::Cell;
6
7use dom_struct::dom_struct;
8use js::context::JSContext;
9use log::warn;
10use script_bindings::codegen::InheritTypes::{
11    AudioNodeTypeId, AudioScheduledSourceNodeTypeId, EventTargetTypeId,
12};
13use servo_media::audio::graph::NodeId;
14use servo_media::audio::node::{
15    AudioNodeInit, AudioNodeMessage, ChannelCountMode as ServoMediaChannelCountMode, ChannelInfo,
16    ChannelInterpretation as ServoMediaChannelInterpretation,
17};
18
19use crate::conversions::Convert;
20use crate::dom::audio::audioparam::AudioParam;
21use crate::dom::audio::baseaudiocontext::BaseAudioContext;
22use crate::dom::bindings::codegen::Bindings::AudioNodeBinding::{
23    AudioNodeMethods, AudioNodeOptions, ChannelCountMode, ChannelInterpretation,
24};
25use crate::dom::bindings::error::{Error, ErrorResult, Fallible};
26use crate::dom::bindings::inheritance::Castable;
27use crate::dom::bindings::reflector::DomGlobal;
28use crate::dom::bindings::root::{Dom, DomRoot};
29use crate::dom::console::Console;
30use crate::dom::eventtarget::EventTarget;
31
32// 32 is the minimum required by the spec for createBuffer() and the deprecated
33// createScriptProcessor() and matches what is used by Blink and Gecko.
34// The limit protects against large memory allocations.
35pub(crate) const MAX_CHANNEL_COUNT: u32 = 32;
36
37#[dom_struct]
38pub(crate) struct AudioNode {
39    eventtarget: EventTarget,
40    #[no_trace]
41    node_id: Option<NodeId>,
42    context: Dom<BaseAudioContext>,
43    number_of_inputs: u32,
44    number_of_outputs: u32,
45    channel_count: Cell<u32>,
46    channel_count_mode: Cell<ChannelCountMode>,
47    channel_interpretation: Cell<ChannelInterpretation>,
48}
49
50impl AudioNode {
51    pub(crate) fn new_inherited(
52        cx: &mut JSContext,
53        node_type: AudioNodeInit,
54        context: &BaseAudioContext,
55        options: UnwrappedAudioNodeOptions,
56        number_of_inputs: u32,
57        number_of_outputs: u32,
58    ) -> Fallible<AudioNode> {
59        if options.count == 0 || options.count > MAX_CHANNEL_COUNT {
60            return Err(Error::NotSupported(None));
61        }
62        let ch = ChannelInfo {
63            count: options.count as u8,
64            mode: options.mode.convert(),
65            interpretation: options.interpretation.convert(),
66            context_channel_count: context.channel_count() as u8,
67        };
68        let node_id = context
69            .audio_context_impl()
70            .lock()
71            .unwrap()
72            .create_node(node_type, ch);
73
74        if node_id.is_none() {
75            // Follow Chromuim and Gecko, we just warn and create an inert AudioNode.
76            const MESSAGE: &str =
77                "Failed to create an AudioNode backend. The constructed AudioNode will be inert.";
78            warn!("{MESSAGE}");
79            Console::internal_warn(cx, &context.global(), MESSAGE.to_string());
80        }
81
82        Ok(AudioNode::new_inherited_for_id(
83            node_id,
84            context,
85            options,
86            number_of_inputs,
87            number_of_outputs,
88        ))
89    }
90
91    pub(crate) fn new_inherited_for_id(
92        node_id: Option<NodeId>,
93        context: &BaseAudioContext,
94        options: UnwrappedAudioNodeOptions,
95        number_of_inputs: u32,
96        number_of_outputs: u32,
97    ) -> AudioNode {
98        AudioNode {
99            eventtarget: EventTarget::new_inherited(),
100            node_id,
101            context: Dom::from_ref(context),
102            number_of_inputs,
103            number_of_outputs,
104            channel_count: Cell::new(options.count),
105            channel_count_mode: Cell::new(options.mode),
106            channel_interpretation: Cell::new(options.interpretation),
107        }
108    }
109
110    pub(crate) fn message(&self, message: AudioNodeMessage) {
111        if let Some(node_id) = self.node_id {
112            self.context
113                .audio_context_impl()
114                .lock()
115                .unwrap()
116                .message_node(node_id, message);
117        }
118    }
119
120    pub(crate) fn node_id(&self) -> Option<NodeId> {
121        self.node_id
122    }
123}
124
125impl AudioNodeMethods<crate::DomTypeHolder> for AudioNode {
126    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-connect>
127    fn Connect(
128        &self,
129        destination: &AudioNode,
130        output: u32,
131        input: u32,
132    ) -> Fallible<DomRoot<AudioNode>> {
133        if self.context != destination.context {
134            return Err(Error::InvalidAccess(None));
135        }
136
137        if output >= self.NumberOfOutputs() || input >= destination.NumberOfInputs() {
138            return Err(Error::IndexSize(None));
139        }
140
141        // servo-media takes care of ignoring duplicated connections.
142
143        let Some(source_id) = self.node_id() else {
144            return Ok(DomRoot::from_ref(destination));
145        };
146        let Some(dest_id) = destination.node_id() else {
147            return Ok(DomRoot::from_ref(destination));
148        };
149
150        self.context
151            .audio_context_impl()
152            .lock()
153            .unwrap()
154            .connect_ports(source_id.output(output), dest_id.input(input));
155
156        Ok(DomRoot::from_ref(destination))
157    }
158
159    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-connect-destinationparam-output>
160    fn Connect_(&self, dest: &AudioParam, output: u32) -> Fallible<()> {
161        if self.context != dest.context() {
162            return Err(Error::InvalidAccess(None));
163        }
164
165        if output >= self.NumberOfOutputs() {
166            return Err(Error::IndexSize(None));
167        }
168
169        // servo-media takes care of ignoring duplicated connections.
170
171        let Some(source_id) = self.node_id() else {
172            return Ok(());
173        };
174        let Some(param_node) = dest.node_id() else {
175            return Ok(());
176        };
177
178        self.context
179            .audio_context_impl()
180            .lock()
181            .unwrap()
182            .connect_ports(
183                source_id.output(output),
184                param_node.param(dest.param_type()),
185            );
186
187        Ok(())
188    }
189
190    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-disconnect>
191    fn Disconnect(&self) -> ErrorResult {
192        if let Some(node_id) = self.node_id() {
193            self.context
194                .audio_context_impl()
195                .lock()
196                .unwrap()
197                .disconnect_all_from(node_id);
198        }
199        Ok(())
200    }
201
202    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-disconnect-output>
203    fn Disconnect_(&self, out: u32) -> ErrorResult {
204        if let Some(node_id) = self.node_id() {
205            self.context
206                .audio_context_impl()
207                .lock()
208                .unwrap()
209                .disconnect_output(node_id.output(out));
210        }
211        Ok(())
212    }
213
214    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-disconnect-destinationnode>
215    fn Disconnect__(&self, to: &AudioNode) -> ErrorResult {
216        if let (Some(from_node), Some(to_node)) = (self.node_id(), to.node_id()) {
217            self.context
218                .audio_context_impl()
219                .lock()
220                .unwrap()
221                .disconnect_between(from_node, to_node);
222        }
223        Ok(())
224    }
225
226    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-disconnect-destinationnode-output>
227    fn Disconnect___(&self, to: &AudioNode, out: u32) -> ErrorResult {
228        if let (Some(from_node), Some(to_node)) = (self.node_id(), to.node_id()) {
229            self.context
230                .audio_context_impl()
231                .lock()
232                .unwrap()
233                .disconnect_output_between(from_node.output(out), to_node);
234        }
235        Ok(())
236    }
237
238    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-disconnect-destinationnode-output-input>
239    fn Disconnect____(&self, to: &AudioNode, out: u32, inp: u32) -> ErrorResult {
240        if let (Some(from_node), Some(to_node)) = (self.node_id(), to.node_id()) {
241            self.context
242                .audio_context_impl()
243                .lock()
244                .unwrap()
245                .disconnect_output_between_to(from_node.output(out), to_node.input(inp));
246        }
247        Ok(())
248    }
249
250    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-disconnect>
251    fn Disconnect_____(&self, param: &AudioParam) -> ErrorResult {
252        if let (Some(from_node), Some(param_node)) = (self.node_id(), param.node_id()) {
253            self.context
254                .audio_context_impl()
255                .lock()
256                .unwrap()
257                .disconnect_to(from_node, param_node.param(param.param_type()));
258        }
259        Ok(())
260    }
261
262    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-disconnect>
263    fn Disconnect______(&self, param: &AudioParam, out: u32) -> ErrorResult {
264        if let (Some(from_node), Some(param_node)) = (self.node_id(), param.node_id()) {
265            self.context
266                .audio_context_impl()
267                .lock()
268                .unwrap()
269                .disconnect_output_between_to(
270                    from_node.output(out),
271                    param_node.param(param.param_type()),
272                );
273        }
274        Ok(())
275    }
276
277    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-context>
278    fn Context(&self) -> DomRoot<BaseAudioContext> {
279        DomRoot::from_ref(&self.context)
280    }
281
282    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-numberofinputs>
283    fn NumberOfInputs(&self) -> u32 {
284        self.number_of_inputs
285    }
286
287    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-numberofoutputs>
288    fn NumberOfOutputs(&self) -> u32 {
289        self.number_of_outputs
290    }
291
292    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-channelcount>
293    fn ChannelCount(&self) -> u32 {
294        self.channel_count.get()
295    }
296
297    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-channelcount>
298    fn SetChannelCount(&self, value: u32) -> ErrorResult {
299        match self.upcast::<EventTarget>().type_id() {
300            EventTargetTypeId::AudioNode(AudioNodeTypeId::AudioDestinationNode) => {
301                if self.context.is_offline() {
302                    return Err(Error::InvalidState(None));
303                } else if !(1..=MAX_CHANNEL_COUNT).contains(&value) {
304                    return Err(Error::IndexSize(None));
305                }
306            },
307            EventTargetTypeId::AudioNode(AudioNodeTypeId::PannerNode) if value > 2 => {
308                return Err(Error::NotSupported(None));
309            },
310            EventTargetTypeId::AudioNode(AudioNodeTypeId::AudioScheduledSourceNode(
311                AudioScheduledSourceNodeTypeId::StereoPannerNode,
312            )) if value > 2 => {
313                return Err(Error::NotSupported(None));
314            },
315            EventTargetTypeId::AudioNode(AudioNodeTypeId::ChannelMergerNode) => {
316                return Err(Error::InvalidState(None));
317            },
318            EventTargetTypeId::AudioNode(AudioNodeTypeId::ChannelSplitterNode) => {
319                return Err(Error::InvalidState(None));
320            },
321            // XXX We do not support any of the other AudioNodes with
322            // constraints yet. Add more cases here as we add support
323            // for new AudioNodes.
324            _ => (),
325        };
326
327        if value == 0 || value > MAX_CHANNEL_COUNT {
328            return Err(Error::NotSupported(None));
329        }
330
331        self.channel_count.set(value);
332        self.message(AudioNodeMessage::SetChannelCount(value as u8));
333        Ok(())
334    }
335
336    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-channelcountmode>
337    fn ChannelCountMode(&self) -> ChannelCountMode {
338        self.channel_count_mode.get()
339    }
340
341    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-channelcountmode>
342    fn SetChannelCountMode(&self, value: ChannelCountMode) -> ErrorResult {
343        // Channel count mode has no effect for nodes with no inputs.
344        if self.number_of_inputs == 0 {
345            return Ok(());
346        }
347
348        match self.upcast::<EventTarget>().type_id() {
349            EventTargetTypeId::AudioNode(AudioNodeTypeId::AudioDestinationNode)
350                if self.context.is_offline() =>
351            {
352                return Err(Error::InvalidState(None));
353            },
354            EventTargetTypeId::AudioNode(AudioNodeTypeId::PannerNode)
355                if value == ChannelCountMode::Max =>
356            {
357                return Err(Error::NotSupported(None));
358            },
359            EventTargetTypeId::AudioNode(AudioNodeTypeId::AudioScheduledSourceNode(
360                AudioScheduledSourceNodeTypeId::StereoPannerNode,
361            )) if value == ChannelCountMode::Max => {
362                return Err(Error::NotSupported(None));
363            },
364            EventTargetTypeId::AudioNode(AudioNodeTypeId::ChannelMergerNode) => {
365                return Err(Error::InvalidState(None));
366            },
367            EventTargetTypeId::AudioNode(AudioNodeTypeId::ChannelSplitterNode) => {
368                return Err(Error::InvalidState(None));
369            },
370            // XXX We do not support any of the other AudioNodes with
371            // constraints yet. Add more cases here as we add support
372            // for new AudioNodes.
373            _ => (),
374        };
375
376        self.channel_count_mode.set(value);
377        self.message(AudioNodeMessage::SetChannelMode(value.convert()));
378        Ok(())
379    }
380
381    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-channelinterpretation>
382    fn ChannelInterpretation(&self) -> ChannelInterpretation {
383        self.channel_interpretation.get()
384    }
385
386    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-channelinterpretation>
387    fn SetChannelInterpretation(&self, value: ChannelInterpretation) -> ErrorResult {
388        // Channel interpretation mode has no effect for nodes with no inputs.
389        if self.number_of_inputs == 0 {
390            return Ok(());
391        }
392
393        if let EventTargetTypeId::AudioNode(AudioNodeTypeId::ChannelSplitterNode) =
394            self.upcast::<EventTarget>().type_id()
395        {
396            return Err(Error::InvalidState(None));
397        };
398
399        self.channel_interpretation.set(value);
400        self.message(AudioNodeMessage::SetChannelInterpretation(value.convert()));
401        Ok(())
402    }
403}
404
405impl Convert<ServoMediaChannelCountMode> for ChannelCountMode {
406    fn convert(self) -> ServoMediaChannelCountMode {
407        match self {
408            ChannelCountMode::Max => ServoMediaChannelCountMode::Max,
409            ChannelCountMode::Clamped_max => ServoMediaChannelCountMode::ClampedMax,
410            ChannelCountMode::Explicit => ServoMediaChannelCountMode::Explicit,
411        }
412    }
413}
414
415impl Convert<ServoMediaChannelInterpretation> for ChannelInterpretation {
416    fn convert(self) -> ServoMediaChannelInterpretation {
417        match self {
418            ChannelInterpretation::Discrete => ServoMediaChannelInterpretation::Discrete,
419            ChannelInterpretation::Speakers => ServoMediaChannelInterpretation::Speakers,
420        }
421    }
422}
423
424pub(crate) trait AudioNodeOptionsHelper {
425    fn unwrap_or(
426        &self,
427        count: u32,
428        mode: ChannelCountMode,
429        interpretation: ChannelInterpretation,
430    ) -> UnwrappedAudioNodeOptions;
431}
432
433impl AudioNodeOptionsHelper for AudioNodeOptions {
434    fn unwrap_or(
435        &self,
436        count: u32,
437        mode: ChannelCountMode,
438        interpretation: ChannelInterpretation,
439    ) -> UnwrappedAudioNodeOptions {
440        UnwrappedAudioNodeOptions {
441            count: self.channelCount.unwrap_or(count),
442            mode: self.channelCountMode.unwrap_or(mode),
443            interpretation: self.channelInterpretation.unwrap_or(interpretation),
444        }
445    }
446}
447
448/// Each node has a set of defaults, so this lets us work with them
449/// easily without having to deal with the Options
450pub(crate) struct UnwrappedAudioNodeOptions {
451    pub(crate) count: u32,
452    pub(crate) mode: ChannelCountMode,
453    pub(crate) interpretation: ChannelInterpretation,
454}
455
456impl Default for UnwrappedAudioNodeOptions {
457    fn default() -> Self {
458        UnwrappedAudioNodeOptions {
459            count: 2,
460            mode: ChannelCountMode::Max,
461            interpretation: ChannelInterpretation::Speakers,
462        }
463    }
464}