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::{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(Some(
86                "Invalid number of channels, length or sample rate".into(),
87            )));
88        }
89        let pipeline_id = window.pipeline_id();
90        let context =
91            OfflineAudioContext::new_inherited(channel_count, length, sample_rate, pipeline_id)?;
92        Ok(reflect_dom_object_with_proto(
93            cx,
94            Box::new(context),
95            window,
96            proto,
97        ))
98    }
99}
100
101impl OfflineAudioContextMethods<crate::DomTypeHolder> for OfflineAudioContext {
102    /// <https://webaudio.github.io/web-audio-api/#dom-offlineaudiocontext-offlineaudiocontext>
103    fn Constructor(
104        cx: &mut JSContext,
105        window: &Window,
106        proto: Option<HandleObject>,
107        options: &OfflineAudioContextOptions,
108    ) -> Fallible<DomRoot<OfflineAudioContext>> {
109        OfflineAudioContext::new(
110            cx,
111            window,
112            proto,
113            options.numberOfChannels,
114            options.length,
115            *options.sampleRate,
116        )
117    }
118
119    /// <https://webaudio.github.io/web-audio-api/#dom-offlineaudiocontext-offlineaudiocontext-numberofchannels-length-samplerate>
120    fn Constructor_(
121        cx: &mut JSContext,
122        window: &Window,
123        proto: Option<HandleObject>,
124        number_of_channels: u32,
125        length: u32,
126        sample_rate: Finite<f32>,
127    ) -> Fallible<DomRoot<OfflineAudioContext>> {
128        OfflineAudioContext::new(cx, window, proto, number_of_channels, length, *sample_rate)
129    }
130
131    // https://webaudio.github.io/web-audio-api/#dom-offlineaudiocontext-oncomplete
132    event_handler!(complete, GetOncomplete, SetOncomplete);
133
134    /// <https://webaudio.github.io/web-audio-api/#dom-offlineaudiocontext-length>
135    fn Length(&self) -> u32 {
136        self.length
137    }
138
139    /// <https://webaudio.github.io/web-audio-api/#dom-offlineaudiocontext-startrendering>
140    fn StartRendering(&self, cx: &mut CurrentRealm) -> RootedPromise {
141        let promise = Promise::new_in_realm(cx);
142        if self.rendering_started.get() {
143            promise.reject_error(
144                cx,
145                Error::InvalidState(Some("Rendering has already started".into())),
146            );
147            return promise;
148        }
149        self.rendering_started.set(true);
150
151        *self.pending_rendering_promise.safe_borrow_mut(cx.no_gc()) = Some(promise.to_traced());
152
153        let length = self.length as usize;
154        let channel_count = self.channel_count as usize;
155        let (sender, receiver) = mpsc::channel();
156        let sender = Mutex::new(sender);
157        self.context
158            .audio_context_impl()
159            .lock()
160            .unwrap()
161            .set_eos_callback(Box::new(move |buffer| {
162                let channels: Vec<Vec<f32>> = (*buffer)
163                    .as_ref()
164                    .chunks(length)
165                    .take(channel_count)
166                    .map(|channel| channel.to_vec())
167                    .collect();
168                let _ = sender.lock().unwrap().send(channels);
169            }));
170
171        let this = Trusted::new(self);
172        let task_source = self
173            .global()
174            .task_manager()
175            .dom_manipulation_task_source()
176            .to_sendable();
177        Builder::new()
178            .name("OfflineACResolver".to_owned())
179            .spawn(move || {
180                let processed_audio = receiver.recv().unwrap_or_default();
181                task_source.queue(task!(resolve: move |cx| {
182                    let this = this.root();
183                    let mut processed_audio = processed_audio;
184                    // it can end up being empty if the task failed
185                    processed_audio.resize_with(this.channel_count as usize, || {
186                        vec![0.; this.length as usize]
187                    });
188                    let buffer = AudioBuffer::new_with_channel_data(
189                        cx,
190                        this.global().as_window(),
191                        this.length,
192                        *this.context.SampleRate(),
193                        processed_audio,
194                    );
195                    let promise = {
196                        (*this
197                            .pending_rendering_promise
198                            .safe_borrow_mut(cx.no_gc()))
199                            .take()
200                            .unwrap()
201                            .root(cx)
202                    };
203                    promise.resolve_native(cx, &buffer);
204                    let global = &this.global();
205                    let window = global.as_window();
206                    let event = OfflineAudioCompletionEvent::new(cx, window,
207                                                                 atom!("complete"),
208                                                                 EventBubbles::DoesNotBubble,
209                                                                 EventCancelable::NotCancelable,
210                                                                 &buffer);
211                    event.upcast::<Event>().fire(cx, this.upcast());
212                }));
213            })
214            .unwrap();
215
216        if self
217            .context
218            .audio_context_impl()
219            .lock()
220            .unwrap()
221            .resume()
222            .is_none()
223        {
224            promise.reject_error(
225                cx,
226                Error::Type(c"Could not start offline rendering".to_owned()),
227            );
228        }
229
230        promise
231    }
232}