servo/components/script/dom/baseaudiocontext.rs
Shamir Khodzha 492faa3105 fixed channels indexing in progress callback in BaseAudioContext.DecodeAudioData
Gstreamer backend returns channel as single bit mask (ie 1, 2, 4, 8, 32 etc).
Progress callback was using this mask as plain channel index, thus storing decoded
audio in wrong channel.
2020-05-04 00:48:51 +03:00

578 lines
24 KiB
Rust

/* 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,
}
}
}