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