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