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::audio_node::{
14    AudioNodeInit, AudioNodeMessage, ChannelCountMode as ServoMediaChannelCountMode, ChannelInfo,
15    ChannelInterpretation as ServoMediaChannelInterpretation,
16};
17use servo_media::audio::graph::NodeId;
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        node_id: Option<NodeId>,
53        context: &BaseAudioContext,
54        options: UnwrappedAudioNodeOptions,
55        number_of_inputs: u32,
56        number_of_outputs: u32,
57    ) -> AudioNode {
58        AudioNode {
59            eventtarget: EventTarget::new_inherited(),
60            node_id,
61            context: Dom::from_ref(context),
62            number_of_inputs,
63            number_of_outputs,
64            channel_count: Cell::new(options.count),
65            channel_count_mode: Cell::new(options.mode),
66            channel_interpretation: Cell::new(options.interpretation),
67        }
68    }
69
70    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-channelcount>
71    /// > If this value is set to zero or to a value greater than the implementation’s
72    /// > maximum number of channels the implementation MUST throw a NotSupportedError exception.
73    pub(crate) fn channel_count_check(count: u32) -> bool {
74        count == 0 || count > MAX_CHANNEL_COUNT
75    }
76
77    pub(crate) fn generate_audio_node_id(
78        cx: &mut JSContext,
79        node_options: &UnwrappedAudioNodeOptions,
80        audio_node_init: AudioNodeInit,
81        context: &BaseAudioContext,
82    ) -> Option<NodeId> {
83        let ch = ChannelInfo {
84            count: node_options.count as u8,
85            mode: node_options.mode.convert(),
86            interpretation: node_options.interpretation.convert(),
87            context_channel_count: context.channel_count() as u8,
88        };
89        let node_id = context
90            .audio_context_impl()
91            .lock()
92            .unwrap()
93            .create_node(audio_node_init, ch);
94        if node_id.is_none() {
95            // Follow Chromuim and Gecko, we just warn and create an inert AudioNode.
96            const MESSAGE: &str =
97                "Failed to create an AudioNode backend. The constructed AudioNode will be inert.";
98            warn!("{MESSAGE}");
99            Console::internal_warn(cx, &context.global(), MESSAGE.to_string());
100        }
101        node_id
102    }
103
104    pub(crate) fn message(&self, message: AudioNodeMessage) {
105        if let Some(node_id) = self.node_id {
106            self.context
107                .audio_context_impl()
108                .lock()
109                .unwrap()
110                .message_node(node_id, message);
111        }
112    }
113
114    pub(crate) fn node_id(&self) -> Option<NodeId> {
115        self.node_id
116    }
117}
118
119impl AudioNodeMethods<crate::DomTypeHolder> for AudioNode {
120    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-connect>
121    fn Connect(
122        &self,
123        destination: &AudioNode,
124        output: u32,
125        input: u32,
126    ) -> Fallible<DomRoot<AudioNode>> {
127        if self.context != destination.context {
128            return Err(Error::InvalidAccess(Some(
129                "Destination node belongs to a different AudioContext".into(),
130            )));
131        }
132
133        if output >= self.NumberOfOutputs() || input >= destination.NumberOfInputs() {
134            return Err(Error::IndexSize(Some(
135                "Output or input index is out of range".into(),
136            )));
137        }
138
139        // servo-media takes care of ignoring duplicated connections.
140
141        let Some(source_id) = self.node_id() else {
142            return Ok(DomRoot::from_ref(destination));
143        };
144        let Some(dest_id) = destination.node_id() else {
145            return Ok(DomRoot::from_ref(destination));
146        };
147
148        self.context
149            .audio_context_impl()
150            .lock()
151            .unwrap()
152            .connect_ports(source_id.output(output), dest_id.input(input));
153
154        Ok(DomRoot::from_ref(destination))
155    }
156
157    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-connect-destinationparam-output>
158    fn Connect_(&self, dest: &AudioParam, output: u32) -> Fallible<()> {
159        if self.context != dest.context() {
160            return Err(Error::InvalidAccess(Some(
161                "Destination AudioParam belongs to a different AudioContext".into(),
162            )));
163        }
164
165        if output >= self.NumberOfOutputs() {
166            return Err(Error::IndexSize(Some(
167                "Output index is out of range".into(),
168            )));
169        }
170
171        // servo-media takes care of ignoring duplicated connections.
172
173        let Some(source_id) = self.node_id() else {
174            return Ok(());
175        };
176        let Some(param_node) = dest.node_id() else {
177            return Ok(());
178        };
179
180        self.context
181            .audio_context_impl()
182            .lock()
183            .unwrap()
184            .connect_ports(
185                source_id.output(output),
186                param_node.param(dest.param_type()),
187            );
188
189        Ok(())
190    }
191
192    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-disconnect>
193    fn Disconnect(&self) -> ErrorResult {
194        if let Some(node_id) = self.node_id() {
195            self.context
196                .audio_context_impl()
197                .lock()
198                .unwrap()
199                .disconnect_all_from(node_id);
200        }
201        Ok(())
202    }
203
204    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-disconnect-output>
205    fn Disconnect_(&self, out: u32) -> ErrorResult {
206        if let Some(node_id) = self.node_id() {
207            self.context
208                .audio_context_impl()
209                .lock()
210                .unwrap()
211                .disconnect_output(node_id.output(out));
212        }
213        Ok(())
214    }
215
216    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-disconnect-destinationnode>
217    fn Disconnect__(&self, to: &AudioNode) -> ErrorResult {
218        if let (Some(from_node), Some(to_node)) = (self.node_id(), to.node_id()) {
219            self.context
220                .audio_context_impl()
221                .lock()
222                .unwrap()
223                .disconnect_between(from_node, to_node);
224        }
225        Ok(())
226    }
227
228    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-disconnect-destinationnode-output>
229    fn Disconnect___(&self, to: &AudioNode, out: u32) -> ErrorResult {
230        if let (Some(from_node), Some(to_node)) = (self.node_id(), to.node_id()) {
231            self.context
232                .audio_context_impl()
233                .lock()
234                .unwrap()
235                .disconnect_output_between(from_node.output(out), to_node);
236        }
237        Ok(())
238    }
239
240    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-disconnect-destinationnode-output-input>
241    fn Disconnect____(&self, to: &AudioNode, out: u32, inp: u32) -> ErrorResult {
242        if let (Some(from_node), Some(to_node)) = (self.node_id(), to.node_id()) {
243            self.context
244                .audio_context_impl()
245                .lock()
246                .unwrap()
247                .disconnect_output_between_to(from_node.output(out), to_node.input(inp));
248        }
249        Ok(())
250    }
251
252    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-disconnect>
253    fn Disconnect_____(&self, param: &AudioParam) -> ErrorResult {
254        if let (Some(from_node), Some(param_node)) = (self.node_id(), param.node_id()) {
255            self.context
256                .audio_context_impl()
257                .lock()
258                .unwrap()
259                .disconnect_to(from_node, param_node.param(param.param_type()));
260        }
261        Ok(())
262    }
263
264    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-disconnect>
265    fn Disconnect______(&self, param: &AudioParam, out: u32) -> ErrorResult {
266        if let (Some(from_node), Some(param_node)) = (self.node_id(), param.node_id()) {
267            self.context
268                .audio_context_impl()
269                .lock()
270                .unwrap()
271                .disconnect_output_between_to(
272                    from_node.output(out),
273                    param_node.param(param.param_type()),
274                );
275        }
276        Ok(())
277    }
278
279    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-context>
280    fn Context(&self) -> DomRoot<BaseAudioContext> {
281        DomRoot::from_ref(&self.context)
282    }
283
284    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-numberofinputs>
285    fn NumberOfInputs(&self) -> u32 {
286        self.number_of_inputs
287    }
288
289    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-numberofoutputs>
290    fn NumberOfOutputs(&self) -> u32 {
291        self.number_of_outputs
292    }
293
294    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-channelcount>
295    fn ChannelCount(&self) -> u32 {
296        self.channel_count.get()
297    }
298
299    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-channelcount>
300    fn SetChannelCount(&self, value: u32) -> ErrorResult {
301        match self.upcast::<EventTarget>().type_id() {
302            EventTargetTypeId::AudioNode(AudioNodeTypeId::AudioDestinationNode) => {
303                if self.context.is_offline() {
304                    return Err(Error::InvalidState(Some(
305                        "Cannot change channel count".into(),
306                    )));
307                } else if !(1..=MAX_CHANNEL_COUNT).contains(&value) {
308                    return Err(Error::IndexSize(Some(
309                        "Channel count is out of range".into(),
310                    )));
311                }
312            },
313            EventTargetTypeId::AudioNode(AudioNodeTypeId::PannerNode) if value > 2 => {
314                return Err(Error::NotSupported(Some(
315                    "PannerNode channel count cannot be greater than 2".into(),
316                )));
317            },
318            EventTargetTypeId::AudioNode(AudioNodeTypeId::AudioScheduledSourceNode(
319                AudioScheduledSourceNodeTypeId::StereoPannerNode,
320            )) if value > 2 => {
321                return Err(Error::NotSupported(Some(
322                    "StereoPannerNode channel count cannot be greater than 2".into(),
323                )));
324            },
325            EventTargetTypeId::AudioNode(AudioNodeTypeId::ChannelMergerNode) => {
326                return Err(Error::InvalidState(Some(
327                    "Cannot change channel count of a ChannelMergerNode".into(),
328                )));
329            },
330            EventTargetTypeId::AudioNode(AudioNodeTypeId::ChannelSplitterNode) => {
331                return Err(Error::InvalidState(Some(
332                    "Cannot change channel count of a ChannelSplitterNode".into(),
333                )));
334            },
335            // XXX We do not support any of the other AudioNodes with
336            // constraints yet. Add more cases here as we add support
337            // for new AudioNodes.
338            _ => (),
339        };
340
341        if value == 0 || value > MAX_CHANNEL_COUNT {
342            return Err(Error::NotSupported(Some(
343                "Channel count is out of range".into(),
344            )));
345        }
346
347        self.channel_count.set(value);
348        self.message(AudioNodeMessage::SetChannelCount(value as u8));
349        Ok(())
350    }
351
352    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-channelcountmode>
353    fn ChannelCountMode(&self) -> ChannelCountMode {
354        self.channel_count_mode.get()
355    }
356
357    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-channelcountmode>
358    fn SetChannelCountMode(&self, value: ChannelCountMode) -> ErrorResult {
359        // Channel count mode has no effect for nodes with no inputs.
360        if self.number_of_inputs == 0 {
361            return Ok(());
362        }
363
364        match self.upcast::<EventTarget>().type_id() {
365            EventTargetTypeId::AudioNode(AudioNodeTypeId::AudioDestinationNode)
366                if self.context.is_offline() =>
367            {
368                return Err(Error::InvalidState(Some(
369                    "Cannot change channel count mode".into(),
370                )));
371            },
372            EventTargetTypeId::AudioNode(AudioNodeTypeId::PannerNode)
373                if value == ChannelCountMode::Max =>
374            {
375                return Err(Error::NotSupported(Some(
376                    "PannerNode channel count mode cannot be \"max\"".into(),
377                )));
378            },
379            EventTargetTypeId::AudioNode(AudioNodeTypeId::AudioScheduledSourceNode(
380                AudioScheduledSourceNodeTypeId::StereoPannerNode,
381            )) if value == ChannelCountMode::Max => {
382                return Err(Error::NotSupported(Some(
383                    "StereoPannerNode channel count mode cannot be \"max\"".into(),
384                )));
385            },
386            EventTargetTypeId::AudioNode(AudioNodeTypeId::ChannelMergerNode) => {
387                return Err(Error::InvalidState(Some(
388                    "Cannot change channel count mode of a ChannelMergerNode".into(),
389                )));
390            },
391            EventTargetTypeId::AudioNode(AudioNodeTypeId::ChannelSplitterNode) => {
392                return Err(Error::InvalidState(Some(
393                    "Cannot change channel count mode of a ChannelSplitterNode".into(),
394                )));
395            },
396            // XXX We do not support any of the other AudioNodes with
397            // constraints yet. Add more cases here as we add support
398            // for new AudioNodes.
399            _ => (),
400        };
401
402        self.channel_count_mode.set(value);
403        self.message(AudioNodeMessage::SetChannelMode(value.convert()));
404        Ok(())
405    }
406
407    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-channelinterpretation>
408    fn ChannelInterpretation(&self) -> ChannelInterpretation {
409        self.channel_interpretation.get()
410    }
411
412    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-channelinterpretation>
413    fn SetChannelInterpretation(&self, value: ChannelInterpretation) -> ErrorResult {
414        // Channel interpretation mode has no effect for nodes with no inputs.
415        if self.number_of_inputs == 0 {
416            return Ok(());
417        }
418
419        if let EventTargetTypeId::AudioNode(AudioNodeTypeId::ChannelSplitterNode) =
420            self.upcast::<EventTarget>().type_id()
421        {
422            return Err(Error::InvalidState(Some(
423                "Cannot change channel interpretation of a ChannelSplitterNode".into(),
424            )));
425        };
426
427        self.channel_interpretation.set(value);
428        self.message(AudioNodeMessage::SetChannelInterpretation(value.convert()));
429        Ok(())
430    }
431}
432
433impl Convert<ServoMediaChannelCountMode> for ChannelCountMode {
434    fn convert(self) -> ServoMediaChannelCountMode {
435        match self {
436            ChannelCountMode::Max => ServoMediaChannelCountMode::Max,
437            ChannelCountMode::Clamped_max => ServoMediaChannelCountMode::ClampedMax,
438            ChannelCountMode::Explicit => ServoMediaChannelCountMode::Explicit,
439        }
440    }
441}
442
443impl Convert<ServoMediaChannelInterpretation> for ChannelInterpretation {
444    fn convert(self) -> ServoMediaChannelInterpretation {
445        match self {
446            ChannelInterpretation::Discrete => ServoMediaChannelInterpretation::Discrete,
447            ChannelInterpretation::Speakers => ServoMediaChannelInterpretation::Speakers,
448        }
449    }
450}
451
452pub(crate) trait AudioNodeOptionsHelper {
453    fn unwrap_or(
454        &self,
455        count: u32,
456        mode: ChannelCountMode,
457        interpretation: ChannelInterpretation,
458    ) -> UnwrappedAudioNodeOptions;
459}
460
461impl AudioNodeOptionsHelper for AudioNodeOptions {
462    fn unwrap_or(
463        &self,
464        count: u32,
465        mode: ChannelCountMode,
466        interpretation: ChannelInterpretation,
467    ) -> UnwrappedAudioNodeOptions {
468        UnwrappedAudioNodeOptions {
469            count: self.channelCount.unwrap_or(count),
470            mode: self.channelCountMode.unwrap_or(mode),
471            interpretation: self.channelInterpretation.unwrap_or(interpretation),
472        }
473    }
474}
475
476/// Each node has a set of defaults, so this lets us work with them
477/// easily without having to deal with the Options
478pub(crate) struct UnwrappedAudioNodeOptions {
479    pub(crate) count: u32,
480    pub(crate) mode: ChannelCountMode,
481    pub(crate) interpretation: ChannelInterpretation,
482}
483
484impl Default for UnwrappedAudioNodeOptions {
485    fn default() -> Self {
486        UnwrappedAudioNodeOptions {
487            count: 2,
488            mode: ChannelCountMode::Max,
489            interpretation: ChannelInterpretation::Speakers,
490        }
491    }
492}