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