Skip to main content

script/dom/audio/
audiocontext.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 dom_struct::dom_struct;
6use js::context::JSContext;
7use js::realm::CurrentRealm;
8use js::rust::HandleObject;
9use script_bindings::codegen::GenericBindings::MediaElementAudioSourceNodeBinding::{
10    MediaElementAudioSourceNodeMethods, MediaElementAudioSourceOptions,
11};
12use script_bindings::codegen::GenericBindings::MediaStreamAudioDestinationNodeBinding::MediaStreamAudioDestinationNodeMethods;
13use script_bindings::codegen::GenericBindings::MediaStreamAudioSourceNodeBinding::{
14    MediaStreamAudioSourceNodeMethods, MediaStreamAudioSourceOptions,
15};
16use script_bindings::codegen::GenericBindings::MediaStreamTrackAudioSourceNodeBinding::{
17    MediaStreamTrackAudioSourceNodeMethods, MediaStreamTrackAudioSourceOptions,
18};
19use script_bindings::reflector::reflect_dom_object_with_proto;
20use servo_base::id::PipelineId;
21use servo_media::audio::context::{LatencyCategory, ProcessingState, RealTimeAudioContextOptions};
22
23use crate::conversions::Convert;
24use crate::dom::audio::baseaudiocontext::{BaseAudioContext, BaseAudioContextOptions};
25use crate::dom::audio::mediaelementaudiosourcenode::MediaElementAudioSourceNode;
26use crate::dom::audio::mediastreamaudiodestinationnode::MediaStreamAudioDestinationNode;
27use crate::dom::audio::mediastreamaudiosourcenode::MediaStreamAudioSourceNode;
28use crate::dom::audio::mediastreamtrackaudiosourcenode::MediaStreamTrackAudioSourceNode;
29use crate::dom::bindings::codegen::Bindings::AudioContextBinding::{
30    AudioContextLatencyCategory, AudioContextMethods, AudioContextOptions, AudioTimestamp,
31};
32use crate::dom::bindings::codegen::Bindings::AudioNodeBinding::AudioNodeOptions;
33use crate::dom::bindings::codegen::Bindings::BaseAudioContextBinding::AudioContextState;
34use crate::dom::bindings::codegen::Bindings::BaseAudioContextBinding::BaseAudioContext_Binding::BaseAudioContextMethods;
35use crate::dom::bindings::codegen::UnionTypes::AudioContextLatencyCategoryOrDouble;
36use crate::dom::bindings::error::{Error, Fallible};
37use crate::dom::bindings::inheritance::Castable;
38use crate::dom::bindings::num::Finite;
39use crate::dom::bindings::refcounted::{Trusted, TrustedPromise};
40use crate::dom::bindings::reflector::DomGlobal;
41use crate::dom::bindings::root::DomRoot;
42use crate::dom::html::htmlmediaelement::HTMLMediaElement;
43use crate::dom::mediastream::MediaStream;
44use crate::dom::mediastreamtrack::MediaStreamTrack;
45use crate::dom::promise::{Promise, RootedPromise};
46use crate::dom::window::Window;
47
48#[dom_struct]
49pub(crate) struct AudioContext {
50    context: BaseAudioContext,
51    latency_hint: AudioContextLatencyCategory,
52    /// <https://webaudio.github.io/web-audio-api/#dom-audiocontext-baselatency>
53    base_latency: f64,
54    /// <https://webaudio.github.io/web-audio-api/#dom-audiocontext-outputlatency>
55    output_latency: f64,
56}
57
58impl AudioContext {
59    #[cfg_attr(crown, expect(crown::unrooted_must_root))]
60    /// <https://webaudio.github.io/web-audio-api/#AudioContext-constructors>
61    fn new_inherited(
62        options: &AudioContextOptions,
63        pipeline_id: PipelineId,
64    ) -> Fallible<AudioContext> {
65        // Steps 1-3.
66        let context = BaseAudioContext::new_inherited(
67            BaseAudioContextOptions::AudioContext(options.convert()),
68            pipeline_id,
69        )?;
70
71        // Step 4.1.
72        let latency_hint = match options.latencyHint {
73            AudioContextLatencyCategoryOrDouble::AudioContextLatencyCategory(category) => category,
74            AudioContextLatencyCategoryOrDouble::Double(_) => {
75                AudioContextLatencyCategory::Interactive
76            }, // TODO
77        };
78
79        // Step 4.2. The sample rate is set during the creation of the BaseAudioContext.
80        // servo-media takes care of setting the default sample rate of the output device
81        // and of resampling the audio output if needed.
82
83        // Steps 5 and 6 of the construction algorithm will happen in `resume`,
84        // after reflecting dom object.
85
86        Ok(AudioContext {
87            context,
88            latency_hint,
89            base_latency: 0.,   // TODO
90            output_latency: 0., // TODO
91        })
92    }
93
94    #[cfg_attr(crown, expect(crown::unrooted_must_root))]
95    fn new(
96        cx: &mut js::context::JSContext,
97        window: &Window,
98        proto: Option<HandleObject>,
99        options: &AudioContextOptions,
100    ) -> Fallible<DomRoot<AudioContext>> {
101        let pipeline_id = window.pipeline_id();
102        let context = AudioContext::new_inherited(options, pipeline_id)?;
103        let context = reflect_dom_object_with_proto(cx, Box::new(context), window, proto);
104        context.resume();
105        Ok(context)
106    }
107
108    fn resume(&self) {
109        // Step 5.
110        if self.context.is_allowed_to_start() {
111            // Step 6.
112            self.context.resume();
113        }
114    }
115
116    pub(crate) fn base(&self) -> DomRoot<BaseAudioContext> {
117        DomRoot::from_ref(&self.context)
118    }
119}
120
121impl AudioContextMethods<crate::DomTypeHolder> for AudioContext {
122    /// <https://webaudio.github.io/web-audio-api/#AudioContext-constructors>
123    fn Constructor(
124        cx: &mut js::context::JSContext,
125        window: &Window,
126        proto: Option<HandleObject>,
127        options: &AudioContextOptions,
128    ) -> Fallible<DomRoot<AudioContext>> {
129        AudioContext::new(cx, window, proto, options)
130    }
131
132    /// <https://webaudio.github.io/web-audio-api/#dom-audiocontext-baselatency>
133    fn BaseLatency(&self) -> Finite<f64> {
134        Finite::wrap(self.base_latency)
135    }
136
137    /// <https://webaudio.github.io/web-audio-api/#dom-audiocontext-outputlatency>
138    fn OutputLatency(&self) -> Finite<f64> {
139        Finite::wrap(self.output_latency)
140    }
141
142    /// <https://webaudio.github.io/web-audio-api/#dom-audiocontext-outputlatency>
143    fn GetOutputTimestamp(&self) -> AudioTimestamp {
144        // TODO
145        AudioTimestamp {
146            contextTime: Some(Finite::wrap(0.)),
147            performanceTime: Some(Finite::wrap(0.)),
148        }
149    }
150
151    /// <https://webaudio.github.io/web-audio-api/#dom-audiocontext-suspend>
152    fn Suspend(&self, cx: &mut CurrentRealm) -> RootedPromise {
153        // Step 1.
154        let promise = Promise::new_in_realm(cx);
155
156        // Step 2.
157        if self.context.control_thread_state() == ProcessingState::Closed {
158            promise.reject_error(
159                cx,
160                Error::InvalidState(Some("Cannot suspend a closed AudioContext".into())),
161            );
162            return promise;
163        }
164
165        // Step 3.
166        if self.context.State() == AudioContextState::Suspended {
167            promise.resolve_native(cx, &());
168            return promise;
169        }
170
171        // Steps 4 and 5.
172        let trusted_promise = TrustedPromise::from(&promise);
173        match self.context.audio_context_impl().lock().unwrap().suspend() {
174            Some(_) => {
175                let base_context = Trusted::new(&self.context);
176                let context = Trusted::new(self);
177                self.global().task_manager().dom_manipulation_task_source().queue(
178                    task!(suspend_ok: move |cx| {
179                        let base_context = base_context.root();
180                        let context = context.root();
181                        let promise = trusted_promise.root(cx);
182                        promise.resolve_native(cx, &());
183                        if base_context.State() != AudioContextState::Suspended {
184                            base_context.set_state_attribute(AudioContextState::Suspended);
185                            context.global().task_manager().dom_manipulation_task_source().queue_simple_event(
186                                context.upcast(),
187                                atom!("statechange"),
188                            );
189                        }
190                    })
191                );
192            },
193            None => {
194                // The spec does not define the error case and `suspend` should
195                // never fail, but we handle the case here for completion.
196                self.global()
197                    .task_manager()
198                    .dom_manipulation_task_source()
199                    .queue(task!(suspend_error: move |cx| {
200                        let promise = trusted_promise.root(cx);
201                        promise.reject_error(cx, Error::Type(c"Something went wrong".to_owned()));
202                    }));
203            },
204        };
205
206        // Step 6.
207        promise
208    }
209
210    /// <https://webaudio.github.io/web-audio-api/#dom-audiocontext-close>
211    fn Close(&self, cx: &mut CurrentRealm) -> RootedPromise {
212        // Step 1.
213        let promise = Promise::new_in_realm(cx);
214
215        // Step 2.
216        if self.context.control_thread_state() == ProcessingState::Closed {
217            promise.reject_error(
218                cx,
219                Error::InvalidState(Some("AudioContext is already closed".into())),
220            );
221            return promise;
222        }
223
224        // Step 3.
225        if self.context.State() == AudioContextState::Closed {
226            promise.resolve_native(cx, &());
227            return promise;
228        }
229
230        // Steps 4 and 5.
231        let trusted_promise = TrustedPromise::from(&promise);
232        match self.context.audio_context_impl().lock().unwrap().close() {
233            Some(_) => {
234                let base_context = Trusted::new(&self.context);
235                let context = Trusted::new(self);
236                self.global().task_manager().dom_manipulation_task_source().queue(
237                    task!(suspend_ok: move |cx| {
238                        let base_context = base_context.root();
239                        let context = context.root();
240                        let promise = trusted_promise.root(cx);
241                        promise.resolve_native(cx, &());
242                        if base_context.State() != AudioContextState::Closed {
243                            base_context.set_state_attribute(AudioContextState::Closed);
244                            context.global().task_manager().dom_manipulation_task_source().queue_simple_event(
245                                context.upcast(),
246                                atom!("statechange"),
247                            );
248                        }
249                    })
250                );
251            },
252            None => {
253                // The spec does not define the error case and `suspend` should
254                // never fail, but we handle the case here for completion.
255                self.global()
256                    .task_manager()
257                    .dom_manipulation_task_source()
258                    .queue(task!(suspend_error: move |cx| {
259                        let promise = trusted_promise.root(cx);
260                        promise.reject_error(cx, Error::Type(c"Something went wrong".to_owned()));
261                    }));
262            },
263        };
264
265        // Step 6.
266        promise
267    }
268
269    /// <https://webaudio.github.io/web-audio-api/#dom-audiocontext-createmediaelementsource>
270    fn CreateMediaElementSource(
271        &self,
272        cx: &mut JSContext,
273        media_element: &HTMLMediaElement,
274    ) -> Fallible<DomRoot<MediaElementAudioSourceNode>> {
275        let global = self.global();
276        let window = global.as_window();
277        let options = MediaElementAudioSourceOptions {
278            mediaElement: DomRoot::from_ref(media_element),
279        };
280        MediaElementAudioSourceNode::Constructor(cx, window, None, self, &options)
281    }
282
283    /// <https://webaudio.github.io/web-audio-api/#dom-audiocontext-createmediastreamsource>
284    fn CreateMediaStreamSource(
285        &self,
286        cx: &mut JSContext,
287        stream: &MediaStream,
288    ) -> Fallible<DomRoot<MediaStreamAudioSourceNode>> {
289        let global = self.global();
290        let window = global.as_window();
291        let options = MediaStreamAudioSourceOptions {
292            mediaStream: DomRoot::from_ref(stream),
293        };
294        MediaStreamAudioSourceNode::Constructor(cx, window, None, self, &options)
295    }
296
297    /// <https://webaudio.github.io/web-audio-api/#dom-audiocontext-createmediastreamtracksource>
298    fn CreateMediaStreamTrackSource(
299        &self,
300        cx: &mut JSContext,
301        track: &MediaStreamTrack,
302    ) -> Fallible<DomRoot<MediaStreamTrackAudioSourceNode>> {
303        let global = self.global();
304        let window = global.as_window();
305        let options = MediaStreamTrackAudioSourceOptions {
306            mediaStreamTrack: DomRoot::from_ref(track),
307        };
308        MediaStreamTrackAudioSourceNode::Constructor(cx, window, None, self, &options)
309    }
310
311    /// <https://webaudio.github.io/web-audio-api/#dom-audiocontext-createmediastreamdestination>
312    fn CreateMediaStreamDestination(
313        &self,
314        cx: &mut JSContext,
315    ) -> Fallible<DomRoot<MediaStreamAudioDestinationNode>> {
316        let global = self.global();
317        let window = global.as_window();
318        MediaStreamAudioDestinationNode::Constructor(
319            cx,
320            window,
321            None,
322            self,
323            &AudioNodeOptions::empty(),
324        )
325    }
326}
327
328impl Convert<LatencyCategory> for AudioContextLatencyCategory {
329    fn convert(self) -> LatencyCategory {
330        match self {
331            AudioContextLatencyCategory::Balanced => LatencyCategory::Balanced,
332            AudioContextLatencyCategory::Interactive => LatencyCategory::Interactive,
333            AudioContextLatencyCategory::Playback => LatencyCategory::Playback,
334        }
335    }
336}
337
338impl Convert<RealTimeAudioContextOptions> for &AudioContextOptions {
339    fn convert(self) -> RealTimeAudioContextOptions {
340        RealTimeAudioContextOptions {
341            sample_rate: *self.sampleRate.unwrap_or(Finite::wrap(44100.)),
342            latency_hint: match self.latencyHint {
343                AudioContextLatencyCategoryOrDouble::AudioContextLatencyCategory(category) => {
344                    category.convert()
345                },
346                AudioContextLatencyCategoryOrDouble::Double(_) => LatencyCategory::Interactive, // TODO
347            },
348        }
349    }
350}