diff options
Diffstat (limited to 'components/script/dom/baseaudiocontext.rs')
-rw-r--r-- | components/script/dom/baseaudiocontext.rs | 578 |
1 files changed, 578 insertions, 0 deletions
diff --git a/components/script/dom/baseaudiocontext.rs b/components/script/dom/baseaudiocontext.rs new file mode 100644 index 00000000000..42836dd61f0 --- /dev/null +++ b/components/script/dom/baseaudiocontext.rs @@ -0,0 +1,578 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use crate::dom::analysernode::AnalyserNode; +use crate::dom::audiobuffer::AudioBuffer; +use crate::dom::audiobuffersourcenode::AudioBufferSourceNode; +use crate::dom::audiodestinationnode::AudioDestinationNode; +use crate::dom::audiolistener::AudioListener; +use crate::dom::audionode::MAX_CHANNEL_COUNT; +use crate::dom::bindings::callback::ExceptionHandling; +use crate::dom::bindings::cell::DomRefCell; +use crate::dom::bindings::codegen::Bindings::AnalyserNodeBinding::AnalyserOptions; +use crate::dom::bindings::codegen::Bindings::AudioBufferSourceNodeBinding::AudioBufferSourceOptions; +use crate::dom::bindings::codegen::Bindings::AudioNodeBinding::AudioNodeOptions; +use crate::dom::bindings::codegen::Bindings::AudioNodeBinding::{ + ChannelCountMode, ChannelInterpretation, +}; +use crate::dom::bindings::codegen::Bindings::BaseAudioContextBinding::AudioContextState; +use crate::dom::bindings::codegen::Bindings::BaseAudioContextBinding::BaseAudioContextMethods; +use crate::dom::bindings::codegen::Bindings::BaseAudioContextBinding::DecodeErrorCallback; +use crate::dom::bindings::codegen::Bindings::BaseAudioContextBinding::DecodeSuccessCallback; +use crate::dom::bindings::codegen::Bindings::BiquadFilterNodeBinding::BiquadFilterOptions; +use crate::dom::bindings::codegen::Bindings::ChannelMergerNodeBinding::ChannelMergerOptions; +use crate::dom::bindings::codegen::Bindings::ChannelSplitterNodeBinding::ChannelSplitterOptions; +use crate::dom::bindings::codegen::Bindings::ConstantSourceNodeBinding::ConstantSourceOptions; +use crate::dom::bindings::codegen::Bindings::GainNodeBinding::GainOptions; +use crate::dom::bindings::codegen::Bindings::OscillatorNodeBinding::OscillatorOptions; +use crate::dom::bindings::codegen::Bindings::PannerNodeBinding::PannerOptions; +use crate::dom::bindings::codegen::Bindings::StereoPannerNodeBinding::StereoPannerOptions; +use crate::dom::bindings::error::{Error, ErrorResult, Fallible}; +use crate::dom::bindings::inheritance::Castable; +use crate::dom::bindings::num::Finite; +use crate::dom::bindings::refcounted::Trusted; +use crate::dom::bindings::reflector::DomObject; +use crate::dom::bindings::root::{DomRoot, MutNullableDom}; +use crate::dom::biquadfilternode::BiquadFilterNode; +use crate::dom::channelmergernode::ChannelMergerNode; +use crate::dom::channelsplitternode::ChannelSplitterNode; +use crate::dom::constantsourcenode::ConstantSourceNode; +use crate::dom::domexception::{DOMErrorName, DOMException}; +use crate::dom::eventtarget::EventTarget; +use crate::dom::gainnode::GainNode; +use crate::dom::oscillatornode::OscillatorNode; +use crate::dom::pannernode::PannerNode; +use crate::dom::promise::Promise; +use crate::dom::stereopannernode::StereoPannerNode; +use crate::dom::window::Window; +use crate::realms::InRealm; +use crate::task_source::TaskSource; +use dom_struct::dom_struct; +use js::rust::CustomAutoRooterGuard; +use js::typedarray::ArrayBuffer; +use msg::constellation_msg::PipelineId; +use servo_media::audio::context::{AudioContext, AudioContextOptions, ProcessingState}; +use servo_media::audio::context::{OfflineAudioContextOptions, RealTimeAudioContextOptions}; +use servo_media::audio::decoder::AudioDecoderCallbacks; +use servo_media::audio::graph::NodeId; +use servo_media::{ClientContextId, ServoMedia}; +use std::cell::Cell; +use std::collections::hash_map::Entry; +use std::collections::{HashMap, VecDeque}; +use std::mem; +use std::rc::Rc; +use std::sync::{Arc, Mutex}; +use uuid::Uuid; + +#[allow(dead_code)] +pub enum BaseAudioContextOptions { + AudioContext(RealTimeAudioContextOptions), + OfflineAudioContext(OfflineAudioContextOptions), +} + +#[derive(JSTraceable)] +struct DecodeResolver { + pub promise: Rc<Promise>, + pub success_callback: Option<Rc<DecodeSuccessCallback>>, + pub error_callback: Option<Rc<DecodeErrorCallback>>, +} + +#[dom_struct] +pub struct BaseAudioContext { + eventtarget: EventTarget, + #[ignore_malloc_size_of = "servo_media"] + audio_context_impl: Arc<Mutex<AudioContext>>, + /// https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-destination + destination: MutNullableDom<AudioDestinationNode>, + listener: MutNullableDom<AudioListener>, + /// Resume promises which are soon to be fulfilled by a queued task. + #[ignore_malloc_size_of = "promises are hard"] + in_flight_resume_promises_queue: DomRefCell<VecDeque<(Box<[Rc<Promise>]>, ErrorResult)>>, + /// https://webaudio.github.io/web-audio-api/#pendingresumepromises + #[ignore_malloc_size_of = "promises are hard"] + pending_resume_promises: DomRefCell<Vec<Rc<Promise>>>, + #[ignore_malloc_size_of = "promises are hard"] + decode_resolvers: DomRefCell<HashMap<String, DecodeResolver>>, + /// https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-samplerate + sample_rate: f32, + /// https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-state + /// Although servo-media already keeps track of the control thread state, + /// we keep a state flag here as well. This is so that we can synchronously + /// throw when trying to do things on the context when the context has just + /// been "closed()". + state: Cell<AudioContextState>, + channel_count: u32, +} + +impl BaseAudioContext { + #[allow(unrooted_must_root)] + pub fn new_inherited( + options: BaseAudioContextOptions, + pipeline_id: PipelineId, + ) -> BaseAudioContext { + let (sample_rate, channel_count) = match options { + BaseAudioContextOptions::AudioContext(ref opt) => (opt.sample_rate, 2), + BaseAudioContextOptions::OfflineAudioContext(ref opt) => { + (opt.sample_rate, opt.channels) + }, + }; + + let client_context_id = + ClientContextId::build(pipeline_id.namespace_id.0, pipeline_id.index.0.get()); + let context = BaseAudioContext { + eventtarget: EventTarget::new_inherited(), + audio_context_impl: ServoMedia::get() + .unwrap() + .create_audio_context(&client_context_id, options.into()), + destination: Default::default(), + listener: Default::default(), + in_flight_resume_promises_queue: Default::default(), + pending_resume_promises: Default::default(), + decode_resolvers: Default::default(), + sample_rate, + state: Cell::new(AudioContextState::Suspended), + channel_count: channel_count.into(), + }; + + context + } + + /// Tells whether this is an OfflineAudioContext or not. + pub fn is_offline(&self) -> bool { + false + } + + pub fn audio_context_impl(&self) -> Arc<Mutex<AudioContext>> { + self.audio_context_impl.clone() + } + + pub fn destination_node(&self) -> NodeId { + self.audio_context_impl.lock().unwrap().dest_node() + } + + pub fn listener(&self) -> NodeId { + self.audio_context_impl.lock().unwrap().listener() + } + + // https://webaudio.github.io/web-audio-api/#allowed-to-start + pub fn is_allowed_to_start(&self) -> bool { + self.state.get() == AudioContextState::Suspended + } + + fn push_pending_resume_promise(&self, promise: &Rc<Promise>) { + self.pending_resume_promises + .borrow_mut() + .push(promise.clone()); + } + + /// Takes the pending resume promises. + /// + /// The result with which these promises will be fulfilled is passed here + /// and this method returns nothing because we actually just move the + /// current list of pending resume promises to the + /// `in_flight_resume_promises_queue` field. + /// + /// Each call to this method must be followed by a call to + /// `fulfill_in_flight_resume_promises`, to actually fulfill the promises + /// which were taken and moved to the in-flight queue. + fn take_pending_resume_promises(&self, result: ErrorResult) { + let pending_resume_promises = + mem::replace(&mut *self.pending_resume_promises.borrow_mut(), vec![]); + self.in_flight_resume_promises_queue + .borrow_mut() + .push_back((pending_resume_promises.into(), result)); + } + + /// Fulfills the next in-flight resume promises queue after running a closure. + /// + /// See the comment on `take_pending_resume_promises` for why this method + /// does not take a list of promises to fulfill. Callers cannot just pop + /// the front list off of `in_flight_resume_promises_queue` and later fulfill + /// the promises because that would mean putting + /// `#[allow(unrooted_must_root)]` on even more functions, potentially + /// hiding actual safety bugs. + #[allow(unrooted_must_root)] + fn fulfill_in_flight_resume_promises<F>(&self, f: F) + where + F: FnOnce(), + { + let (promises, result) = self + .in_flight_resume_promises_queue + .borrow_mut() + .pop_front() + .expect("there should be at least one list of in flight resume promises"); + f(); + for promise in &*promises { + match result { + Ok(ref value) => promise.resolve_native(value), + Err(ref error) => promise.reject_error(error.clone()), + } + } + } + + /// Control thread processing state + pub fn control_thread_state(&self) -> ProcessingState { + self.audio_context_impl.lock().unwrap().state() + } + + /// Set audio context state + pub fn set_state_attribute(&self, state: AudioContextState) { + self.state.set(state); + } + + pub fn resume(&self) { + let global = self.global(); + let window = global.as_window(); + let task_source = window.task_manager().dom_manipulation_task_source(); + let this = Trusted::new(self); + // Set the rendering thread state to 'running' and start + // rendering the audio graph. + match self.audio_context_impl.lock().unwrap().resume() { + Ok(()) => { + self.take_pending_resume_promises(Ok(())); + let _ = task_source.queue( + task!(resume_success: move || { + let this = this.root(); + this.fulfill_in_flight_resume_promises(|| { + if this.state.get() != AudioContextState::Running { + this.state.set(AudioContextState::Running); + let window = DomRoot::downcast::<Window>(this.global()).unwrap(); + window.task_manager().dom_manipulation_task_source().queue_simple_event( + this.upcast(), + atom!("statechange"), + &window + ); + } + }); + }), + window.upcast(), + ); + }, + Err(()) => { + self.take_pending_resume_promises(Err(Error::Type( + "Something went wrong".to_owned(), + ))); + let _ = task_source.queue( + task!(resume_error: move || { + this.root().fulfill_in_flight_resume_promises(|| {}) + }), + window.upcast(), + ); + }, + } + } +} + +impl BaseAudioContextMethods for BaseAudioContext { + /// https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-samplerate + fn SampleRate(&self) -> Finite<f32> { + Finite::wrap(self.sample_rate) + } + + /// https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-currenttime + fn CurrentTime(&self) -> Finite<f64> { + let current_time = self.audio_context_impl.lock().unwrap().current_time(); + Finite::wrap(current_time) + } + + /// https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-state + fn State(&self) -> AudioContextState { + self.state.get() + } + + /// https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-resume + fn Resume(&self, comp: InRealm) -> Rc<Promise> { + // Step 1. + let promise = Promise::new_in_current_realm(&self.global(), comp); + + // Step 2. + if self.audio_context_impl.lock().unwrap().state() == ProcessingState::Closed { + promise.reject_error(Error::InvalidState); + return promise; + } + + // Step 3. + if self.state.get() == AudioContextState::Running { + promise.resolve_native(&()); + return promise; + } + + self.push_pending_resume_promise(&promise); + + // Step 4. + if !self.is_allowed_to_start() { + return promise; + } + + // Steps 5 and 6. + self.resume(); + + // Step 7. + promise + } + + /// https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-destination + fn Destination(&self) -> DomRoot<AudioDestinationNode> { + let global = self.global(); + self.destination.or_init(|| { + let mut options = AudioNodeOptions::empty(); + options.channelCount = Some(self.channel_count); + options.channelCountMode = Some(ChannelCountMode::Explicit); + options.channelInterpretation = Some(ChannelInterpretation::Speakers); + AudioDestinationNode::new(&global, self, &options) + }) + } + + /// https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-listener + fn Listener(&self) -> DomRoot<AudioListener> { + let global = self.global(); + let window = global.as_window(); + self.listener.or_init(|| AudioListener::new(&window, self)) + } + + // https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-onstatechange + event_handler!(statechange, GetOnstatechange, SetOnstatechange); + + /// https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-createoscillator + fn CreateOscillator(&self) -> Fallible<DomRoot<OscillatorNode>> { + OscillatorNode::new( + &self.global().as_window(), + &self, + &OscillatorOptions::empty(), + ) + } + + /// https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-creategain + fn CreateGain(&self) -> Fallible<DomRoot<GainNode>> { + GainNode::new(&self.global().as_window(), &self, &GainOptions::empty()) + } + + /// https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-createpanner + fn CreatePanner(&self) -> Fallible<DomRoot<PannerNode>> { + PannerNode::new(&self.global().as_window(), &self, &PannerOptions::empty()) + } + + /// https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-createanalyser + fn CreateAnalyser(&self) -> Fallible<DomRoot<AnalyserNode>> { + AnalyserNode::new(&self.global().as_window(), &self, &AnalyserOptions::empty()) + } + + /// https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-createbiquadfilter + fn CreateBiquadFilter(&self) -> Fallible<DomRoot<BiquadFilterNode>> { + BiquadFilterNode::new( + &self.global().as_window(), + &self, + &BiquadFilterOptions::empty(), + ) + } + + /// https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-createstereopanner + fn CreateStereoPanner(&self) -> Fallible<DomRoot<StereoPannerNode>> { + StereoPannerNode::new( + &self.global().as_window(), + &self, + &StereoPannerOptions::empty(), + ) + } + + /// https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-createconstantsource + fn CreateConstantSource(&self) -> Fallible<DomRoot<ConstantSourceNode>> { + ConstantSourceNode::new( + &self.global().as_window(), + &self, + &ConstantSourceOptions::empty(), + ) + } + + /// https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-createchannelmerger + fn CreateChannelMerger(&self, count: u32) -> Fallible<DomRoot<ChannelMergerNode>> { + let mut opts = ChannelMergerOptions::empty(); + opts.numberOfInputs = count; + ChannelMergerNode::new(&self.global().as_window(), &self, &opts) + } + + /// https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-createchannelsplitter + fn CreateChannelSplitter(&self, count: u32) -> Fallible<DomRoot<ChannelSplitterNode>> { + let mut opts = ChannelSplitterOptions::empty(); + opts.numberOfOutputs = count; + ChannelSplitterNode::new(&self.global().as_window(), &self, &opts) + } + + /// https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-createbuffer + fn CreateBuffer( + &self, + number_of_channels: u32, + length: u32, + sample_rate: Finite<f32>, + ) -> Fallible<DomRoot<AudioBuffer>> { + if number_of_channels <= 0 || + number_of_channels > MAX_CHANNEL_COUNT || + length <= 0 || + *sample_rate <= 0. + { + return Err(Error::NotSupported); + } + Ok(AudioBuffer::new( + &self.global().as_window(), + number_of_channels, + length, + *sample_rate, + None, + )) + } + + // https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-createbuffersource + fn CreateBufferSource(&self) -> Fallible<DomRoot<AudioBufferSourceNode>> { + AudioBufferSourceNode::new( + &self.global().as_window(), + &self, + &AudioBufferSourceOptions::empty(), + ) + } + + // https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-decodeaudiodata + fn DecodeAudioData( + &self, + audio_data: CustomAutoRooterGuard<ArrayBuffer>, + decode_success_callback: Option<Rc<DecodeSuccessCallback>>, + decode_error_callback: Option<Rc<DecodeErrorCallback>>, + comp: InRealm, + ) -> Rc<Promise> { + // Step 1. + let promise = Promise::new_in_current_realm(&self.global(), comp); + let global = self.global(); + let window = global.as_window(); + + if audio_data.len() > 0 { + // Step 2. + // XXX detach array buffer. + let uuid = Uuid::new_v4().to_simple().to_string(); + let uuid_ = uuid.clone(); + self.decode_resolvers.borrow_mut().insert( + uuid.clone(), + DecodeResolver { + promise: promise.clone(), + success_callback: decode_success_callback, + error_callback: decode_error_callback, + }, + ); + let audio_data = audio_data.to_vec(); + let decoded_audio = Arc::new(Mutex::new(Vec::new())); + let decoded_audio_ = decoded_audio.clone(); + let decoded_audio__ = decoded_audio.clone(); + // servo-media returns an audio channel position along + // with the AudioDecoderCallback progress callback, which + // may not be the same as the index of the decoded_audio + // Vec. + let channels = Arc::new(Mutex::new(HashMap::new())); + let this = Trusted::new(self); + let this_ = this.clone(); + let (task_source, canceller) = window + .task_manager() + .dom_manipulation_task_source_with_canceller(); + let (task_source_, canceller_) = window + .task_manager() + .dom_manipulation_task_source_with_canceller(); + let callbacks = AudioDecoderCallbacks::new() + .ready(move |channel_count| { + decoded_audio + .lock() + .unwrap() + .resize(channel_count as usize, Vec::new()); + }) + .progress(move |buffer, channel_pos_mask| { + let mut decoded_audio = decoded_audio_.lock().unwrap(); + let mut channels = channels.lock().unwrap(); + let channel = match channels.entry(channel_pos_mask) { + Entry::Occupied(entry) => *entry.get(), + Entry::Vacant(entry) => { + let x = (channel_pos_mask as f32).log2() as usize; + *entry.insert(x) + }, + }; + decoded_audio[channel].extend_from_slice((*buffer).as_ref()); + }) + .eos(move || { + let _ = task_source.queue_with_canceller( + task!(audio_decode_eos: move || { + let this = this.root(); + let decoded_audio = decoded_audio__.lock().unwrap(); + let length = if decoded_audio.len() >= 1 { + decoded_audio[0].len() + } else { + 0 + }; + let buffer = AudioBuffer::new( + &this.global().as_window(), + decoded_audio.len() as u32 /* number of channels */, + length as u32, + this.sample_rate, + Some(decoded_audio.as_slice())); + let mut resolvers = this.decode_resolvers.borrow_mut(); + assert!(resolvers.contains_key(&uuid_)); + let resolver = resolvers.remove(&uuid_).unwrap(); + if let Some(callback) = resolver.success_callback { + let _ = callback.Call__(&buffer, ExceptionHandling::Report); + } + resolver.promise.resolve_native(&buffer); + }), + &canceller, + ); + }) + .error(move |error| { + let _ = task_source_.queue_with_canceller( + task!(audio_decode_eos: move || { + let this = this_.root(); + let mut resolvers = this.decode_resolvers.borrow_mut(); + assert!(resolvers.contains_key(&uuid)); + let resolver = resolvers.remove(&uuid).unwrap(); + if let Some(callback) = resolver.error_callback { + let _ = callback.Call__( + &DOMException::new(&this.global(), DOMErrorName::DataCloneError), + ExceptionHandling::Report); + } + let error = format!("Audio decode error {:?}", error); + resolver.promise.reject_error(Error::Type(error)); + }), + &canceller_, + ); + }) + .build(); + self.audio_context_impl + .lock() + .unwrap() + .decode_audio_data(audio_data, callbacks); + } else { + // Step 3. + promise.reject_error(Error::DataClone); + return promise; + } + + // Step 4. + promise + } +} + +impl From<BaseAudioContextOptions> for AudioContextOptions { + fn from(options: BaseAudioContextOptions) -> Self { + match options { + BaseAudioContextOptions::AudioContext(options) => { + AudioContextOptions::RealTimeAudioContext(options) + }, + BaseAudioContextOptions::OfflineAudioContext(options) => { + AudioContextOptions::OfflineAudioContext(options) + }, + } + } +} + +impl From<ProcessingState> for AudioContextState { + fn from(state: ProcessingState) -> Self { + match state { + ProcessingState::Suspended => AudioContextState::Suspended, + ProcessingState::Running => AudioContextState::Running, + ProcessingState::Closed => AudioContextState::Closed, + } + } +} |