mirror of
https://github.com/lexmount/moli.git
synced 2026-10-06 00:00:56 +00:00
fix(audio): implement native AudioBuffer channel storage and copying
This commit is contained in:
@@ -52,8 +52,8 @@ use super::super::{
|
||||
url_form::build_url_constructor_template,
|
||||
url_search_params_runtime::build_url_search_params_constructor_template,
|
||||
web_audio_runtime::{
|
||||
build_audio_context_constructor_template, build_audio_worklet_node_constructor_template,
|
||||
offline_audio_context_constructor_callback,
|
||||
build_audio_buffer_constructor_template, build_audio_context_constructor_template,
|
||||
build_audio_worklet_node_constructor_template, offline_audio_context_constructor_callback,
|
||||
},
|
||||
webrtc::{rtc_ice_candidate_constructor_callback, rtc_peer_connection_constructor_callback},
|
||||
websocket::{
|
||||
@@ -617,12 +617,12 @@ pub(in crate::context_bootstrap) fn build_constructor_template<'s>(
|
||||
| ConstructorKind::DynamicsCompressorNode
|
||||
| ConstructorKind::AnalyserNode
|
||||
| ConstructorKind::BiquadFilterNode
|
||||
| ConstructorKind::AudioParam
|
||||
| ConstructorKind::AudioBuffer => {
|
||||
| ConstructorKind::AudioParam => {
|
||||
v8::FunctionTemplate::builder(illegal_constructor_callback)
|
||||
.length(0)
|
||||
.build(scope)
|
||||
}
|
||||
ConstructorKind::AudioBuffer => build_audio_buffer_constructor_template(scope),
|
||||
ConstructorKind::Text => v8::FunctionTemplate::builder(text_constructor_callback)
|
||||
.length(0)
|
||||
.build(scope),
|
||||
|
||||
@@ -52,9 +52,7 @@ use super::super::{
|
||||
svg_runtime::install_svg_template_bindings,
|
||||
touch_runtime::install_touch_template_bindings,
|
||||
view_transition_runtime::install_view_transition_template_bindings,
|
||||
web_audio_runtime::{
|
||||
audio_buffer_get_channel_data_callback, install_web_audio_template_bindings,
|
||||
},
|
||||
web_audio_runtime::install_web_audio_template_bindings,
|
||||
webrtc::install_webrtc_template_bindings,
|
||||
websocket::{install_websocket_bindings, install_websocket_stream_bindings},
|
||||
window_runtime::storage_bucket_caches_getter_callback,
|
||||
@@ -181,17 +179,6 @@ struct HtmlCanvasElementTemplateMethodsDeclaration {
|
||||
transfer_control_to_offscreen: (),
|
||||
}
|
||||
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
#[webapi(name = "AudioBuffer", enumerable)]
|
||||
struct AudioBufferTemplateMethodsDeclaration {
|
||||
#[webapi(
|
||||
method = "getChannelData",
|
||||
length = 1,
|
||||
callback = audio_buffer_get_channel_data_callback
|
||||
)]
|
||||
get_channel_data: (),
|
||||
}
|
||||
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
#[webapi(name = "Worker", enumerable)]
|
||||
struct WorkerTemplateMethodsDeclaration {
|
||||
@@ -563,10 +550,6 @@ pub(super) fn install_constructor_template_bindings<'s>(
|
||||
"Navigation" => {
|
||||
install_navigation_bindings(scope, template);
|
||||
}
|
||||
"AudioBuffer" => {
|
||||
let proto = template.prototype_template(scope);
|
||||
AudioBufferTemplateMethodsDeclaration::initialize_prototype_template(scope, proto);
|
||||
}
|
||||
"Worker" => {
|
||||
let proto = template.prototype_template(scope);
|
||||
WorkerTemplateMethodsDeclaration::initialize_prototype_template(scope, proto);
|
||||
|
||||
@@ -40,7 +40,6 @@ pub(crate) const SIMPLE_EVENT_TARGET_ORDERED_HANDLERS_SLOT: &str =
|
||||
"__moliSimpleEventTargetOrderedHandlers";
|
||||
pub(in crate::context_bootstrap) const OFFLINE_AUDIO_LISTENERS_SLOT: &str =
|
||||
"__moliOfflineAudioListeners";
|
||||
pub(in crate::context_bootstrap) const OFFLINE_AUDIO_BUFFER_SLOT: &str = "__moliOfflineAudioBuffer";
|
||||
pub(in crate::context_bootstrap) const SCREEN_EVENT_LISTENERS_SLOT: &str =
|
||||
"__moliScreenEventListeners";
|
||||
pub(in crate::context_bootstrap) const SCREEN_ORIENTATION_EVENT_LISTENERS_SLOT: &str =
|
||||
|
||||
@@ -12,10 +12,12 @@ use crate::util::{
|
||||
use crate::webidl;
|
||||
use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject};
|
||||
|
||||
mod audio_buffer;
|
||||
mod audio_param;
|
||||
mod biquad;
|
||||
mod graph;
|
||||
|
||||
pub(in crate::context_bootstrap) use audio_buffer::build_constructor_template as build_audio_buffer_constructor_template;
|
||||
use audio_param::{audio_param, detune_param};
|
||||
|
||||
const AUDIO_CONTEXT_LISTENERS_SLOT: &str = "__moliAudioContextListeners";
|
||||
@@ -265,19 +267,6 @@ struct OfflineAudioCompletionEventDeclaration<'scope> {
|
||||
#[webapi(interface = "AudioDestinationNode", allow_empty)]
|
||||
struct AudioDestinationNodeObjectDeclaration {}
|
||||
|
||||
#[derive(WebApiObject)]
|
||||
#[webapi(interface = "AudioBuffer")]
|
||||
struct AudioBufferObjectDeclaration<'scope> {
|
||||
#[webapi(data_property)]
|
||||
length: f64,
|
||||
#[webapi(data_property = "sampleRate")]
|
||||
sample_rate: f64,
|
||||
#[webapi(data_property)]
|
||||
duration: f64,
|
||||
#[webapi(slot = OFFLINE_AUDIO_BUFFER_SLOT)]
|
||||
channel_data: v8::Local<'scope, v8::Object>,
|
||||
}
|
||||
|
||||
// Captured from the Chromium-on-Linux baseline we use for Zhihu probe parity.
|
||||
// If that browser profile changes, update this together with the audio probe
|
||||
// assertions in `script_vm/tests.rs`.
|
||||
@@ -361,13 +350,6 @@ struct OfflineAudioContextConstructorArgs {
|
||||
sample_rate: f64,
|
||||
}
|
||||
|
||||
#[derive(webidl::WebIdlArgs)]
|
||||
#[webidl(prefix = "AudioBuffer.getChannelData")]
|
||||
struct AudioBufferGetChannelDataArgs {
|
||||
#[webidl(required)]
|
||||
channel: f64,
|
||||
}
|
||||
|
||||
#[derive(webidl::WebIdlArgs)]
|
||||
#[webidl(prefix = "AudioParam.setValueAtTime")]
|
||||
struct AudioParamSetValueAtTimeArgs {
|
||||
@@ -413,6 +395,9 @@ struct AudioWorkletNodeTemplateDeclaration {
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
#[webapi(name = "BaseAudioContext", enumerable)]
|
||||
struct BaseAudioContextPrototypeDeclaration {
|
||||
#[webapi(method = "createBuffer", length = 3, callback = audio_buffer::create_buffer)]
|
||||
create_buffer: (),
|
||||
|
||||
#[webapi(method = "createBiquadFilter", length = 0, callback = biquad::create_biquad_filter)]
|
||||
create_biquad_filter: (),
|
||||
|
||||
@@ -1282,30 +1267,6 @@ pub(in crate::context_bootstrap) fn offline_audio_context_constructor_callback<'
|
||||
rv.set(context.into());
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap) fn audio_buffer_get_channel_data_callback<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'s, v8::Value>,
|
||||
) {
|
||||
let buffer = args.this();
|
||||
let Some(parsed) = webidl::parse_args::<AudioBufferGetChannelDataArgs>(scope, &args) else {
|
||||
return;
|
||||
};
|
||||
let requested_channel = parsed.channel.trunc();
|
||||
if !requested_channel.is_finite() || requested_channel != 0.0 {
|
||||
throw_range_error(
|
||||
scope,
|
||||
"Failed to execute 'getChannelData' on 'AudioBuffer': channel index is out of range.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
let Some(data) = web_audio_object_slot(scope, buffer, OFFLINE_AUDIO_BUFFER_SLOT) else {
|
||||
rv.set_undefined();
|
||||
return;
|
||||
};
|
||||
rv.set(data.into());
|
||||
}
|
||||
|
||||
fn require_base_audio_context<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
@@ -1387,7 +1348,13 @@ fn offline_audio_context_start_rendering_callback<'s>(
|
||||
let sample_rate =
|
||||
web_audio_number_slot(scope, context, OFFLINE_AUDIO_SAMPLE_RATE_SLOT).unwrap_or(44_100.0);
|
||||
let has_input = graph::prepare_offline_render(scope, context, length as f64 / sample_rate);
|
||||
let rendered_buffer = build_audio_buffer(scope, length, sample_rate, has_input);
|
||||
let channel_count = web_audio_number_slot(scope, context, OFFLINE_AUDIO_CHANNEL_COUNT_SLOT)
|
||||
.unwrap_or(1.0) as u32;
|
||||
let Some(rendered_buffer) =
|
||||
build_audio_buffer(scope, channel_count, length, sample_rate, has_input)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
define_non_enumerable_string_property(scope, context, "state", "closed");
|
||||
|
||||
let payload = OfflineAudioCompletePayloadDeclaration::new(context, rendered_buffer)
|
||||
@@ -1750,40 +1717,30 @@ fn audio_destination_node<'s>(
|
||||
|
||||
fn build_audio_buffer<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
channel_count: u32,
|
||||
length: usize,
|
||||
sample_rate: f64,
|
||||
has_input: bool,
|
||||
) -> v8::Local<'s, v8::Object> {
|
||||
let channel_data = build_channel_data_view(scope, length, has_input);
|
||||
AudioBufferObjectDeclaration::new(
|
||||
length as f64,
|
||||
sample_rate,
|
||||
(length as f64) / sample_rate.max(1.0),
|
||||
channel_data,
|
||||
)
|
||||
.bind(scope)
|
||||
.expect("AudioBuffer declaration should bind")
|
||||
}
|
||||
|
||||
fn build_channel_data_view<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
length: usize,
|
||||
has_input: bool,
|
||||
) -> v8::Local<'s, v8::Object> {
|
||||
let samples = if has_input {
|
||||
synthetic_audio_samples(length)
|
||||
} else {
|
||||
vec![0.0; length]
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
let Ok(length) = u32::try_from(length) else {
|
||||
throw_dom_exception(
|
||||
scope,
|
||||
"NotSupportedError",
|
||||
9,
|
||||
"AudioBuffer length is too large.",
|
||||
);
|
||||
return None;
|
||||
};
|
||||
let mut bytes = Vec::with_capacity(samples.len() * std::mem::size_of::<f32>());
|
||||
for sample in samples {
|
||||
bytes.extend_from_slice(&sample.to_le_bytes());
|
||||
let buffer = audio_buffer::new_buffer(scope, channel_count, length, sample_rate)?;
|
||||
if has_input {
|
||||
// The existing input renderer remains synthetic. Buffer ownership and
|
||||
// channel copies are independent of that backend and use actual PCM.
|
||||
let samples = synthetic_audio_samples(length as usize);
|
||||
for channel in 0..channel_count {
|
||||
audio_buffer::write_channel(scope, buffer, channel, &samples)?;
|
||||
}
|
||||
}
|
||||
let backing_store = v8::ArrayBuffer::new_backing_store_from_vec(bytes).make_shared();
|
||||
let buffer = v8::ArrayBuffer::with_backing_store(scope, &backing_store);
|
||||
let view = v8::Float32Array::new(scope, buffer, 0, length)
|
||||
.expect("Float32Array construction should succeed");
|
||||
view.into()
|
||||
Some(buffer)
|
||||
}
|
||||
|
||||
fn synthetic_audio_samples(length: usize) -> Vec<f32> {
|
||||
|
||||
@@ -0,0 +1,405 @@
|
||||
//! Owned PCM channels shared by AudioBuffer's constructor and context factories.
|
||||
//!
|
||||
//! The channel views are private, GC-traced values. Metadata remains valid when
|
||||
//! a caller transfers a channel's ArrayBuffer; all copies use intrinsic view
|
||||
//! bounds after argument conversion, never author-overridable JS properties.
|
||||
|
||||
use super::*;
|
||||
use crate::util::callback_data_index_value;
|
||||
|
||||
const CHANNELS: &str = "__moliAudioBufferChannels";
|
||||
const LENGTH: &str = "__moliAudioBufferLength";
|
||||
const SAMPLE_RATE: &str = "__moliAudioBufferSampleRate";
|
||||
|
||||
#[derive(WebApiObject)]
|
||||
#[webapi(interface = "AudioBuffer")]
|
||||
struct AudioBufferData<'s> {
|
||||
#[webapi(slot = CHANNELS)]
|
||||
channels: v8::Local<'s, v8::Array>,
|
||||
#[webapi(slot = LENGTH)]
|
||||
length: u32,
|
||||
#[webapi(slot = SAMPLE_RATE)]
|
||||
sample_rate: f64,
|
||||
}
|
||||
|
||||
#[derive(webidl::WebIdlDictionary)]
|
||||
#[webidl(prefix = "AudioBufferOptions")]
|
||||
struct AudioBufferOptions {
|
||||
// WebIDL dictionary members are converted in lexicographic order.
|
||||
#[webidl(required)]
|
||||
length: u32,
|
||||
#[webidl(default = 1)]
|
||||
number_of_channels: u32,
|
||||
#[webidl(required, converter = "double")]
|
||||
sample_rate: f64,
|
||||
}
|
||||
|
||||
#[derive(webidl::WebIdlArgs)]
|
||||
#[webidl(prefix = "AudioBuffer")]
|
||||
struct ConstructorArgs<'s> {
|
||||
#[webidl(required)]
|
||||
options: v8::Local<'s, v8::Object>,
|
||||
}
|
||||
|
||||
#[derive(webidl::WebIdlArgs)]
|
||||
#[webidl(prefix = "BaseAudioContext.createBuffer")]
|
||||
struct CreateBufferArgs {
|
||||
#[webidl(required)]
|
||||
number_of_channels: u32,
|
||||
#[webidl(required)]
|
||||
length: u32,
|
||||
#[webidl(required, converter = "double")]
|
||||
sample_rate: f64,
|
||||
}
|
||||
|
||||
#[derive(webidl::WebIdlArgs)]
|
||||
#[webidl(prefix = "AudioBuffer.getChannelData")]
|
||||
struct GetChannelDataArgs {
|
||||
#[webidl(required)]
|
||||
channel: u32,
|
||||
}
|
||||
|
||||
#[derive(webidl::WebIdlArgs)]
|
||||
#[webidl(prefix = "AudioBuffer channel copy")]
|
||||
struct CopyChannelArgs {
|
||||
#[webidl(index = 1, required)]
|
||||
channel: u32,
|
||||
#[webidl(index = 2, default = 0)]
|
||||
offset: u32,
|
||||
}
|
||||
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
#[webapi(name = "AudioBuffer", constructor_callback = constructor, constructor_length = 1, enumerable)]
|
||||
struct AudioBufferTemplate {
|
||||
#[webapi(accessor_property, getter = metadata, data = callback_data_index_value(scope, 0))]
|
||||
length: (),
|
||||
#[webapi(accessor_property, getter = metadata, data = callback_data_index_value(scope, 1))]
|
||||
duration: (),
|
||||
#[webapi(accessor_property, getter = metadata, data = callback_data_index_value(scope, 2))]
|
||||
sample_rate: (),
|
||||
#[webapi(accessor_property, getter = metadata, data = callback_data_index_value(scope, 3))]
|
||||
number_of_channels: (),
|
||||
#[webapi(method, length = 1, callback = get_channel_data)]
|
||||
get_channel_data: (),
|
||||
#[webapi(method, length = 2, callback = copy_from_channel)]
|
||||
copy_from_channel: (),
|
||||
#[webapi(method, length = 2, callback = copy_to_channel)]
|
||||
copy_to_channel: (),
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap) fn build_constructor_template<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_, ()>,
|
||||
) -> v8::Local<'s, v8::FunctionTemplate> {
|
||||
AudioBufferTemplate::build(scope)
|
||||
}
|
||||
|
||||
fn constructor<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'s, v8::Value>,
|
||||
) {
|
||||
if !args.is_construct_call() {
|
||||
throw_type_error(
|
||||
scope,
|
||||
"Failed to construct 'AudioBuffer': Please use the 'new' operator.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
let Some(parsed) = webidl::parse_args::<ConstructorArgs>(scope, &args) else {
|
||||
return;
|
||||
};
|
||||
let options = match webidl::parse_dictionary_object::<AudioBufferOptions>(scope, parsed.options)
|
||||
{
|
||||
Ok(options) => options,
|
||||
Err(error) => {
|
||||
webidl::throw_error(scope, &error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let Some(data) = allocate(
|
||||
scope,
|
||||
options.number_of_channels,
|
||||
options.length,
|
||||
options.sample_rate,
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
// Initialize the object allocated for newTarget, preserving subclasses.
|
||||
data.initialize(scope, args.this())
|
||||
.expect("AudioBuffer should initialize");
|
||||
rv.set(args.this().into());
|
||||
}
|
||||
|
||||
pub(super) fn create_buffer<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'s, v8::Value>,
|
||||
) {
|
||||
if !require_base_audio_context(scope, args.this()) {
|
||||
return;
|
||||
}
|
||||
let Some(parsed) = webidl::parse_args::<CreateBufferArgs>(scope, &args) else {
|
||||
return;
|
||||
};
|
||||
if let Some(buffer) = new_buffer(
|
||||
scope,
|
||||
parsed.number_of_channels,
|
||||
parsed.length,
|
||||
parsed.sample_rate,
|
||||
) {
|
||||
rv.set(buffer.into());
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn new_buffer<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
number_of_channels: u32,
|
||||
length: u32,
|
||||
sample_rate: f64,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
let data = allocate(scope, number_of_channels, length, sample_rate)?;
|
||||
Some(data.bind(scope).expect("AudioBuffer should bind"))
|
||||
}
|
||||
|
||||
fn allocate<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
number_of_channels: u32,
|
||||
length: u32,
|
||||
sample_rate: f64,
|
||||
) -> Option<AudioBufferData<'s>> {
|
||||
// AudioBufferOptions uses restricted float, not double. Round before the
|
||||
// algorithm's range checks, including values near the supported endpoints.
|
||||
let sample_rate = sample_rate as f32 as f64;
|
||||
if !sample_rate.is_finite() {
|
||||
throw_type_error(scope, "AudioBuffer sampleRate must be a finite float.");
|
||||
return None;
|
||||
}
|
||||
if !(1..=32).contains(&number_of_channels)
|
||||
|| !(3000.0..=768_000.0).contains(&sample_rate)
|
||||
|| length == 0
|
||||
{
|
||||
throw_dom_exception(
|
||||
scope,
|
||||
"NotSupportedError",
|
||||
9,
|
||||
"AudioBuffer requires 1 to 32 channels, a positive length, and a sample rate between 3000 and 768000.",
|
||||
);
|
||||
return None;
|
||||
}
|
||||
let Some(byte_length) = (length as usize)
|
||||
.checked_mul(size_of::<f32>())
|
||||
.filter(|_| length as usize <= v8::Float32Array::MAX_LENGTH)
|
||||
else {
|
||||
allocation_failed(scope);
|
||||
return None;
|
||||
};
|
||||
let mut channels = Vec::with_capacity(number_of_channels as usize);
|
||||
for _ in 0..number_of_channels {
|
||||
let mut bytes = Vec::new();
|
||||
if bytes.try_reserve_exact(byte_length).is_err() {
|
||||
allocation_failed(scope);
|
||||
return None;
|
||||
}
|
||||
bytes.resize(byte_length, 0);
|
||||
let store = v8::ArrayBuffer::new_backing_store_from_vec(bytes).make_shared();
|
||||
let buffer = v8::ArrayBuffer::with_backing_store(scope, &store);
|
||||
let Some(view) = v8::Float32Array::new(scope, buffer, 0, length as usize) else {
|
||||
allocation_failed(scope);
|
||||
return None;
|
||||
};
|
||||
channels.push(view.into());
|
||||
}
|
||||
// Define own dense elements directly. Array.prototype setters must not see
|
||||
// this internal collection, nor can callers obtain or mutate it.
|
||||
let channels = v8::Array::new_with_elements(scope, &channels);
|
||||
Some(AudioBufferData::new(channels, length, sample_rate))
|
||||
}
|
||||
|
||||
fn allocation_failed(scope: &mut v8::PinScope<'_, '_>) {
|
||||
throw_dom_exception(
|
||||
scope,
|
||||
"NotSupportedError",
|
||||
9,
|
||||
"Unable to allocate AudioBuffer channel data.",
|
||||
);
|
||||
}
|
||||
|
||||
fn require_channels<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
receiver: v8::Local<'s, v8::Object>,
|
||||
) -> Option<v8::Local<'s, v8::Array>> {
|
||||
let channels = web_audio_array_slot(scope, receiver, CHANNELS);
|
||||
if channels.is_none() {
|
||||
throw_type_error(scope, "Illegal invocation: expected an AudioBuffer.");
|
||||
}
|
||||
channels
|
||||
}
|
||||
|
||||
fn channel_view<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
channels: v8::Local<'s, v8::Array>,
|
||||
channel: u32,
|
||||
) -> Option<v8::Local<'s, v8::Float32Array>> {
|
||||
if channel >= channels.length() {
|
||||
throw_dom_exception(
|
||||
scope,
|
||||
"IndexSizeError",
|
||||
1,
|
||||
"AudioBuffer channel index is out of range.",
|
||||
);
|
||||
return None;
|
||||
}
|
||||
channels
|
||||
.get_index(scope, channel)
|
||||
.and_then(|value| v8::Local::<v8::Float32Array>::try_from(value).ok())
|
||||
}
|
||||
|
||||
fn metadata<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'s, v8::Value>,
|
||||
) {
|
||||
let Some(channels) = require_channels(scope, args.this()) else {
|
||||
return;
|
||||
};
|
||||
let length = web_audio_number_slot(scope, args.this(), LENGTH).expect("AudioBuffer length");
|
||||
let sample_rate =
|
||||
web_audio_number_slot(scope, args.this(), SAMPLE_RATE).expect("AudioBuffer sample rate");
|
||||
let value = match args.data().int32_value(scope) {
|
||||
Some(0) => length,
|
||||
Some(1) => length / sample_rate,
|
||||
Some(2) => sample_rate,
|
||||
Some(3) => channels.length() as f64,
|
||||
_ => unreachable!("AudioBuffer metadata field"),
|
||||
};
|
||||
rv.set(v8::Number::new(scope, value).into());
|
||||
}
|
||||
|
||||
fn get_channel_data<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'s, v8::Value>,
|
||||
) {
|
||||
let Some(channels) = require_channels(scope, args.this()) else {
|
||||
return;
|
||||
};
|
||||
let Some(parsed) = webidl::parse_args::<GetChannelDataArgs>(scope, &args) else {
|
||||
return;
|
||||
};
|
||||
if let Some(view) = channel_view(scope, channels, parsed.channel) {
|
||||
rv.set(view.into());
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_from_channel<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
_rv: v8::ReturnValue<'s, v8::Value>,
|
||||
) {
|
||||
copy_channel(scope, &args, true);
|
||||
}
|
||||
|
||||
fn copy_to_channel<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
_rv: v8::ReturnValue<'s, v8::Value>,
|
||||
) {
|
||||
copy_channel(scope, &args, false);
|
||||
}
|
||||
|
||||
fn copy_channel<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: &v8::FunctionCallbackArguments<'s>,
|
||||
from_channel: bool,
|
||||
) {
|
||||
let Some(channels) = require_channels(scope, args.this()) else {
|
||||
return;
|
||||
};
|
||||
if args.length() < 2 {
|
||||
throw_type_error(scope, "AudioBuffer channel copy requires two arguments.");
|
||||
return;
|
||||
}
|
||||
let Ok(array) = v8::Local::<v8::Float32Array>::try_from(args.get(0)) else {
|
||||
throw_type_error(scope, "AudioBuffer channel copy requires a Float32Array.");
|
||||
return;
|
||||
};
|
||||
if let Some(store) = array.get_backing_store()
|
||||
&& (store.is_shared() || store.is_resizable_by_user_javascript())
|
||||
{
|
||||
throw_type_error(
|
||||
scope,
|
||||
"AudioBuffer channel copy requires a non-shared, fixed-length buffer.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Numeric coercion can transfer either view's backing buffer. Acquire the
|
||||
// intrinsic lengths and stores only after every argument has been converted.
|
||||
let Some(parsed) = webidl::parse_args::<CopyChannelArgs>(scope, args) else {
|
||||
return;
|
||||
};
|
||||
if array.length() == 0 {
|
||||
return;
|
||||
}
|
||||
let Some(channel) = channel_view(scope, channels, parsed.channel) else {
|
||||
return;
|
||||
};
|
||||
let offset = parsed.offset as usize;
|
||||
let length = array.length().min(channel.length().saturating_sub(offset));
|
||||
if length == 0 {
|
||||
return;
|
||||
}
|
||||
let Some(channel_store) = channel.get_backing_store() else {
|
||||
return;
|
||||
};
|
||||
let Some(array_store) = array.get_backing_store() else {
|
||||
return;
|
||||
};
|
||||
let Some(channel_data) = channel_store.data() else {
|
||||
return;
|
||||
};
|
||||
let Some(array_data) = array_store.data() else {
|
||||
return;
|
||||
};
|
||||
// SAFETY: both stores are live and non-shared, and intrinsic view lengths
|
||||
// bound these ranges. No JS runs between the bounds check and copy. `copy`
|
||||
// (memmove) also handles views overlapping the same channel's backing store.
|
||||
unsafe {
|
||||
let channel_ptr = channel_data
|
||||
.as_ptr()
|
||||
.cast::<u8>()
|
||||
.add(channel.byte_offset() + offset * size_of::<f32>());
|
||||
let array_ptr = array_data.as_ptr().cast::<u8>().add(array.byte_offset());
|
||||
let (src, dst) = if from_channel {
|
||||
(channel_ptr, array_ptr)
|
||||
} else {
|
||||
(array_ptr, channel_ptr)
|
||||
};
|
||||
std::ptr::copy(src, dst, length * size_of::<f32>());
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn write_channel<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
buffer: v8::Local<'s, v8::Object>,
|
||||
channel: u32,
|
||||
samples: &[f32],
|
||||
) -> Option<()> {
|
||||
let channels = require_channels(scope, buffer)?;
|
||||
let view = channel_view(scope, channels, channel)?;
|
||||
let length = samples.len().min(view.length());
|
||||
if length == 0 {
|
||||
return Some(());
|
||||
}
|
||||
let store = view.get_backing_store()?;
|
||||
let data = store.data()?;
|
||||
// SAFETY: newly allocated channels are fixed, non-shared buffers. The source
|
||||
// is independent Rust-owned PCM data and length is bounded by both ranges.
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(
|
||||
samples.as_ptr().cast::<u8>(),
|
||||
data.as_ptr().cast::<u8>().add(view.byte_offset()),
|
||||
length * size_of::<f32>(),
|
||||
);
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn audio_buffer_public_api_matches_chromium_fixture() {
|
||||
let mut vm = new_storage_test_vm("https://audio-buffer.test/");
|
||||
let source = include_str!("../../../../tests/fixtures/audio-buffer.js");
|
||||
let result = vm
|
||||
.eval(&format!("JSON.stringify({source})"))
|
||||
.expect("AudioBuffer fixture");
|
||||
let result: serde_json::Value = serde_json::from_str(&result).expect("fixture JSON");
|
||||
assert_eq!(
|
||||
result,
|
||||
serde_json::json!({
|
||||
"construction": "ok",
|
||||
"metadata": "ok",
|
||||
"channels": "ok",
|
||||
"copy": "ok",
|
||||
"numeric_conversion": "ok",
|
||||
"bitwise_storage": "ok",
|
||||
"receivers_and_argument_order": "ok",
|
||||
"detached_views": "ok",
|
||||
"context_factory": "ok",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_buffer_offline_result_shares_channel_views_and_copy_storage() {
|
||||
let mut vm = new_storage_test_vm("https://audio-buffer-render.test/");
|
||||
vm.exec(r#"
|
||||
globalThis.bufferResults = [];
|
||||
for (const connected of [false, true]) {
|
||||
const context = new OfflineAudioContext(2, 32, 8000);
|
||||
if (connected) {
|
||||
const oscillator = context.createOscillator();
|
||||
oscillator.connect(context.destination);
|
||||
oscillator.start();
|
||||
}
|
||||
context.startRendering().then(buffer => {
|
||||
const channels = [buffer.getChannelData(0), buffer.getChannelData(1)];
|
||||
const initialCopiesMatch = channels.every((channel, index) => {
|
||||
const copy = new Float32Array(32);
|
||||
buffer.copyFromChannel(copy, index);
|
||||
return copy.every((value, i) => value === channel[i]);
|
||||
});
|
||||
const initiallySilent = channels.every(channel => channel.every(x => x === 0));
|
||||
const oldSecondValue = channels[1][1];
|
||||
buffer.copyToChannel(new Float32Array([10, 20]), 0, 1);
|
||||
const copy = new Float32Array(3).fill(99);
|
||||
buffer.copyFromChannel(copy, 0, 30);
|
||||
bufferResults.push({
|
||||
connected,
|
||||
metadata: [buffer.numberOfChannels, buffer.length, buffer.sampleRate, buffer.duration],
|
||||
prototype: Object.getPrototypeOf(buffer) === AudioBuffer.prototype,
|
||||
initialCopiesMatch,
|
||||
initiallySilent,
|
||||
sharedView: buffer.getChannelData(0) === channels[0] && channels[0][1] === 10 && channels[0][2] === 20,
|
||||
independentChannels: channels[1][1] === oldSecondValue,
|
||||
untouchedSuffix: copy[2] === 99
|
||||
});
|
||||
});
|
||||
}
|
||||
"#, None).expect("render AudioBuffer outputs");
|
||||
let result = vm
|
||||
.eval("JSON.stringify(bufferResults)")
|
||||
.expect("completed render results");
|
||||
let result: serde_json::Value = serde_json::from_str(&result).expect("render JSON");
|
||||
assert_eq!(
|
||||
result,
|
||||
serde_json::json!([
|
||||
{"connected": false, "metadata": [2,32,8000,0.004], "prototype": true,
|
||||
"initialCopiesMatch": true, "initiallySilent": true, "sharedView": true,
|
||||
"independentChannels": true, "untouchedSuffix": true},
|
||||
{"connected": true, "metadata": [2,32,8000,0.004], "prototype": true,
|
||||
"initialCopiesMatch": true, "initiallySilent": false, "sharedView": true,
|
||||
"independentChannels": true, "untouchedSuffix": true}
|
||||
])
|
||||
);
|
||||
}
|
||||
@@ -2875,7 +2875,7 @@ fn web_audio_numeric_entrypoints_parse_webidl_arguments() {
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
r#"{"ctorMissing":"TypeError","ctorSymbol":"TypeError","length":16,"sampleRate":44100,"setMissingTime":"TypeError","setSymbol":"TypeError","frequency":123.5,"channelMissing":"TypeError","channelSymbol":"TypeError","channelRange":"RangeError","channel0":"[object Float32Array]"}"#
|
||||
r#"{"ctorMissing":"TypeError","ctorSymbol":"TypeError","length":16,"sampleRate":44100,"setMissingTime":"TypeError","setSymbol":"TypeError","frequency":123.5,"channelMissing":"TypeError","channelSymbol":"TypeError","channelRange":"IndexSizeError","channel0":"[object Float32Array]"}"#
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::*;
|
||||
|
||||
mod audio_buffer;
|
||||
mod broadcast_channel;
|
||||
mod chrome;
|
||||
mod console;
|
||||
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
(() => {
|
||||
const results = {};
|
||||
const check = (condition, message) => { if (!condition) throw new Error(message); };
|
||||
const equal = (actual, expected, message) => check(JSON.stringify(actual) === JSON.stringify(expected), message);
|
||||
const throws = (name, fn) => {
|
||||
try { fn(); } catch (error) { check(error.name === name, `expected ${name}, got ${error.name}`); return; }
|
||||
throw new Error(`expected ${name}`);
|
||||
};
|
||||
const run = (name, fn) => {
|
||||
try { fn(); results[name] = 'ok'; }
|
||||
catch (error) { results[name] = {name: error.name, message: error.message}; }
|
||||
};
|
||||
|
||||
run('construction', () => {
|
||||
const buffer = new AudioBuffer({length: 8, sampleRate: 8000});
|
||||
check(buffer instanceof AudioBuffer, 'brand');
|
||||
check(Object.prototype.toString.call(buffer) === '[object AudioBuffer]', 'tag');
|
||||
equal([buffer.length, buffer.sampleRate, buffer.duration, buffer.numberOfChannels], [8, 8000, .001, 1], 'metadata');
|
||||
check(Object.getOwnPropertyNames(buffer).length === 0, 'no public backing fields');
|
||||
check(AudioBuffer.length === 1, 'constructor arity');
|
||||
class Derived extends AudioBuffer {}
|
||||
check(Object.getPrototypeOf(new Derived({length: 1, sampleRate: 44100})) === Derived.prototype, 'newTarget prototype');
|
||||
throws('TypeError', () => AudioBuffer({length: 8, sampleRate: 8000}));
|
||||
for (const options of [undefined, null, 1, {}, {length: 8}, {sampleRate: 8000}]) {
|
||||
throws('TypeError', () => new AudioBuffer(options));
|
||||
}
|
||||
});
|
||||
|
||||
run('metadata', () => {
|
||||
const buffer = new AudioBuffer({length: 8, sampleRate: 8000, numberOfChannels: 2});
|
||||
for (const name of ['length', 'sampleRate', 'duration', 'numberOfChannels']) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(AudioBuffer.prototype, name);
|
||||
check(descriptor.enumerable && descriptor.configurable, `${name} flags`);
|
||||
check(typeof descriptor.get === 'function' && descriptor.set === undefined, `${name} readonly getter`);
|
||||
check(descriptor.get.name === `get ${name}` && descriptor.get.length === 0, `${name} function metadata`);
|
||||
check(Reflect.set(buffer, name, 100) === false, `${name} write`);
|
||||
for (const receiver of [{}, AudioBuffer.prototype, Object.create(buffer), new Proxy(buffer, {})]) {
|
||||
throws('TypeError', () => descriptor.get.call(receiver));
|
||||
}
|
||||
}
|
||||
for (const [name, length] of [['getChannelData', 1], ['copyFromChannel', 2], ['copyToChannel', 2]]) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(AudioBuffer.prototype, name);
|
||||
check(descriptor.enumerable && descriptor.configurable && descriptor.writable, `${name} flags`);
|
||||
check(descriptor.value.name === name && descriptor.value.length === length, `${name} function metadata`);
|
||||
check(!Object.hasOwn(buffer, name), `${name} not per-instance`);
|
||||
}
|
||||
});
|
||||
|
||||
run('channels', () => {
|
||||
const buffer = new AudioBuffer({length: 8, sampleRate: 8000, numberOfChannels: 3});
|
||||
const channels = [0, 1, 2].map(i => buffer.getChannelData(i));
|
||||
for (let i = 0; i < channels.length; i++) {
|
||||
check(channels[i] instanceof Float32Array && channels[i].length === 8, 'float32 channel');
|
||||
check(channels[i] === buffer.getChannelData(i), 'stable view identity');
|
||||
check(channels[i].every(x => x === 0), 'zero initialized');
|
||||
}
|
||||
channels[0][0] = 1;
|
||||
channels[1][1] = 2;
|
||||
equal(channels.map(x => [x[0], x[1]]), [[1, 0], [0, 2], [0, 0]], 'independent channels');
|
||||
buffer.__moliOfflineAudioBuffer = new Float32Array([99]);
|
||||
buffer.__moliAudioBufferChannels = [new Float32Array([99])];
|
||||
Object.defineProperty(buffer, 'length', {value: 99});
|
||||
check(buffer.getChannelData(0) === channels[0] && channels[0].length === 8, 'private storage');
|
||||
});
|
||||
|
||||
run('copy', () => {
|
||||
const buffer = new AudioBuffer({length: 6, sampleRate: 8000, numberOfChannels: 2});
|
||||
const source = new Float32Array([90, 1, 2, 3, 4, 91]);
|
||||
check(buffer.copyToChannel(source.subarray(1, 5), 1, 2) === undefined, 'copyTo return');
|
||||
equal([...buffer.getChannelData(1)], [0, 0, 1, 2, 3, 4], 'source view and offset');
|
||||
check(buffer.getChannelData(0).every(x => x === 0), 'other channel unchanged');
|
||||
const target = new Float32Array(8).fill(99);
|
||||
check(buffer.copyFromChannel(target.subarray(1, 7), 1, 2) === undefined, 'copyFrom return');
|
||||
equal([...target], [99, 1, 2, 3, 4, 99, 99, 99], 'destination view and untouched suffix');
|
||||
buffer.copyToChannel(new Float32Array([5, 6]), 1, 5);
|
||||
equal([...buffer.getChannelData(1)], [0, 0, 1, 2, 3, 5], 'clamped write');
|
||||
buffer.copyFromChannel(target, 1, 6);
|
||||
buffer.copyToChannel(source, 1, 6);
|
||||
buffer.copyToChannel(source, 1, -1);
|
||||
equal([...target], [99, 1, 2, 3, 4, 99, 99, 99], 'offset at/past end is no-op');
|
||||
const channel = buffer.getChannelData(0);
|
||||
channel.set([1, 2, 3, 4, 5, 6]);
|
||||
buffer.copyToChannel(channel.subarray(0, 4), 0, 1);
|
||||
equal([...channel], [1, 1, 2, 3, 4, 6], 'overlapping copyTo');
|
||||
channel.set([1, 2, 3, 4, 5, 6]);
|
||||
buffer.copyFromChannel(channel.subarray(1), 0);
|
||||
equal([...channel], [1, 1, 2, 3, 4, 5], 'overlapping copyFrom');
|
||||
});
|
||||
|
||||
run('numeric_conversion', () => {
|
||||
const buffer = new AudioBuffer({length: '4.9', numberOfChannels: 2 ** 32 + 2, sampleRate: 8000.123456});
|
||||
equal([buffer.length, buffer.numberOfChannels, buffer.sampleRate], [4, 2, Math.fround(8000.123456)], 'WebIDL conversion');
|
||||
const zero = buffer.getChannelData(0);
|
||||
for (const channel of [null, undefined, NaN, Infinity, 2 ** 32, '0.9']) {
|
||||
check(buffer.getChannelData(channel) === zero, 'unsigned long channel');
|
||||
}
|
||||
for (const channel of [-1, 2]) throws('IndexSizeError', () => buffer.getChannelData(channel));
|
||||
for (const channel of [Symbol(), 1n]) throws('TypeError', () => buffer.getChannelData(channel));
|
||||
throws('TypeError', () => buffer.getChannelData());
|
||||
for (const length of [0, 2 ** 32]) throws('NotSupportedError', () => new AudioBuffer({length, sampleRate: 8000}));
|
||||
for (const numberOfChannels of [0, 33, -1]) throws('NotSupportedError', () => new AudioBuffer({length: 1, sampleRate: 8000, numberOfChannels}));
|
||||
for (const sampleRate of [0, 2999, 768001]) throws('NotSupportedError', () => new AudioBuffer({length: 1, sampleRate}));
|
||||
for (const sampleRate of [NaN, Infinity, -Infinity, 1e40, Symbol(), 1n]) throws('TypeError', () => new AudioBuffer({length: 1, sampleRate}));
|
||||
check(new AudioBuffer({length: 1, sampleRate: 2999.99999}).sampleRate === 3000, 'float conversion precedes range check');
|
||||
check(new AudioBuffer({length: 1, sampleRate: 768000}).sampleRate === 768000, 'sample rate upper bound');
|
||||
});
|
||||
|
||||
run('bitwise_storage', () => {
|
||||
const buffer = new AudioBuffer({length: 4, sampleRate: 8000});
|
||||
const source = new Float32Array(4);
|
||||
const bits = [0x80000000, 0x7fc00001, 0xff800000, 0x00000001];
|
||||
new Uint32Array(source.buffer).set(bits);
|
||||
buffer.copyToChannel(source, 0);
|
||||
const channel = buffer.getChannelData(0);
|
||||
equal([...new Uint32Array(channel.buffer)], bits, 'preserve sample bits');
|
||||
const copy = new Float32Array(4);
|
||||
Object.defineProperty(copy, 'length', {value: 0});
|
||||
Object.defineProperty(copy, 'byteOffset', {value: 100000});
|
||||
buffer.copyFromChannel(copy, 0);
|
||||
equal([...new Uint32Array(copy.buffer)], bits, 'intrinsic destination bounds');
|
||||
const previous = Object.getOwnPropertyDescriptor(Array.prototype, '0');
|
||||
try {
|
||||
Object.defineProperty(Array.prototype, '0', {
|
||||
configurable: true,
|
||||
get() { throw new Error('internal channel read invoked page getter'); },
|
||||
set() { throw new Error('internal channel write invoked page setter'); }
|
||||
});
|
||||
const clean = new AudioBuffer({length: 4, sampleRate: 8000});
|
||||
check(clean.getChannelData(0)[0] === 0, 'own channel storage');
|
||||
} finally {
|
||||
if (previous) Object.defineProperty(Array.prototype, '0', previous);
|
||||
else delete Array.prototype[0];
|
||||
}
|
||||
});
|
||||
|
||||
run('receivers_and_argument_order', () => {
|
||||
const buffer = new AudioBuffer({length: 4, sampleRate: 8000});
|
||||
const array = new Float32Array(4);
|
||||
let conversions = 0;
|
||||
const channel = {valueOf() { conversions++; return 0; }};
|
||||
for (const name of ['getChannelData', 'copyFromChannel', 'copyToChannel']) {
|
||||
const args = name === 'getChannelData' ? [channel] : [array, channel];
|
||||
for (const receiver of [{}, AudioBuffer.prototype, Object.create(buffer), new Proxy(buffer, {})]) {
|
||||
throws('TypeError', () => buffer[name].apply(receiver, args));
|
||||
}
|
||||
}
|
||||
check(conversions === 0, 'brand check before conversion');
|
||||
for (const name of ['copyFromChannel', 'copyToChannel']) {
|
||||
for (const bad of [undefined, null, [], new Float64Array(4), new Proxy(array, {})]) {
|
||||
throws('TypeError', () => buffer[name](bad, channel));
|
||||
}
|
||||
check(conversions === 0, 'typed array check before channel conversion');
|
||||
throws('TypeError', () => buffer[name](array));
|
||||
throws('IndexSizeError', () => buffer[name](array, 1));
|
||||
check(buffer[name](new Float32Array(0), 99) === undefined, 'empty view returns before range check');
|
||||
const rab = new ArrayBuffer(16, {maxByteLength: 32});
|
||||
throws('TypeError', () => buffer[name](new Float32Array(rab), 0));
|
||||
if (typeof SharedArrayBuffer === 'function') {
|
||||
throws('TypeError', () => buffer[name](new Float32Array(new SharedArrayBuffer(16)), 0));
|
||||
}
|
||||
}
|
||||
const order = [];
|
||||
new AudioBuffer({
|
||||
get sampleRate() { order.push('sampleRate'); return 8000; },
|
||||
get numberOfChannels() { order.push('numberOfChannels'); return 1; },
|
||||
get length() { order.push('length'); return 4; }
|
||||
});
|
||||
equal(order, ['length', 'numberOfChannels', 'sampleRate'], 'dictionary member order');
|
||||
const sentinel = new Error('sentinel');
|
||||
try { new AudioBuffer({get length() { throw sentinel; }}); throw new Error('missing exception'); }
|
||||
catch (error) { check(error === sentinel, 'preserve getter exception'); }
|
||||
try { buffer.copyFromChannel(array, {valueOf() { throw sentinel; }}); throw new Error('missing exception'); }
|
||||
catch (error) { check(error === sentinel, 'preserve argument exception'); }
|
||||
});
|
||||
|
||||
run('detached_views', () => {
|
||||
const buffer = new AudioBuffer({length: 4, sampleRate: 8000});
|
||||
const channel = buffer.getChannelData(0);
|
||||
channel.set([1, 2, 3, 4]);
|
||||
structuredClone(channel.buffer, {transfer: [channel.buffer]});
|
||||
check(buffer.getChannelData(0) === channel && channel.length === 0, 'retain detached channel view');
|
||||
equal([buffer.length, buffer.duration, buffer.numberOfChannels], [4, .0005, 1], 'detaching does not change metadata');
|
||||
const target = new Float32Array([9, 9, 9, 9]);
|
||||
buffer.copyFromChannel(target, 0);
|
||||
equal([...target], [9, 9, 9, 9], 'detached source is empty');
|
||||
buffer.copyToChannel(target, 0);
|
||||
const second = new AudioBuffer({length: 4, sampleRate: 8000});
|
||||
second.copyFromChannel(target, {valueOf() { structuredClone(target.buffer, {transfer: [target.buffer]}); return 0; }});
|
||||
check(target.length === 0, 'argument conversion may detach destination');
|
||||
});
|
||||
|
||||
run('context_factory', () => {
|
||||
const contexts = [new AudioContext(), new OfflineAudioContext(1, 4, 8000)];
|
||||
const method = BaseAudioContext.prototype.createBuffer;
|
||||
check(method.length === 3, 'factory arity');
|
||||
for (const context of contexts) {
|
||||
check(context.createBuffer === method, 'shared factory');
|
||||
const buffer = context.createBuffer(2, 16, 16000);
|
||||
equal([buffer.numberOfChannels, buffer.length, buffer.sampleRate, buffer.duration], [2, 16, 16000, .001], 'factory metadata');
|
||||
check(buffer instanceof AudioBuffer && buffer.getChannelData(1).every(x => x === 0), 'factory channel storage');
|
||||
throws('TypeError', () => method.call({}, 1, 4, 8000));
|
||||
throws('TypeError', () => method.call(context, 1, 4));
|
||||
throws('NotSupportedError', () => method.call(context, 0, 4, 8000));
|
||||
}
|
||||
contexts[0].close();
|
||||
});
|
||||
|
||||
return results;
|
||||
})()
|
||||
Reference in New Issue
Block a user