Skip to main content

script/dom/audio/
offlineaudiocontext.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;
6use std::sync::{Arc, Mutex, mpsc};
7use std::thread::Builder;
8
9use dom_struct::dom_struct;
10use js::context::JSContext;
11use js::realm::CurrentRealm;
12use js::rust::HandleObject;
13use script_bindings::cell::DomRefCell;
14use script_bindings::reflector::reflect_dom_object_with_proto;
15use servo_base::id::PipelineId;
16use servo_media::audio::context::OfflineAudioContextOptions as ServoMediaOfflineAudioContextOptions;
17
18use crate::dom::audio::audiobuffer::{AudioBuffer, MAX_SAMPLE_RATE, MIN_SAMPLE_RATE};
19use crate::dom::audio::audionode::MAX_CHANNEL_COUNT;
20use crate::dom::audio::baseaudiocontext::{BaseAudioContext, BaseAudioContextOptions};
21use crate::dom::audio::offlineaudiocompletionevent::OfflineAudioCompletionEvent;
22use crate::dom::bindings::codegen::Bindings::BaseAudioContextBinding::BaseAudioContext_Binding::BaseAudioContextMethods;
23use crate::dom::bindings::codegen::Bindings::OfflineAudioContextBinding::{
24    OfflineAudioContextMethods, OfflineAudioContextOptions,
25};
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;
30use crate::dom::bindings::reflector::DomGlobal;
31use crate::dom::bindings::root::DomRoot;
32use crate::dom::event::{Event, EventBubbles, EventCancelable};
33use crate::dom::promise::{Promise, RootedPromise, TracedPromise};
34use crate::dom::window::Window;
35
36#[dom_struct]
37pub(crate) struct OfflineAudioContext {
38    context: BaseAudioContext,
39    channel_count: u32,
40    length: u32,
41    rendering_started: Cell<bool>,
42    pending_rendering_promise: DomRefCell<Option<TracedPromise>>,
43}
44
45impl OfflineAudioContext {
46    #[cfg_attr(crown, expect(crown::unrooted_must_root))]
47    fn new_inherited(
48        channel_count: u32,
49        length: u32,
50        sample_rate: f32,
51        pipeline_id: PipelineId,
52    ) -> Fallible<OfflineAudioContext> {
53        let options = ServoMediaOfflineAudioContextOptions {
54            channels: channel_count as u8,
55            length: length as usize,
56            sample_rate,
57        };
58        let context = BaseAudioContext::new_inherited(
59            BaseAudioContextOptions::OfflineAudioContext(options),
60            pipeline_id,
61        )?;
62        Ok(OfflineAudioContext {
63            context,
64            channel_count,
65            length,
66            rendering_started: Cell::new(false),
67            pending_rendering_promise: Default::default(),
68        })
69    }
70
71    #[cfg_attr(crown, expect(crown::unrooted_must_root))]
72    fn new(
73        cx: &mut JSContext,
74        window: &Window,
75        proto: Option<HandleObject>,
76        channel_count: u32,
77        length: u32,
78        sample_rate: f32,
79    ) -> Fallible<DomRoot<OfflineAudioContext>> {
80        if channel_count > MAX_CHANNEL_COUNT ||
81            channel_count == 0 ||
82            length == 0 ||
83            !(MIN_SAMPLE_RATE..=MAX_SAMPLE_RATE).contains(&sample_rate)
84        {
85            return Err(Error::NotSupported(None));
86        }
87        let pipeline_id = window.pipeline_id();
88        let context =
89            OfflineAudioContext::new_inherited(channel_count, length, sample_rate, pipeline_id)?;
90        Ok(reflect_dom_object_with_proto(
91            cx,
92            Box::new(context),
93            window,
94            proto,
95        ))
96    }
97}
98
99impl OfflineAudioContextMethods<crate::DomTypeHolder> for OfflineAudioContext {
100    /// <https://webaudio.github.io/web-audio-api/#dom-offlineaudiocontext-offlineaudiocontext>
101    fn Constructor(
102        cx: &mut JSContext,
103        window: &Window,
104        proto: Option<HandleObject>,
105        options: &OfflineAudioContextOptions,
106    ) -> Fallible<DomRoot<OfflineAudioContext>> {
107        OfflineAudioContext::new(
108            cx,
109            window,
110            proto,
111            options.numberOfChannels,
112            options.length,
113            *options.sampleRate,
114        )
115    }
116
117    /// <https://webaudio.github.io/web-audio-api/#dom-offlineaudiocontext-offlineaudiocontext-numberofchannels-length-samplerate>
118    fn Constructor_(
119        cx: &mut JSContext,
120        window: &Window,
121        proto: Option<HandleObject>,
122        number_of_channels: u32,
123        length: u32,
124        sample_rate: Finite<f32>,
125    ) -> Fallible<DomRoot<OfflineAudioContext>> {
126        OfflineAudioContext::new(cx, window, proto, number_of_channels, length, *sample_rate)
127    }
128
129    // https://webaudio.github.io/web-audio-api/#dom-offlineaudiocontext-oncomplete
130    event_handler!(complete, GetOncomplete, SetOncomplete);
131
132    /// <https://webaudio.github.io/web-audio-api/#dom-offlineaudiocontext-length>
133    fn Length(&self) -> u32 {
134        self.length
135    }
136
137    /// <https://webaudio.github.io/web-audio-api/#dom-offlineaudiocontext-startrendering>
138    fn StartRendering(&self, cx: &mut CurrentRealm) -> RootedPromise {
139        let promise = Promise::new_in_realm_rooted(cx);
140        if self.rendering_started.get() {
141            promise.reject_error(cx, Error::InvalidState(None));
142            return promise;
143        }
144        self.rendering_started.set(true);
145
146        *self.pending_rendering_promise.safe_borrow_mut(cx.no_gc()) = Some(promise.to_traced());
147
148        let processed_audio = Arc::new(Mutex::new(Vec::new()));
149        let processed_audio_ = processed_audio.clone();
150        let (sender, receiver) = mpsc::channel();
151        let sender = Mutex::new(sender);
152        self.context
153            .audio_context_impl()
154            .lock()
155            .unwrap()
156            .set_eos_callback(Box::new(move |buffer| {
157                processed_audio_
158                    .lock()
159                    .unwrap()
160                    .extend_from_slice((*buffer).as_ref());
161                let _ = sender.lock().unwrap().send(());
162            }));
163
164        let this = Trusted::new(self);
165        let task_source = self
166            .global()
167            .task_manager()
168            .dom_manipulation_task_source()
169            .to_sendable();
170        Builder::new()
171            .name("OfflineACResolver".to_owned())
172            .spawn(move || {
173                let _ = receiver.recv();
174                task_source.queue(task!(resolve: move |cx| {
175                    let this = this.root();
176                    let processed_audio = processed_audio.lock().unwrap();
177                    let mut processed_audio: Vec<_> = processed_audio
178                        .chunks(this.length as usize)
179                        .map(|channel| channel.to_vec())
180                        .collect();
181                    // it can end up being empty if the task failed
182                    if processed_audio.len() != this.length as usize {
183                        processed_audio.resize(this.length as usize, Vec::new())
184                    }
185                    let buffer = AudioBuffer::new(
186                        cx,
187                        this.global().as_window(),
188                        this.channel_count,
189                        this.length,
190                        *this.context.SampleRate(),
191                        Some(processed_audio.as_slice()),
192                    );
193                    let promise = {
194                        (*this
195                            .pending_rendering_promise
196                            .safe_borrow_mut(cx.no_gc()))
197                            .take()
198                            .unwrap()
199                            .root(cx)
200                    };
201                    promise.resolve_native(cx, &buffer);
202                    let global = &this.global();
203                    let window = global.as_window();
204                    let event = OfflineAudioCompletionEvent::new(cx, window,
205                                                                 atom!("complete"),
206                                                                 EventBubbles::DoesNotBubble,
207                                                                 EventCancelable::NotCancelable,
208                                                                 &buffer);
209                    event.upcast::<Event>().fire(cx, this.upcast());
210                }));
211            })
212            .unwrap();
213
214        if self
215            .context
216            .audio_context_impl()
217            .lock()
218            .unwrap()
219            .resume()
220            .is_none()
221        {
222            promise.reject_error(
223                cx,
224                Error::Type(c"Could not start offline rendering".to_owned()),
225            );
226        }
227
228        promise
229    }
230}