From 9942a1bbf045ab7236547bcb2f1a08b32211fa30 Mon Sep 17 00:00:00 2001 From: b-ma Date: Mon, 26 Feb 2024 17:00:04 +0100 Subject: [PATCH 01/12] feat: wrap AudioBuffer with JS proxy --- Cargo.toml | 4 +- generator/js/BaseAudioContext.mixin.tmpl.js | 5 ++ generator/js/monkey-patch.tmpl.js | 1 + js/AudioBuffer.js | 60 +++++++++++++++++++++ js/BaseAudioContext.mixin.js | 5 ++ js/lib/cast.js | 20 +++++++ js/monkey-patch.js | 1 + tests/cast.spec.mjs | 37 +++++++++++++ tests/wpt.mjs | 7 ++- 9 files changed, 136 insertions(+), 4 deletions(-) create mode 100644 js/AudioBuffer.js create mode 100644 js/lib/cast.js create mode 100644 tests/cast.spec.mjs diff --git a/Cargo.toml b/Cargo.toml index c9a2a507..776ebc2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,8 +13,8 @@ crate-type = ["cdylib"] napi = {version="2.15", features=["napi9", "tokio_rt"]} napi-derive = "2.15" uuid = {version="1.6.1", features = ["v4", "fast-rng"]} -web-audio-api = "1.0.0-rc.1" -# web-audio-api = { path = "../web-audio-api-rs" } +# web-audio-api = "1.0.0-rc.1" +web-audio-api = { path = "../web-audio-api-rs" } [target.'cfg(all(any(windows, unix), target_arch = "x86_64", not(target_env = "musl")))'.dependencies] mimalloc = {version = "0.1"} diff --git a/generator/js/BaseAudioContext.mixin.tmpl.js b/generator/js/BaseAudioContext.mixin.tmpl.js index 4a91c68c..5da7f5b7 100644 --- a/generator/js/BaseAudioContext.mixin.tmpl.js +++ b/generator/js/BaseAudioContext.mixin.tmpl.js @@ -4,6 +4,7 @@ const { isFunction } = require('./lib/utils.js'); module.exports = (superclass, bindings) => { const { ${d.nodes.map(n => ` ${d.name(n)},`).join('\n')} + AudioBuffer, PeriodicWave, } = bindings; @@ -40,6 +41,10 @@ ${d.nodes.map(n => ` ${d.name(n)},`).join('\n')} } } + createBuffer(numberOfChannels, length, sampleRate) { + return new AudioBuffer({ numberOfChannels, length, sampleRate }); + } + createPeriodicWave(real, imag) { return new PeriodicWave(this, { real, imag }); } diff --git a/generator/js/monkey-patch.tmpl.js b/generator/js/monkey-patch.tmpl.js index a701f206..3bbb1f4c 100644 --- a/generator/js/monkey-patch.tmpl.js +++ b/generator/js/monkey-patch.tmpl.js @@ -9,6 +9,7 @@ ${d.nodes.map((node) => { // @todo - wrap AudioBuffer interface as well nativeBinding.PeriodicWave = require('./PeriodicWave.js')(nativeBinding.PeriodicWave); + nativeBinding.AudioBuffer = require('./AudioBuffer.js')(nativeBinding.AudioBuffer); nativeBinding.AudioContext = require('./AudioContext.js')(nativeBinding); nativeBinding.OfflineAudioContext = require('./OfflineAudioContext.js')(nativeBinding); diff --git a/js/AudioBuffer.js b/js/AudioBuffer.js new file mode 100644 index 00000000..8c9f58bd --- /dev/null +++ b/js/AudioBuffer.js @@ -0,0 +1,60 @@ +const { throwSanitizedError, DOMException } = require('./lib/errors.js'); + +module.exports = (NativeAudioBuffer) => { + class AudioBuffer extends NativeAudioBuffer { + constructor(options) { + if (typeof options !== 'object') { + throw new TypeError("Failed to construct 'AudioBuffer': argument 1 is not of type 'AudioBufferOptions'"); + } + + try { + super(options); + } catch (err) { + throwSanitizedError(err); + } + } + + copyFromChannel(destination, channelNumber, bufferOffset = 0) { + if (!(destination instanceof Float32Array)) { + throw new TypeError(`Failed to execute 'copyFromChannel' on 'AudioBuffer': parameter 1 is not of type 'Float32Array'`); + } + + if (channelNumber < 0) { + throw new DOMException(`Failed to execute 'copyFromChannel' on 'AudioBuffer': channelNumber must equal or greater than 0`, 'IndexSizeError'); + } + + try { + super.copyFromChannel(destination, channelNumber, bufferOffset); + } catch (err) { + throwSanitizedError(err); + } + } + + copyToChannel(source, channelNumber, bufferOffset = 0) { + if (!(source instanceof Float32Array)) { + throw new TypeError(`Failed to execute 'copyToChannel' on 'AudioBuffer': parameter 1 is not of type 'Float32Array'`); + } + + if (channelNumber < 0) { + throw new DOMException(`Failed to execute 'copyToChannel' on 'AudioBuffer': channelNumber must equal or greater than 0`, 'IndexSizeError'); + } + + try { + super.copyToChannel(source, channelNumber, bufferOffset); + } catch (err) { + throwSanitizedError(err); + } + } + + getChannelData(channel) { + try { + return super.getChannelData(channel); + } catch (err) { + throwSanitizedError(err); + } + } + } + + return AudioBuffer; +}; + diff --git a/js/BaseAudioContext.mixin.js b/js/BaseAudioContext.mixin.js index e52ff5dc..e7babdfd 100644 --- a/js/BaseAudioContext.mixin.js +++ b/js/BaseAudioContext.mixin.js @@ -37,6 +37,7 @@ module.exports = (superclass, bindings) => { PannerNode, StereoPannerNode, WaveShaperNode, + AudioBuffer, PeriodicWave, } = bindings; @@ -73,6 +74,10 @@ module.exports = (superclass, bindings) => { } } + createBuffer(numberOfChannels, length, sampleRate) { + return new AudioBuffer({ numberOfChannels, length, sampleRate }); + } + createPeriodicWave(real, imag) { return new PeriodicWave(this, { real, imag }); } diff --git a/js/lib/cast.js b/js/lib/cast.js new file mode 100644 index 00000000..96519f30 --- /dev/null +++ b/js/lib/cast.js @@ -0,0 +1,20 @@ + +exports.toSanitizedSequence = function toSanitizedSequence(data, targetCtor) { + console.warn('toSanitizedSequence: this change the instance, maybe not the right way to do') + if ( + (data.buffer && data.buffer instanceof ArrayBuffer) + || Array.isArray(data) + ) { + data = new targetCtor(data); + } else { + throw new TypeError(`Failed to execute 'copyFromChannel' on 'AudioBuffer': parameter 1 is not of type 'Float32Array'`); + } + + for (let i = 0; i < data.length; i++) { + if (!Number.isFinite(data[i])) { + throw Error(`which one?`); + } + } + + return data; +} diff --git a/js/monkey-patch.js b/js/monkey-patch.js index dd4c016b..e343bd10 100644 --- a/js/monkey-patch.js +++ b/js/monkey-patch.js @@ -40,6 +40,7 @@ module.exports = function monkeyPatch(nativeBinding) { // @todo - wrap AudioBuffer interface as well nativeBinding.PeriodicWave = require('./PeriodicWave.js')(nativeBinding.PeriodicWave); + nativeBinding.AudioBuffer = require('./AudioBuffer.js')(nativeBinding.AudioBuffer); nativeBinding.AudioContext = require('./AudioContext.js')(nativeBinding); nativeBinding.OfflineAudioContext = require('./OfflineAudioContext.js')(nativeBinding); diff --git a/tests/cast.spec.mjs b/tests/cast.spec.mjs new file mode 100644 index 00000000..f08caa67 --- /dev/null +++ b/tests/cast.spec.mjs @@ -0,0 +1,37 @@ +import { assert } from 'chai'; +import { toSanitizedSequence } from '../js/lib/cast.js' + +describe('toSanitizedSequence - Float32Array', () => { + const target = Float32Array; + it('should work with Float32', () => { + const data = new Float32Array([0., 1]); + const result = toSanitizedSequence(data, target); + const expected = new target([0., 1]); + + assert.deepEqual(result, expected); + }); + + it('should work with Float64', () => { + const data = new Float64Array([0., 1]); + const result = toSanitizedSequence(data, target); + const expected = new target([0., 1]); + + assert.deepEqual(result, expected); + }); + + it('should work with Arrays', () => { + const data = [0, 1]; + const result = toSanitizedSequence(data, target); + const expected = new target([0., 1]); + + assert.deepEqual(result, expected); + }); + + it('should throw if item is non finite', () => { + const data = [0., NaN]; + + assert.throws(() => { + toSanitizedSequence(data, target) + }); + }); +}); diff --git a/tests/wpt.mjs b/tests/wpt.mjs index 8d8fb1c1..cf6e1b3f 100644 --- a/tests/wpt.mjs +++ b/tests/wpt.mjs @@ -1,4 +1,4 @@ -import { AudioBufferSourceNode, AnalyserNode, AudioContext, GainNode, OfflineAudioContext, StereoPannerNode, mediaDevices, PeriodicWave } from '../index.mjs'; +import { AudioBufferSourceNode, AnalyserNode, AudioContext, GainNode, OfflineAudioContext, StereoPannerNode, mediaDevices, PeriodicWave, AudioBuffer } from '../index.mjs'; const context = new OfflineAudioContext(2, 1, 48000); // // const node = new AudioBufferSourceNode(context, 42) @@ -10,7 +10,10 @@ const context = new OfflineAudioContext(2, 1, 48000); try { // new OfflineAudioContext({"length":42,"sampleRate":12345}) // new PeriodicWave(context, { real : new Float32Array(8192), imag : new Float32Array(4) }) - context.createPeriodicWave(new Float32Array(512), new Float32Array(4)) + const buffer = context.createBuffer(4, 88200, 44100); + buffer.copyFromChannel(new Float32Array([0, 1]), -1); } catch (err) { console.log(err); } + +await context.startRendering(); From 2284563d3c9ea6b179ecb447a464d32d78cd905a Mon Sep 17 00:00:00 2001 From: b-ma Date: Tue, 27 Feb 2024 18:06:26 +0100 Subject: [PATCH 02/12] refactor: simplify rs code, some checks delegated to JS facade --- src/audio_buffer.rs | 170 +++++++++++++++++++------------------------- wpt | 2 +- 2 files changed, 73 insertions(+), 99 deletions(-) diff --git a/src/audio_buffer.rs b/src/audio_buffer.rs index ae33a674..97efd16a 100644 --- a/src/audio_buffer.rs +++ b/src/audio_buffer.rs @@ -1,7 +1,4 @@ -use napi::{ - noop_finalize, CallContext, Either, Env, JsFunction, JsNumber, JsObject, JsTypedArray, - JsUndefined, Property, Result, TypedArrayType, -}; +use napi::*; use napi_derive::js_function; use std::mem::ManuallyDrop; use web_audio_api::{AudioBuffer, AudioBufferOptions}; @@ -54,58 +51,47 @@ impl NapiAudioBuffer { #[js_function(1)] fn constructor(ctx: CallContext) -> Result { let mut js_this = ctx.this_unchecked::(); + // JS wrapper guarantees we have an option object + let options_js = ctx.get::(0)?; + // created by decodeAudioData (to be cleaned) + if options_js.has_own_property("__internal_caller__")? { + let napi_node = NapiAudioBuffer(None); + ctx.env.wrap(&mut js_this, napi_node)?; + } else { + let some_number_of_channels_js = options_js.get::<&str, JsNumber>("numberOfChannels")?; + let number_of_channels = if let Some(number_of_channels_js) = some_number_of_channels_js { + number_of_channels_js.get_double()? as usize + } else { + 1 + }; - match ctx.try_get::(0)? { - Either::A(options_js) => { - // created by decodeAudioData (to be cleaned) - if options_js.has_own_property("__internal_caller__")? { - let napi_node = NapiAudioBuffer(None); - ctx.env.wrap(&mut js_this, napi_node)?; - } else { - let some_number_of_channels_js = - options_js.get::<&str, JsNumber>("numberOfChannels")?; - let number_of_channels = - if let Some(number_of_channels_js) = some_number_of_channels_js { - number_of_channels_js.get_double()? as usize - } else { - 1 - }; - - let some_length_js = options_js.get::<&str, JsNumber>("length")?; - if some_length_js.is_none() { - return Err(napi::Error::new( - napi::Status::InvalidArg, - "TypeError - Invalid AudioBuffer options: length is required".to_string(), - )); - } - let length = some_length_js.unwrap().get_double()? as usize; - - let some_sample_rate_js = options_js.get::<&str, JsNumber>("sampleRate")?; - if some_sample_rate_js.is_none() { - return Err(napi::Error::new( - napi::Status::InvalidArg, // error code - "TypeError - Invalid AudioBuffer options: sampleRate is required" - .to_string(), - )); - } - let sample_rate = some_sample_rate_js.unwrap().get_double()? as f32; - - let audio_buffer = AudioBuffer::new(AudioBufferOptions { - number_of_channels, - length, - sample_rate, - }); - - let napi_node = NapiAudioBuffer(Some(audio_buffer)); - ctx.env.wrap(&mut js_this, napi_node)? - } - } - Either::B(_) => { + let some_length_js = options_js.get::<&str, JsNumber>("length")?; + if some_length_js.is_none() { return Err(napi::Error::new( napi::Status::InvalidArg, - "TypeError - Invalid AudioBuffer options: options are required".to_string(), + "TypeError - Failed to construct 'AudioBuffer': Failed to read the 'length' property from 'AudioBufferOptions': Required member is undefined.".to_string(), + )); + } + let length = some_length_js.unwrap().get_double()? as usize; + + let some_sample_rate_js = options_js.get::<&str, JsNumber>("sampleRate")?; + if some_sample_rate_js.is_none() { + return Err(napi::Error::new( + napi::Status::InvalidArg, // error code + "TypeError - Failed to construct 'AudioBuffer': Failed to read the 'sampleRate' property from 'AudioBufferOptions': Required member is undefined." + .to_string(), )); } + let sample_rate = some_sample_rate_js.unwrap().get_double()? as f32; + + let audio_buffer = AudioBuffer::new(AudioBufferOptions { + number_of_channels, + length, + sample_rate, + }); + + let napi_node = NapiAudioBuffer(Some(audio_buffer)); + ctx.env.wrap(&mut js_this, napi_node)? } ctx.env.get_undefined() @@ -151,7 +137,41 @@ fn number_of_channels(ctx: CallContext) -> Result { ctx.env.create_double(number_of_channels as f64) } -// #[napi] +#[js_function(3)] +fn copy_to_channel(ctx: CallContext) -> Result { + let js_this = ctx.this_unchecked::(); + let napi_obj = ctx.env.unwrap::(&js_this)?; + let obj = napi_obj.unwrap_mut(); + + let source_js = ctx.get::(0)?.into_value()?; + let source: &[f32] = source_js.as_ref(); + + let channel_number = ctx.get::(1)?.get_double()? as usize; + let offset = ctx.get::(2)?.get_double()? as usize; + + obj.copy_to_channel_with_offset(source, channel_number, offset); + + ctx.env.get_undefined() +} + +#[js_function(3)] +fn copy_from_channel(ctx: CallContext) -> Result { + let js_this = ctx.this_unchecked::(); + let napi_obj = ctx.env.unwrap::(&js_this)?; + let obj = napi_obj.unwrap_mut(); + + let mut dest_js = ctx.get::(0)?.into_value()?; + let dest: &mut [f32] = dest_js.as_mut(); + + let channel_number = ctx.get::(1)?.get_double()? as usize; + let offset = ctx.get::(2)?.get_double()? as usize; + + obj.copy_from_channel_with_offset(dest, channel_number, offset); + + ctx.env.get_undefined() +} + +// # FIXME #[js_function(1)] fn get_channel_data(ctx: CallContext) -> Result { let js_this = ctx.this_unchecked::(); @@ -187,49 +207,3 @@ fn get_channel_data(ctx: CallContext) -> Result { .unwrap() } } - -#[js_function(3)] -fn copy_to_channel(ctx: CallContext) -> Result { - let js_this = ctx.this_unchecked::(); - let napi_obj = ctx.env.unwrap::(&js_this)?; - let obj = napi_obj.unwrap_mut(); - - let source_js = ctx.get::(0)?.into_value()?; - let source: &[f32] = source_js.as_ref(); - - let channel_number = ctx.get::(1)?.get_double()? as usize; - - let some_offset_js: Option = ctx.try_get::(2)?.into(); - let offset = if let Some(offset_js) = some_offset_js { - offset_js.get_double()? as usize - } else { - 0 - }; - - obj.copy_to_channel_with_offset(source, channel_number, offset); - - ctx.env.get_undefined() -} - -#[js_function(3)] -fn copy_from_channel(ctx: CallContext) -> Result { - let js_this = ctx.this_unchecked::(); - let napi_obj = ctx.env.unwrap::(&js_this)?; - let obj = napi_obj.unwrap_mut(); - - let mut dest_js = ctx.get::(0)?.into_value()?; - let dest: &mut [f32] = dest_js.as_mut(); - - let channel_number = ctx.get::(1)?.get_double()? as usize; - - let some_offset_js: Option = ctx.try_get::(2)?.into(); - let offset = if let Some(offset_js) = some_offset_js { - offset_js.get_double()? as usize - } else { - 0 - }; - - obj.copy_from_channel_with_offset(dest, channel_number, offset); - - ctx.env.get_undefined() -} diff --git a/wpt b/wpt index 51c87dc4..7155aa91 160000 --- a/wpt +++ b/wpt @@ -1 +1 @@ -Subproject commit 51c87dc4c5d4a61caef22344e4dba6f5f233ffc3 +Subproject commit 7155aa911c1839131503b16ba44754be110af4d2 From 31ec2a7f7fe2e1525e0a6485c9b6bcddad6b5004 Mon Sep 17 00:00:00 2001 From: b-ma Date: Wed, 6 Mar 2024 15:25:15 +0100 Subject: [PATCH 03/12] testing --- generator/rs/audio_nodes.tmpl.rs | 3 ++- generator/rs/lib.tmpl.rs | 2 +- js/AudioBuffer.js | 32 +++++++++++++++++++++++++++----- src/audio_buffer.rs | 2 +- src/lib.rs | 2 +- 5 files changed, 32 insertions(+), 9 deletions(-) diff --git a/generator/rs/audio_nodes.tmpl.rs b/generator/rs/audio_nodes.tmpl.rs index 441a1f90..1f3e1b3a 100644 --- a/generator/rs/audio_nodes.tmpl.rs +++ b/generator/rs/audio_nodes.tmpl.rs @@ -897,7 +897,8 @@ fn set_${d.slug(attr)}(ctx: CallContext) -> Result { } `; break - case 'interface': + case 'interface': // buffer + console.log(attr); return ` #[js_function(1)] fn set_${d.slug(attr)}(ctx: CallContext) -> Result { diff --git a/generator/rs/lib.tmpl.rs b/generator/rs/lib.tmpl.rs index dd22cd3b..e134d2c4 100644 --- a/generator/rs/lib.tmpl.rs +++ b/generator/rs/lib.tmpl.rs @@ -52,7 +52,7 @@ static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc; #[module_exports] fn init(mut exports: JsObject, env: Env) -> Result<()> { // Do not print panic messages, handle through JS errors - std::panic::set_hook(Box::new(|_panic_info| {})); + // std::panic::set_hook(Box::new(|_panic_info| {})); // Store constructors for factory methods and internal instantiations // Note that we need to create the js class twice so that export and store diff --git a/js/AudioBuffer.js b/js/AudioBuffer.js index 8c9f58bd..5ef27b9a 100644 --- a/js/AudioBuffer.js +++ b/js/AudioBuffer.js @@ -1,19 +1,41 @@ const { throwSanitizedError, DOMException } = require('./lib/errors.js'); +const kNativeAudioBuffer = Symbol('node-web-audio-api:audio-buffer'); + module.exports = (NativeAudioBuffer) => { - class AudioBuffer extends NativeAudioBuffer { + class AudioBuffer { constructor(options) { if (typeof options !== 'object') { throw new TypeError("Failed to construct 'AudioBuffer': argument 1 is not of type 'AudioBufferOptions'"); } + if (options[kNativeAudioBuffer] instanceof NativeAudioBuffer) { + this[kNativeAudioBuffer] = options[kNativeAudioBuffer]; + } + try { - super(options); + this[kNativeAudioBuffer] = new NativeAudioBuffer(options); } catch (err) { throwSanitizedError(err); } } + get sampleRate() { + return this[kNativeAudioBuffer].sampleRate; + } + + get duration() { + return this[kNativeAudioBuffer].duration; + } + + get length() { + return this[kNativeAudioBuffer].length; + } + + get numberOfChannels() { + return this[kNativeAudioBuffer].numberOfChannels; + } + copyFromChannel(destination, channelNumber, bufferOffset = 0) { if (!(destination instanceof Float32Array)) { throw new TypeError(`Failed to execute 'copyFromChannel' on 'AudioBuffer': parameter 1 is not of type 'Float32Array'`); @@ -24,7 +46,7 @@ module.exports = (NativeAudioBuffer) => { } try { - super.copyFromChannel(destination, channelNumber, bufferOffset); + this[kNativeAudioBuffer].copyFromChannel(destination, channelNumber, bufferOffset); } catch (err) { throwSanitizedError(err); } @@ -40,7 +62,7 @@ module.exports = (NativeAudioBuffer) => { } try { - super.copyToChannel(source, channelNumber, bufferOffset); + this[kNativeAudioBuffer].copyToChannel(source, channelNumber, bufferOffset); } catch (err) { throwSanitizedError(err); } @@ -48,7 +70,7 @@ module.exports = (NativeAudioBuffer) => { getChannelData(channel) { try { - return super.getChannelData(channel); + return this[kNativeAudioBuffer].getChannelData(channel); } catch (err) { throwSanitizedError(err); } diff --git a/src/audio_buffer.rs b/src/audio_buffer.rs index 97efd16a..f4617e83 100644 --- a/src/audio_buffer.rs +++ b/src/audio_buffer.rs @@ -171,7 +171,7 @@ fn copy_from_channel(ctx: CallContext) -> Result { ctx.env.get_undefined() } -// # FIXME +// @FIXME - cf. https://github.com/ircam-ismm/node-web-audio-api/issues/80 #[js_function(1)] fn get_channel_data(ctx: CallContext) -> Result { let js_this = ctx.this_unchecked::(); diff --git a/src/lib.rs b/src/lib.rs index 7f0e17b6..f352e886 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -99,7 +99,7 @@ static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc; #[module_exports] fn init(mut exports: JsObject, env: Env) -> Result<()> { // Do not print panic messages, handle through JS errors - std::panic::set_hook(Box::new(|_panic_info| {})); + // std::panic::set_hook(Box::new(|_panic_info| {})); // Store constructors for factory methods and internal instantiations // Note that we need to create the js class twice so that export and store From e0c564b7948c7f5e862c5f6c20e085c8bb0560b0 Mon Sep 17 00:00:00 2001 From: b-ma Date: Wed, 13 Mar 2024 10:58:58 +0100 Subject: [PATCH 04/12] fix: wrap all audio buffers + added tests --- generator/index.mjs | 4 + generator/js/AudioNodes.tmpl.js | 73 +++++++++++--- generator/js/BaseAudioContext.mixin.tmpl.js | 4 +- generator/js/monkey-patch.tmpl.js | 5 +- generator/rs/audio_nodes.tmpl.rs | 2 +- js/AnalyserNode.js | 22 +++-- js/AudioBuffer.js | 34 ++++--- js/AudioBufferSourceNode.js | 30 ++++-- js/BaseAudioContext.mixin.js | 4 +- js/BiquadFilterNode.js | 8 +- js/ChannelMergerNode.js | 6 +- js/ChannelSplitterNode.js | 6 +- js/ConstantSourceNode.js | 6 +- js/ConvolverNode.js | 22 +++-- js/DelayNode.js | 6 +- js/DynamicsCompressorNode.js | 8 +- js/GainNode.js | 6 +- js/IIRFilterNode.js | 6 +- js/MediaStreamAudioSourceNode.js | 8 +- js/OfflineAudioContext.js | 19 ++-- js/OscillatorNode.js | 8 +- js/PannerNode.js | 36 +++---- js/StereoPannerNode.js | 6 +- js/WaveShaperNode.js | 12 ++- js/monkey-patch.js | 5 +- package.json | 1 + tests/AudioBuffer.spec.mjs | 101 +++++++++++++++----- tests/AudioParam.spec.mjs | 1 - tests/{errors.mjs => ctor.errors.mjs} | 0 tests/getUserMedia.spec.mjs | 68 +++++++++++-- tests/junk.mjs | 28 ++++++ tests/wpt-sample-accurate-scheduling.mjs | 96 ------------------- tests/wpt.mjs | 21 ---- 33 files changed, 409 insertions(+), 253 deletions(-) rename tests/{errors.mjs => ctor.errors.mjs} (100%) create mode 100644 tests/junk.mjs delete mode 100644 tests/wpt-sample-accurate-scheduling.mjs delete mode 100644 tests/wpt.mjs diff --git a/generator/index.mjs b/generator/index.mjs index 25380179..a79cd1ce 100644 --- a/generator/index.mjs +++ b/generator/index.mjs @@ -181,6 +181,10 @@ const utils = { return camelcase(idl.name, { pascalCase: true, preserveConsecutiveUppercase: true }); }, + + debug(value) { + console.log(JSON.stringify(value, null, 2)); + } }; let audioNodes = []; diff --git a/generator/js/AudioNodes.tmpl.js b/generator/js/AudioNodes.tmpl.js index efefc4b6..60c0860f 100644 --- a/generator/js/AudioNodes.tmpl.js +++ b/generator/js/AudioNodes.tmpl.js @@ -4,6 +4,9 @@ const { throwSanitizedError } = require('./lib/errors.js'); const { AudioParam } = require('./AudioParam.js'); const EventTargetMixin = require('./EventTarget.mixin.js'); const AudioNodeMixin = require('./AudioNode.mixin.js'); + +const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); + ${d.parent(d.node) === 'AudioScheduledSourceNode' ? `const AudioScheduledSourceNodeMixin = require('./AudioScheduledSourceNode.mixin.js');`: ``} @@ -47,14 +50,48 @@ ${d.audioParams(d.node).map(param => { `} // getters ${d.attributes(d.node).map(attr => { - return ` + switch (d.memberType(attr)) { + case 'AudioBuffer': { + return ` + get ${d.name(attr)}() { + if (this[kNativeAudioBuffer]) { + return this[kNativeAudioBuffer]; + } else { + return null; + } + } + `; + break; + } + default: { + return ` get ${d.name(attr)}() { return super.${d.name(attr)}; } -`}).join('')} + `; + break; + } + } +}).join('')} // setters ${d.attributes(d.node).filter(attr => !attr.readonly).map(attr => { - return ` + switch (d.memberType(attr)) { + case 'AudioBuffer': { + return ` + set ${d.name(attr)}(value) { + try { + super.${d.name(attr)} = value[kNativeAudioBuffer]; + } catch (err) { + throwSanitizedError(err); + } + + this[kNativeAudioBuffer] = value; + } + `; + break; + } + default: { + return ` set ${d.name(attr)}(value) { try { super.${d.name(attr)} = value; @@ -62,19 +99,25 @@ ${d.attributes(d.node).filter(attr => !attr.readonly).map(attr => { throwSanitizedError(err); } } -`}).join('')} + `; + break; + } + } +}).join('')} + // methods - ${d.methods(d.node, false).reduce((acc, method) => { - // dedup method names - if (!acc.find(i => d.name(i) === d.name(method))) { - acc.push(method) - } - return acc; - }, []) - // filter AudioScheduledSourceNode methods to prevent re-throwing errors - .filter(method => d.name(method) !== 'start' && d.name(method) !== 'stop') - .map(method => { - return ` +${d.methods(d.node, false) + .reduce((acc, method) => { + // dedup method names + if (!acc.find(i => d.name(i) === d.name(method))) { + acc.push(method) + } + return acc; + }, []) + // filter AudioScheduledSourceNode methods to prevent re-throwing errors + .filter(method => d.name(method) !== 'start' && d.name(method) !== 'stop') + .map(method => { + return ` ${d.name(method)}(...args) { try { return super.${d.name(method)}(...args); diff --git a/generator/js/BaseAudioContext.mixin.tmpl.js b/generator/js/BaseAudioContext.mixin.tmpl.js index fe28f9db..b1a6bc1f 100644 --- a/generator/js/BaseAudioContext.mixin.tmpl.js +++ b/generator/js/BaseAudioContext.mixin.tmpl.js @@ -1,5 +1,6 @@ const { AudioDestinationNode } = require('./AudioDestinationNode.js'); const { isFunction } = require('./lib/utils.js'); +const { kNativeAudioBuffer } = require('./AudioBuffer.js'); module.exports = (superclass, bindings) => { const { @@ -25,7 +26,8 @@ ${d.nodes.map(n => ` ${d.name(n)},`).join('\n')} } try { - const audioBuffer = super.decodeAudioData(audioData); + const nativeAudioBuffer = super.decodeAudioData(audioData); + const audioBuffer = new AudioBuffer({ [kNativeAudioBuffer]: nativeAudioBuffer }); if (isFunction(decodeSuccessCallback)) { decodeSuccessCallback(audioBuffer); diff --git a/generator/js/monkey-patch.tmpl.js b/generator/js/monkey-patch.tmpl.js index 3bbb1f4c..497b9b34 100644 --- a/generator/js/monkey-patch.tmpl.js +++ b/generator/js/monkey-patch.tmpl.js @@ -7,14 +7,13 @@ ${d.nodes.map((node) => { nativeBinding.${d.name(node)} = require('./${d.name(node)}.js')(nativeBinding.${d.name(node)});` }).join('')} - // @todo - wrap AudioBuffer interface as well nativeBinding.PeriodicWave = require('./PeriodicWave.js')(nativeBinding.PeriodicWave); - nativeBinding.AudioBuffer = require('./AudioBuffer.js')(nativeBinding.AudioBuffer); + nativeBinding.AudioBuffer = require('./AudioBuffer.js').AudioBuffer(nativeBinding.AudioBuffer); nativeBinding.AudioContext = require('./AudioContext.js')(nativeBinding); nativeBinding.OfflineAudioContext = require('./OfflineAudioContext.js')(nativeBinding); - // find a way to make the constructor private + // @todo - make the constructor private nativeBinding.AudioParam = require('./AudioParam.js').AudioParam; nativeBinding.AudioDestinationNode = require('./AudioDestinationNode.js').AudioDestinationNode; diff --git a/generator/rs/audio_nodes.tmpl.rs b/generator/rs/audio_nodes.tmpl.rs index c22f1e74..dcaa77df 100644 --- a/generator/rs/audio_nodes.tmpl.rs +++ b/generator/rs/audio_nodes.tmpl.rs @@ -913,7 +913,7 @@ fn set_${d.slug(attr)}(ctx: CallContext) -> Result { } `; break - case 'interface': // buffer + case 'interface': // AudioBuffer console.log(attr); return ` #[js_function(1)] diff --git a/js/AnalyserNode.js b/js/AnalyserNode.js index c5ee55a8..74cd7fa1 100644 --- a/js/AnalyserNode.js +++ b/js/AnalyserNode.js @@ -24,6 +24,9 @@ const { AudioParam } = require('./AudioParam.js'); const EventTargetMixin = require('./EventTarget.mixin.js'); const AudioNodeMixin = require('./AudioNode.mixin.js'); +const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); + + module.exports = (NativeAnalyserNode) => { @@ -45,23 +48,23 @@ module.exports = (NativeAnalyserNode) => { get fftSize() { return super.fftSize; } - + get frequencyBinCount() { return super.frequencyBinCount; } - + get minDecibels() { return super.minDecibels; } - + get maxDecibels() { return super.maxDecibels; } - + get smoothingTimeConstant() { return super.smoothingTimeConstant; } - + // setters set fftSize(value) { @@ -71,7 +74,7 @@ module.exports = (NativeAnalyserNode) => { throwSanitizedError(err); } } - + set minDecibels(value) { try { super.minDecibels = value; @@ -79,7 +82,7 @@ module.exports = (NativeAnalyserNode) => { throwSanitizedError(err); } } - + set maxDecibels(value) { try { super.maxDecibels = value; @@ -87,7 +90,7 @@ module.exports = (NativeAnalyserNode) => { throwSanitizedError(err); } } - + set smoothingTimeConstant(value) { try { super.smoothingTimeConstant = value; @@ -95,9 +98,10 @@ module.exports = (NativeAnalyserNode) => { throwSanitizedError(err); } } + // methods - + getFloatFrequencyData(...args) { try { return super.getFloatFrequencyData(...args); diff --git a/js/AudioBuffer.js b/js/AudioBuffer.js index 5ef27b9a..dcadb0d7 100644 --- a/js/AudioBuffer.js +++ b/js/AudioBuffer.js @@ -1,22 +1,25 @@ const { throwSanitizedError, DOMException } = require('./lib/errors.js'); -const kNativeAudioBuffer = Symbol('node-web-audio-api:audio-buffer'); +const kNativeAudioBuffer = Symbol('node-web-audio-api:native-audio-buffer'); +const kAudioBuffer = Symbol('node-web-audio-api:audio-buffer'); -module.exports = (NativeAudioBuffer) => { +module.exports.AudioBuffer = (NativeAudioBuffer) => { class AudioBuffer { constructor(options) { - if (typeof options !== 'object') { - throw new TypeError("Failed to construct 'AudioBuffer': argument 1 is not of type 'AudioBufferOptions'"); - } - - if (options[kNativeAudioBuffer] instanceof NativeAudioBuffer) { + if (kNativeAudioBuffer in options) { + // internal constructor for `startRendering` and `decodeAudioData` cases this[kNativeAudioBuffer] = options[kNativeAudioBuffer]; - } - - try { - this[kNativeAudioBuffer] = new NativeAudioBuffer(options); - } catch (err) { - throwSanitizedError(err); + } else { + // regular public constructor + if (typeof options !== 'object') { + throw new TypeError("Failed to construct 'AudioBuffer': argument 1 is not of type 'AudioBufferOptions'"); + } + + try { + this[kNativeAudioBuffer] = new NativeAudioBuffer(options); + } catch (err) { + throwSanitizedError(err); + } } } @@ -57,6 +60,7 @@ module.exports = (NativeAudioBuffer) => { throw new TypeError(`Failed to execute 'copyToChannel' on 'AudioBuffer': parameter 1 is not of type 'Float32Array'`); } + // rs implementation uses a usize so this check is irrelevant if (channelNumber < 0) { throw new DOMException(`Failed to execute 'copyToChannel' on 'AudioBuffer': channelNumber must equal or greater than 0`, 'IndexSizeError'); } @@ -80,3 +84,7 @@ module.exports = (NativeAudioBuffer) => { return AudioBuffer; }; +// so that AudioBufferSourceNode and ConvolverNode can retrieve the wrapped value to `super` class +module.exports.kNativeAudioBuffer = kNativeAudioBuffer; +module.exports.kAudioBuffer = kAudioBuffer; + diff --git a/js/AudioBufferSourceNode.js b/js/AudioBufferSourceNode.js index 120d1564..9d0aa444 100644 --- a/js/AudioBufferSourceNode.js +++ b/js/AudioBufferSourceNode.js @@ -23,6 +23,9 @@ const { throwSanitizedError } = require('./lib/errors.js'); const { AudioParam } = require('./AudioParam.js'); const EventTargetMixin = require('./EventTarget.mixin.js'); const AudioNodeMixin = require('./AudioNode.mixin.js'); + +const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); + const AudioScheduledSourceNodeMixin = require('./AudioScheduledSourceNode.mixin.js'); module.exports = (NativeAudioBufferSourceNode) => { @@ -49,31 +52,37 @@ module.exports = (NativeAudioBufferSourceNode) => { // getters get buffer() { - return super.buffer; + if (this[kNativeAudioBuffer]) { + return this[kNativeAudioBuffer]; + } else { + return null; + } } - + get loop() { return super.loop; } - + get loopStart() { return super.loopStart; } - + get loopEnd() { return super.loopEnd; } - + // setters set buffer(value) { try { - super.buffer = value; + super.buffer = value[kNativeAudioBuffer]; } catch (err) { throwSanitizedError(err); } - } + this[kNativeAudioBuffer] = value; + } + set loop(value) { try { super.loop = value; @@ -81,7 +90,7 @@ module.exports = (NativeAudioBufferSourceNode) => { throwSanitizedError(err); } } - + set loopStart(value) { try { super.loopStart = value; @@ -89,7 +98,7 @@ module.exports = (NativeAudioBufferSourceNode) => { throwSanitizedError(err); } } - + set loopEnd(value) { try { super.loopEnd = value; @@ -97,9 +106,10 @@ module.exports = (NativeAudioBufferSourceNode) => { throwSanitizedError(err); } } + // methods - + } return AudioBufferSourceNode; diff --git a/js/BaseAudioContext.mixin.js b/js/BaseAudioContext.mixin.js index 21d7765c..ccc9913f 100644 --- a/js/BaseAudioContext.mixin.js +++ b/js/BaseAudioContext.mixin.js @@ -19,6 +19,7 @@ const { AudioDestinationNode } = require('./AudioDestinationNode.js'); const { isFunction } = require('./lib/utils.js'); +const { kNativeAudioBuffer } = require('./AudioBuffer.js'); module.exports = (superclass, bindings) => { const { @@ -59,7 +60,8 @@ module.exports = (superclass, bindings) => { } try { - const audioBuffer = super.decodeAudioData(audioData); + const nativeAudioBuffer = super.decodeAudioData(audioData); + const audioBuffer = new AudioBuffer({ [kNativeAudioBuffer]: nativeAudioBuffer }); if (isFunction(decodeSuccessCallback)) { decodeSuccessCallback(audioBuffer); diff --git a/js/BiquadFilterNode.js b/js/BiquadFilterNode.js index c5e49c17..d924fd40 100644 --- a/js/BiquadFilterNode.js +++ b/js/BiquadFilterNode.js @@ -24,6 +24,9 @@ const { AudioParam } = require('./AudioParam.js'); const EventTargetMixin = require('./EventTarget.mixin.js'); const AudioNodeMixin = require('./AudioNode.mixin.js'); +const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); + + module.exports = (NativeBiquadFilterNode) => { @@ -49,7 +52,7 @@ module.exports = (NativeBiquadFilterNode) => { get type() { return super.type; } - + // setters set type(value) { @@ -59,9 +62,10 @@ module.exports = (NativeBiquadFilterNode) => { throwSanitizedError(err); } } + // methods - + getFrequencyResponse(...args) { try { return super.getFrequencyResponse(...args); diff --git a/js/ChannelMergerNode.js b/js/ChannelMergerNode.js index 2f7a021d..a04a35b9 100644 --- a/js/ChannelMergerNode.js +++ b/js/ChannelMergerNode.js @@ -24,6 +24,9 @@ const { AudioParam } = require('./AudioParam.js'); const EventTargetMixin = require('./EventTarget.mixin.js'); const AudioNodeMixin = require('./AudioNode.mixin.js'); +const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); + + module.exports = (NativeChannelMergerNode) => { @@ -44,8 +47,9 @@ module.exports = (NativeChannelMergerNode) => { // setters + // methods - + } return ChannelMergerNode; diff --git a/js/ChannelSplitterNode.js b/js/ChannelSplitterNode.js index b2d4f175..ebf77cad 100644 --- a/js/ChannelSplitterNode.js +++ b/js/ChannelSplitterNode.js @@ -24,6 +24,9 @@ const { AudioParam } = require('./AudioParam.js'); const EventTargetMixin = require('./EventTarget.mixin.js'); const AudioNodeMixin = require('./AudioNode.mixin.js'); +const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); + + module.exports = (NativeChannelSplitterNode) => { @@ -44,8 +47,9 @@ module.exports = (NativeChannelSplitterNode) => { // setters + // methods - + } return ChannelSplitterNode; diff --git a/js/ConstantSourceNode.js b/js/ConstantSourceNode.js index 2e8d5cae..b88ebfb0 100644 --- a/js/ConstantSourceNode.js +++ b/js/ConstantSourceNode.js @@ -23,6 +23,9 @@ const { throwSanitizedError } = require('./lib/errors.js'); const { AudioParam } = require('./AudioParam.js'); const EventTargetMixin = require('./EventTarget.mixin.js'); const AudioNodeMixin = require('./AudioNode.mixin.js'); + +const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); + const AudioScheduledSourceNodeMixin = require('./AudioScheduledSourceNode.mixin.js'); module.exports = (NativeConstantSourceNode) => { @@ -49,8 +52,9 @@ module.exports = (NativeConstantSourceNode) => { // setters + // methods - + } return ConstantSourceNode; diff --git a/js/ConvolverNode.js b/js/ConvolverNode.js index c90fa889..028e31e2 100644 --- a/js/ConvolverNode.js +++ b/js/ConvolverNode.js @@ -24,6 +24,9 @@ const { AudioParam } = require('./AudioParam.js'); const EventTargetMixin = require('./EventTarget.mixin.js'); const AudioNodeMixin = require('./AudioNode.mixin.js'); +const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); + + module.exports = (NativeConvolverNode) => { @@ -43,23 +46,29 @@ module.exports = (NativeConvolverNode) => { // getters get buffer() { - return super.buffer; + if (this[kNativeAudioBuffer]) { + return this[kNativeAudioBuffer]; + } else { + return null; + } } - + get normalize() { return super.normalize; } - + // setters set buffer(value) { try { - super.buffer = value; + super.buffer = value[kNativeAudioBuffer]; } catch (err) { throwSanitizedError(err); } - } + this[kNativeAudioBuffer] = value; + } + set normalize(value) { try { super.normalize = value; @@ -67,9 +76,10 @@ module.exports = (NativeConvolverNode) => { throwSanitizedError(err); } } + // methods - + } return ConvolverNode; diff --git a/js/DelayNode.js b/js/DelayNode.js index c41d5078..f9b97e40 100644 --- a/js/DelayNode.js +++ b/js/DelayNode.js @@ -24,6 +24,9 @@ const { AudioParam } = require('./AudioParam.js'); const EventTargetMixin = require('./EventTarget.mixin.js'); const AudioNodeMixin = require('./AudioNode.mixin.js'); +const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); + + module.exports = (NativeDelayNode) => { @@ -45,8 +48,9 @@ module.exports = (NativeDelayNode) => { // setters + // methods - + } return DelayNode; diff --git a/js/DynamicsCompressorNode.js b/js/DynamicsCompressorNode.js index 711a2d48..c4535678 100644 --- a/js/DynamicsCompressorNode.js +++ b/js/DynamicsCompressorNode.js @@ -24,6 +24,9 @@ const { AudioParam } = require('./AudioParam.js'); const EventTargetMixin = require('./EventTarget.mixin.js'); const AudioNodeMixin = require('./AudioNode.mixin.js'); +const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); + + module.exports = (NativeDynamicsCompressorNode) => { @@ -50,11 +53,12 @@ module.exports = (NativeDynamicsCompressorNode) => { get reduction() { return super.reduction; } - + // setters + // methods - + } return DynamicsCompressorNode; diff --git a/js/GainNode.js b/js/GainNode.js index e29747c9..459007f9 100644 --- a/js/GainNode.js +++ b/js/GainNode.js @@ -24,6 +24,9 @@ const { AudioParam } = require('./AudioParam.js'); const EventTargetMixin = require('./EventTarget.mixin.js'); const AudioNodeMixin = require('./AudioNode.mixin.js'); +const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); + + module.exports = (NativeGainNode) => { @@ -45,8 +48,9 @@ module.exports = (NativeGainNode) => { // setters + // methods - + } return GainNode; diff --git a/js/IIRFilterNode.js b/js/IIRFilterNode.js index b65df9c1..ae9a6407 100644 --- a/js/IIRFilterNode.js +++ b/js/IIRFilterNode.js @@ -24,6 +24,9 @@ const { AudioParam } = require('./AudioParam.js'); const EventTargetMixin = require('./EventTarget.mixin.js'); const AudioNodeMixin = require('./AudioNode.mixin.js'); +const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); + + module.exports = (NativeIIRFilterNode) => { @@ -44,8 +47,9 @@ module.exports = (NativeIIRFilterNode) => { // setters + // methods - + getFrequencyResponse(...args) { try { return super.getFrequencyResponse(...args); diff --git a/js/MediaStreamAudioSourceNode.js b/js/MediaStreamAudioSourceNode.js index 4905d1ce..6d8c3b1a 100644 --- a/js/MediaStreamAudioSourceNode.js +++ b/js/MediaStreamAudioSourceNode.js @@ -24,6 +24,9 @@ const { AudioParam } = require('./AudioParam.js'); const EventTargetMixin = require('./EventTarget.mixin.js'); const AudioNodeMixin = require('./AudioNode.mixin.js'); +const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); + + module.exports = (NativeMediaStreamAudioSourceNode) => { @@ -45,11 +48,12 @@ module.exports = (NativeMediaStreamAudioSourceNode) => { get mediaStream() { return super.mediaStream; } - + // setters + // methods - + } return MediaStreamAudioSourceNode; diff --git a/js/OfflineAudioContext.js b/js/OfflineAudioContext.js index bd09c31c..ab1e38d9 100644 --- a/js/OfflineAudioContext.js +++ b/js/OfflineAudioContext.js @@ -1,11 +1,13 @@ const { nameCodeMap, DOMException } = require('./lib/errors.js'); const { isPlainObject, isPositiveInt, isPositiveNumber } = require('./lib/utils.js'); +const { kNativeAudioBuffer } = require('./AudioBuffer.js'); module.exports = function patchOfflineAudioContext(bindings) { + const AudioBuffer = bindings.AudioBuffer; + // @todo - EventTarget // - https://github.com/orottier/web-audio-api-rs/issues/411 // - https://github.com/orottier/web-audio-api-rs/issues/416 - const EventTarget = require('./EventTarget.mixin.js')(bindings.OfflineAudioContext, ['statechange']); const BaseAudioContext = require('./BaseAudioContext.mixin.js')(EventTarget, bindings); @@ -40,16 +42,17 @@ module.exports = function patchOfflineAudioContext(bindings) { } async startRendering() { - const renderedBuffer = await super.startRendering(); + const nativeAudioBuffer = await super.startRendering(); + const audioBuffer = new AudioBuffer({ [kNativeAudioBuffer]: nativeAudioBuffer }); - // We do this here, so that we can just share the same audioBuffer instance. - // This also simplifies code on the rust side as we don't need to deal - // with the OfflineAudioCompletionEvent. + // We dispatch the complete envet manually to simplify the sharing of the + // `AudioBuffer` instance. This also simplifies code on the rust side as + // we don't need to deal with the `OfflineAudioCompletionEvent` type. const event = new Event('complete'); - event.renderedBuffer = renderedBuffer; - this.dispatchEvent(event) + event.renderedBuffer = audioBuffer; + this.dispatchEvent(event); - return renderedBuffer; + return audioBuffer; } } diff --git a/js/OscillatorNode.js b/js/OscillatorNode.js index 5a2f95bc..ebaf7c9f 100644 --- a/js/OscillatorNode.js +++ b/js/OscillatorNode.js @@ -23,6 +23,9 @@ const { throwSanitizedError } = require('./lib/errors.js'); const { AudioParam } = require('./AudioParam.js'); const EventTargetMixin = require('./EventTarget.mixin.js'); const AudioNodeMixin = require('./AudioNode.mixin.js'); + +const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); + const AudioScheduledSourceNodeMixin = require('./AudioScheduledSourceNode.mixin.js'); module.exports = (NativeOscillatorNode) => { @@ -51,7 +54,7 @@ module.exports = (NativeOscillatorNode) => { get type() { return super.type; } - + // setters set type(value) { @@ -61,9 +64,10 @@ module.exports = (NativeOscillatorNode) => { throwSanitizedError(err); } } + // methods - + setPeriodicWave(...args) { try { return super.setPeriodicWave(...args); diff --git a/js/PannerNode.js b/js/PannerNode.js index 61babe37..4cab41e2 100644 --- a/js/PannerNode.js +++ b/js/PannerNode.js @@ -24,6 +24,9 @@ const { AudioParam } = require('./AudioParam.js'); const EventTargetMixin = require('./EventTarget.mixin.js'); const AudioNodeMixin = require('./AudioNode.mixin.js'); +const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); + + module.exports = (NativePannerNode) => { @@ -51,35 +54,35 @@ module.exports = (NativePannerNode) => { get panningModel() { return super.panningModel; } - + get distanceModel() { return super.distanceModel; } - + get refDistance() { return super.refDistance; } - + get maxDistance() { return super.maxDistance; } - + get rolloffFactor() { return super.rolloffFactor; } - + get coneInnerAngle() { return super.coneInnerAngle; } - + get coneOuterAngle() { return super.coneOuterAngle; } - + get coneOuterGain() { return super.coneOuterGain; } - + // setters set panningModel(value) { @@ -89,7 +92,7 @@ module.exports = (NativePannerNode) => { throwSanitizedError(err); } } - + set distanceModel(value) { try { super.distanceModel = value; @@ -97,7 +100,7 @@ module.exports = (NativePannerNode) => { throwSanitizedError(err); } } - + set refDistance(value) { try { super.refDistance = value; @@ -105,7 +108,7 @@ module.exports = (NativePannerNode) => { throwSanitizedError(err); } } - + set maxDistance(value) { try { super.maxDistance = value; @@ -113,7 +116,7 @@ module.exports = (NativePannerNode) => { throwSanitizedError(err); } } - + set rolloffFactor(value) { try { super.rolloffFactor = value; @@ -121,7 +124,7 @@ module.exports = (NativePannerNode) => { throwSanitizedError(err); } } - + set coneInnerAngle(value) { try { super.coneInnerAngle = value; @@ -129,7 +132,7 @@ module.exports = (NativePannerNode) => { throwSanitizedError(err); } } - + set coneOuterAngle(value) { try { super.coneOuterAngle = value; @@ -137,7 +140,7 @@ module.exports = (NativePannerNode) => { throwSanitizedError(err); } } - + set coneOuterGain(value) { try { super.coneOuterGain = value; @@ -145,9 +148,10 @@ module.exports = (NativePannerNode) => { throwSanitizedError(err); } } + // methods - + setPosition(...args) { try { return super.setPosition(...args); diff --git a/js/StereoPannerNode.js b/js/StereoPannerNode.js index 3900b2d8..839c69f2 100644 --- a/js/StereoPannerNode.js +++ b/js/StereoPannerNode.js @@ -24,6 +24,9 @@ const { AudioParam } = require('./AudioParam.js'); const EventTargetMixin = require('./EventTarget.mixin.js'); const AudioNodeMixin = require('./AudioNode.mixin.js'); +const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); + + module.exports = (NativeStereoPannerNode) => { @@ -45,8 +48,9 @@ module.exports = (NativeStereoPannerNode) => { // setters + // methods - + } return StereoPannerNode; diff --git a/js/WaveShaperNode.js b/js/WaveShaperNode.js index 13764575..6d2367b1 100644 --- a/js/WaveShaperNode.js +++ b/js/WaveShaperNode.js @@ -24,6 +24,9 @@ const { AudioParam } = require('./AudioParam.js'); const EventTargetMixin = require('./EventTarget.mixin.js'); const AudioNodeMixin = require('./AudioNode.mixin.js'); +const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); + + module.exports = (NativeWaveShaperNode) => { @@ -45,11 +48,11 @@ module.exports = (NativeWaveShaperNode) => { get curve() { return super.curve; } - + get oversample() { return super.oversample; } - + // setters set curve(value) { @@ -59,7 +62,7 @@ module.exports = (NativeWaveShaperNode) => { throwSanitizedError(err); } } - + set oversample(value) { try { super.oversample = value; @@ -67,9 +70,10 @@ module.exports = (NativeWaveShaperNode) => { throwSanitizedError(err); } } + // methods - + } return WaveShaperNode; diff --git a/js/monkey-patch.js b/js/monkey-patch.js index 58fbef6e..dc4701ad 100644 --- a/js/monkey-patch.js +++ b/js/monkey-patch.js @@ -39,14 +39,13 @@ module.exports = function monkeyPatch(nativeBinding) { nativeBinding.StereoPannerNode = require('./StereoPannerNode.js')(nativeBinding.StereoPannerNode); nativeBinding.WaveShaperNode = require('./WaveShaperNode.js')(nativeBinding.WaveShaperNode); - // @todo - wrap AudioBuffer interface as well nativeBinding.PeriodicWave = require('./PeriodicWave.js')(nativeBinding.PeriodicWave); - nativeBinding.AudioBuffer = require('./AudioBuffer.js')(nativeBinding.AudioBuffer); + nativeBinding.AudioBuffer = require('./AudioBuffer.js').AudioBuffer(nativeBinding.AudioBuffer); nativeBinding.AudioContext = require('./AudioContext.js')(nativeBinding); nativeBinding.OfflineAudioContext = require('./OfflineAudioContext.js')(nativeBinding); - // find a way to make the constructor private + // @todo - make the constructor private nativeBinding.AudioParam = require('./AudioParam.js').AudioParam; nativeBinding.AudioDestinationNode = require('./AudioDestinationNode.js').AudioDestinationNode; diff --git a/package.json b/package.json index b83eb5b7..5f7c10d3 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "devDependencies": { "@ircam/eslint-config": "^1.3.0", "@ircam/sc-gettime": "^1.0.0", + "@ircam/sc-utils": "^1.3.3", "@sindresorhus/slugify": "^2.1.1", "camelcase": "^7.0.1", "chai": "^4.3.7", diff --git a/tests/AudioBuffer.spec.mjs b/tests/AudioBuffer.spec.mjs index 8e68d775..ebceef63 100644 --- a/tests/AudioBuffer.spec.mjs +++ b/tests/AudioBuffer.spec.mjs @@ -1,31 +1,31 @@ +import path from 'node:path'; +import fs from 'node:fs'; import { assert } from 'chai'; -import { AudioBuffer, AudioContext } from '../index.mjs'; +import { AudioBuffer, AudioContext, OfflineAudioContext } from '../index.mjs'; describe('# AudioBuffer', () => { - let audioContext; - before(() => { - audioContext = new AudioContext(); - }); - - after(() => { - audioContext.close(); - }); - - describe(`## audioContext.createBuffer`, () => { + describe(`## audioContext.createBuffer(numChannels, length, sampleRate)`, () => { it('should properly create audio buffer', () => { + const audioContext = new AudioContext(); const audioBuffer = audioContext.createBuffer(1, 100, audioContext.sampleRate); assert.equal(audioBuffer instanceof AudioBuffer, true); assert.equal(audioBuffer.numberOfChannels, 1); assert.equal(audioBuffer.length, 100); assert.equal(audioBuffer.sampleRate, audioContext.sampleRate); + + // @fixme - do not lock the process + audioContext.close(); }); it('should properly fail if missing argument', () => { + const audioContext = new AudioContext(); assert.throws(() => { const audioBuffer = audioContext.createBuffer(1, 100); }); + + audioContext.close(); }); }); @@ -33,13 +33,13 @@ describe('# AudioBuffer', () => { it('should properly create audio buffer', () => { const audioBuffer = new AudioBuffer({ length: 100, - sampleRate: audioContext.sampleRate, + sampleRate: 48000, }); assert.equal(audioBuffer instanceof AudioBuffer, true); assert.equal(audioBuffer.numberOfChannels, 1); assert.equal(audioBuffer.length, 100); - assert.equal(audioBuffer.sampleRate, audioContext.sampleRate); + assert.equal(audioBuffer.sampleRate, 48000); }); it('should properly fail if missing argument', () => { @@ -48,31 +48,86 @@ describe('# AudioBuffer', () => { }); }); - it.skip(`should have clean error type`, () => { + it(`should have clean error type`, () => { try { new AudioBuffer(Date, 42); } catch (err) { - console.log(err.name); - console.log(err.message); - assert.fail('should be TypeError'); + console.log(err.name, ':', err.message); + assert.isTrue(err instanceof TypeError); } }); }); describe(`## AudioBuffer returned by other means`, () => { - it.skip(`OfflineAudioContext.startRendering() -> AudioBuffer`, () => { - const audioBuffer = '@todo'; + it(`AudioContext.decodeAudioData() -> AudioBuffer`, async () => { + const pathname = path.join('examples', 'samples', 'sample.wav'); + const buffer = fs.readFileSync(pathname).buffer; + const audioContext = new OfflineAudioContext(1, 1, 48000); + const audioBuffer = await audioContext.decodeAudioData(buffer); + assert.equal(audioBuffer instanceof AudioBuffer, true); + // make sure we use the underlying native buffer + const emptyBuffer = new Float32Array(audioBuffer.length).fill(0); + assert.notDeepEqual(audioBuffer.getChannelData(0), emptyBuffer); + // @fixme - do not lock the process + audioContext.startRendering(); }); - it.skip(`AudioContext.decodeAudioData() -> AudioBuffer`, () => { - const audioBuffer = '@todo'; + it(`OfflineAudioContext.decodeAudioData() -> AudioBuffer`, async () => { + const pathname = path.join('examples', 'samples', 'sample.wav'); + const buffer = fs.readFileSync(pathname).buffer; + const audioContext = new AudioContext(); + const audioBuffer = await audioContext.decodeAudioData(buffer); + assert.equal(audioBuffer instanceof AudioBuffer, true); + // make sure we use the underlying native buffer + const emptyBuffer = new Float32Array(audioBuffer.length).fill(0); + assert.notDeepEqual(audioBuffer.getChannelData(0), emptyBuffer); + // @fixme - do not lock the process + audioContext.close(); }); - it.skip(`OfflineAudioContext.decodeAudioData() -> AudioBuffer`, () => { - const audioBuffer = '@todo'; + it(`OfflineAudioContext.startRendering() -> AudioBuffer`, async () => { + const audioContext = new OfflineAudioContext(1, 1000, 48000); + const src = audioContext.createOscillator(); + src.connect(audioContext.destination); + src.start(0); + + const audioBuffer = await audioContext.startRendering(); assert.equal(audioBuffer instanceof AudioBuffer, true); + // make sure we use the underlying native buffer + const emptyBuffer = new Float32Array(audioBuffer.length).fill(0); + assert.notDeepEqual(audioBuffer.getChannelData(0), emptyBuffer); + }); + }); + + describe(`AudioBufferSourceNode.buffer`, () => { + it(`should work properly`, async () => { + const audioContext = new AudioContext(); + + const pathname = path.join('examples', 'samples', 'sample.wav'); + const buffer = fs.readFileSync(pathname).buffer; + const audioBuffer = await audioContext.decodeAudioData(buffer); + + const src = audioContext.createBufferSource(); + // should retrieve native audio buffer to native buffer source node + src.buffer = audioBuffer; + src.connect(audioContext.destination); + + assert.deepEqual(src.buffer, audioBuffer); + + src.start(audioContext.currentTime); + src.stop(audioContext.currentTime + 0.3); + + await new Promise(resolve => setTimeout(resolve, 400)); + await audioContext.close(); }); }); }); + + + + + + + diff --git a/tests/AudioParam.spec.mjs b/tests/AudioParam.spec.mjs index f764ad4a..26b6300c 100644 --- a/tests/AudioParam.spec.mjs +++ b/tests/AudioParam.spec.mjs @@ -21,7 +21,6 @@ describe('# AudioBuffer', () => { // should accept some delta assert.equal(gain.gain.maxValue, 3.4028234663852886e+38); assert.equal(gain.gain.minValue, -3.4028234663852886e+38); - assert.equal(gain.gain.value, 1); }); }); diff --git a/tests/errors.mjs b/tests/ctor.errors.mjs similarity index 100% rename from tests/errors.mjs rename to tests/ctor.errors.mjs diff --git a/tests/getUserMedia.spec.mjs b/tests/getUserMedia.spec.mjs index 2e992aab..97a0faa2 100644 --- a/tests/getUserMedia.spec.mjs +++ b/tests/getUserMedia.spec.mjs @@ -1,6 +1,7 @@ import { assert } from 'chai'; +import { sleep } from '@ircam/sc-utils'; -import { mediaDevices } from '../index.mjs'; +import { mediaDevices, AudioContext, MediaStreamAudioSourceNode } from '../index.mjs'; describe('# mediaDevices.getUserMedia(options)', () => { it('should fail if no argument given', async () => { @@ -12,7 +13,9 @@ describe('# mediaDevices.getUserMedia(options)', () => { failed = true; } - if (!failed) { assert.fail(); } + if (!failed) { + assert.fail('should have failed'); + } }); // @todo - clean error message @@ -25,7 +28,9 @@ describe('# mediaDevices.getUserMedia(options)', () => { failed = true; } - if (!failed) { assert.fail(); } + if (!failed) { + assert.fail('should have failed'); + } }); it('should fail if options.video', async () => { @@ -37,22 +42,71 @@ describe('# mediaDevices.getUserMedia(options)', () => { failed = true; } - if (!failed) { assert.fail(); } + if (!failed) { + assert.fail('should have failed'); + } }); it('should not fail if options.audio = true', async () => { let failed = false; + const audioContext = new AudioContext(); try { const stream = await mediaDevices.getUserMedia({ audio: true }); - // console.log(stream instanceof mediaDevices.MediaStream); } catch (err) { console.log(err); failed = true; } - console.log(failed); + await sleep(0.4); + await audioContext.close(); + + if (failed) { + assert.fail('should not have failed'); + } + }); + + it('should work with MediaStreamAudioSourceNode [1 factory] (make some noise)', async () => { + let failed = false; + const audioContext = new AudioContext(); + + const stream = await mediaDevices.getUserMedia({ audio: true }); + + try { + const src = audioContext.createMediaStreamSource(stream); + src.connect(audioContext.destination); + } catch (err) { + console.log(err); + failed = true; + } + + await sleep(0.4); + await audioContext.close(); + + if (failed) { + assert.fail('should not have failed'); + } + }); + + it('should work with MediaStreamAudioSourceNode [2 ctor] (make some noise)', async () => { + let failed = false; + const audioContext = new AudioContext(); + + const stream = await mediaDevices.getUserMedia({ audio: true }); - if (failed) { assert.fail('should not have failed'); } + try { + const src = new MediaStreamAudioSourceNode(audioContext, { mediaStream: stream }); + src.connect(audioContext.destination); + } catch (err) { + console.log(err); + failed = true; + } + + await sleep(0.4); + await audioContext.close(); + + if (failed) { + assert.fail('should not have failed'); + } }); }); diff --git a/tests/junk.mjs b/tests/junk.mjs new file mode 100644 index 00000000..f65061e7 --- /dev/null +++ b/tests/junk.mjs @@ -0,0 +1,28 @@ +// import { AudioBufferSourceNode, AnalyserNode, AudioContext, GainNode, OfflineAudioContext, StereoPannerNode, PeriodicWave, MediaStreamAudioSourceNode, mediaDevices } from '../index.mjs'; + + +// const mediaStream = await mediaDevices.getUserMedia({ audio: true }); +// const context = new OfflineAudioContext(2, 1, 48000); +// // // const node = new AudioBufferSourceNode(context, 42) +// // // const src = context.createBufferSource(); +// // // src.start(NaN); +// // new StereoPannerNode(context, {"channelCountMode":"max"}); +// // new StereoPannerNode(context, {"channelCount":3}) + +// try { +// // new OfflineAudioContext({"length":42,"sampleRate":12345}) +// // new PeriodicWave(context, { real : new Float32Array(8192), imag : new Float32Array(4) }) +// const src = new MediaStreamAudioSourceNode(context, { mediaStream }); +// console.log(src); +// } catch (err) { +// console.log(err); +// } + +// await context.startRendering(); + + +const key = Symbol('key'); + +const options = { [key]: 'value' }; + +console.log(key in options); diff --git a/tests/wpt-sample-accurate-scheduling.mjs b/tests/wpt-sample-accurate-scheduling.mjs deleted file mode 100644 index 71949f07..00000000 --- a/tests/wpt-sample-accurate-scheduling.mjs +++ /dev/null @@ -1,96 +0,0 @@ -import { AnalyserNode, AudioContext, GainNode, OfflineAudioContext, mediaDevices } from '../index.mjs'; - -// the-audio-api/the-audiobuffersourcenode-interface/sample-accurate-scheduling.html - -let sampleRate = 44100.0; -let lengthInSeconds = 4; - -let context = 0; -let bufferLoader = 0; -let impulse; - -// See if we can render at exactly these sample offsets. -let sampleOffsets = [0, 3, 512, 517, 1000, 1005, 20000, 21234, 37590]; - -function createImpulse() { - // An impulse has a value of 1 at time 0, and is otherwise 0. - impulse = context.createBuffer(2, 512, sampleRate); - let sampleDataL = impulse.getChannelData(0); - let sampleDataR = impulse.getChannelData(1); - sampleDataL[0] = 1.0; - sampleDataR[0] = 1.0; -} - -function playNote(time) { - console.log('play at time', time); - let bufferSource = context.createBufferSource(); - bufferSource.buffer = impulse; - bufferSource.connect(context.destination); - bufferSource.start(time); -} - -function checkSampleAccuracy(buffer, should) { - let bufferDataL = buffer.getChannelData(0); - let bufferDataR = buffer.getChannelData(1); - // console.log(JSON.stringify(bufferDataL.slice(900, 1100), null, 2)); - - let impulseCount = 0; - let badOffsetCount = 0; - - // Left and right channels must be the same. - // should(bufferDataL, 'Content of left and right channels match and') - // .beEqualToArray(bufferDataR); - for (let i = 0; i < bufferDataL.length; i++) { - if (bufferDataL[i] != 0) { - console.log('non zero found', i); - } - - if (bufferDataL[i] != bufferDataR[i]) { - console.log('should be euqal at index', i, bufferDataL[i], bufferDataR[i]) - } - } - - // Go through every sample and make sure it's 0, except at positions in - // sampleOffsets. - for (let i = 0; i < buffer.length; ++i) { - if (bufferDataL[i] != 0) { - // Make sure this index is in sampleOffsets - let found = false; - for (let j = 0; j < sampleOffsets.length; ++j) { - if (sampleOffsets[j] == i) { - found = true; - break; - } - } - - ++impulseCount; - console.log(found, 'Non-zero sample found at sample offset ' + i) - - if (!found) { - ++badOffsetCount; - } - } - } - - console.log('Number of impulses found', impulseCount, sampleOffsets.length) - - if (impulseCount == sampleOffsets.length) { - console.log('bad offset:', badOffsetCount); - } -} - - -// Create offline audio context. -context = new OfflineAudioContext(2, sampleRate * lengthInSeconds, sampleRate); -createImpulse(); - -for (let i = 0; i < sampleOffsets.length; ++i) { - let timeInSeconds = sampleOffsets[i] / sampleRate; - console.log(i, sampleOffsets[i], timeInSeconds); - playNote(timeInSeconds); -} - -context.startRendering().then(function(buffer) { - checkSampleAccuracy(buffer); -}); - diff --git a/tests/wpt.mjs b/tests/wpt.mjs deleted file mode 100644 index dcf3e3af..00000000 --- a/tests/wpt.mjs +++ /dev/null @@ -1,21 +0,0 @@ -import { AudioBufferSourceNode, AnalyserNode, AudioContext, GainNode, OfflineAudioContext, StereoPannerNode, PeriodicWave, MediaStreamAudioSourceNode, mediaDevices } from '../index.mjs'; - - -const mediaStream = await mediaDevices.getUserMedia({ audio: true }); -const context = new OfflineAudioContext(2, 1, 48000); -// // const node = new AudioBufferSourceNode(context, 42) -// // const src = context.createBufferSource(); -// // src.start(NaN); -// new StereoPannerNode(context, {"channelCountMode":"max"}); -// new StereoPannerNode(context, {"channelCount":3}) - -try { - // new OfflineAudioContext({"length":42,"sampleRate":12345}) - // new PeriodicWave(context, { real : new Float32Array(8192), imag : new Float32Array(4) }) - const src = new MediaStreamAudioSourceNode(context, { mediaStream }); - console.log(src); -} catch (err) { - console.log(err); -} - -await context.startRendering(); From 8d2d2f65e60be6a9dc3874e48c9ed9295b783163 Mon Sep 17 00:00:00 2001 From: b-ma Date: Wed, 13 Mar 2024 11:42:55 +0100 Subject: [PATCH 05/12] fix: properly check type in `set buffer(AudioBuffer)` --- generator/js/AudioNodes.tmpl.js | 7 ++++++ generator/rs/lib.tmpl.rs | 2 +- js/AudioBufferSourceNode.js | 7 ++++++ js/ConvolverNode.js | 7 ++++++ src/lib.rs | 2 +- tests/junk.mjs | 40 ++++++++++++++++++++++++++++----- 6 files changed, 58 insertions(+), 7 deletions(-) diff --git a/generator/js/AudioNodes.tmpl.js b/generator/js/AudioNodes.tmpl.js index 60c0860f..99f0d96e 100644 --- a/generator/js/AudioNodes.tmpl.js +++ b/generator/js/AudioNodes.tmpl.js @@ -78,7 +78,14 @@ ${d.attributes(d.node).filter(attr => !attr.readonly).map(attr => { switch (d.memberType(attr)) { case 'AudioBuffer': { return ` + // @todo - should be able to set to null afterward set ${d.name(attr)}(value) { + if (value === null) { + return; + } else if (!(kNativeAudioBuffer in value)) { + throw new TypeError("Failed to set the 'buffer' property on 'AudioBufferSourceNode': Failed to convert value to 'AudioBuffer'"); + } + try { super.${d.name(attr)} = value[kNativeAudioBuffer]; } catch (err) { diff --git a/generator/rs/lib.tmpl.rs b/generator/rs/lib.tmpl.rs index 9ed69d95..b9339719 100644 --- a/generator/rs/lib.tmpl.rs +++ b/generator/rs/lib.tmpl.rs @@ -48,7 +48,7 @@ static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc; #[module_exports] fn init(mut exports: JsObject, env: Env) -> Result<()> { // Do not print panic messages, handle through JS errors - // std::panic::set_hook(Box::new(|_panic_info| {})); + std::panic::set_hook(Box::new(|_panic_info| {})); // Store constructors for factory methods and internal instantiations // Note that we need to create the js class twice so that export and store diff --git a/js/AudioBufferSourceNode.js b/js/AudioBufferSourceNode.js index 9d0aa444..5a0496a1 100644 --- a/js/AudioBufferSourceNode.js +++ b/js/AudioBufferSourceNode.js @@ -73,7 +73,14 @@ module.exports = (NativeAudioBufferSourceNode) => { // setters + // @todo - should be able to set to null afterward set buffer(value) { + if (value === null) { + return; + } else if (!(kNativeAudioBuffer in value)) { + throw new TypeError("Failed to set the 'buffer' property on 'AudioBufferSourceNode': Failed to convert value to 'AudioBuffer'"); + } + try { super.buffer = value[kNativeAudioBuffer]; } catch (err) { diff --git a/js/ConvolverNode.js b/js/ConvolverNode.js index 028e31e2..e7f96c81 100644 --- a/js/ConvolverNode.js +++ b/js/ConvolverNode.js @@ -59,7 +59,14 @@ module.exports = (NativeConvolverNode) => { // setters + // @todo - should be able to set to null afterward set buffer(value) { + if (value === null) { + return; + } else if (!(kNativeAudioBuffer in value)) { + throw new TypeError("Failed to set the 'buffer' property on 'AudioBufferSourceNode': Failed to convert value to 'AudioBuffer'"); + } + try { super.buffer = value[kNativeAudioBuffer]; } catch (err) { diff --git a/src/lib.rs b/src/lib.rs index e0dcd878..da0ff068 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -97,7 +97,7 @@ static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc; #[module_exports] fn init(mut exports: JsObject, env: Env) -> Result<()> { // Do not print panic messages, handle through JS errors - // std::panic::set_hook(Box::new(|_panic_info| {})); + std::panic::set_hook(Box::new(|_panic_info| {})); // Store constructors for factory methods and internal instantiations // Note that we need to create the js class twice so that export and store diff --git a/tests/junk.mjs b/tests/junk.mjs index f65061e7..67def1aa 100644 --- a/tests/junk.mjs +++ b/tests/junk.mjs @@ -1,4 +1,4 @@ -// import { AudioBufferSourceNode, AnalyserNode, AudioContext, GainNode, OfflineAudioContext, StereoPannerNode, PeriodicWave, MediaStreamAudioSourceNode, mediaDevices } from '../index.mjs'; +import { AudioBufferSourceNode, AnalyserNode, AudioContext, GainNode, OfflineAudioContext, StereoPannerNode, PeriodicWave, MediaStreamAudioSourceNode, mediaDevices } from '../index.mjs'; // const mediaStream = await mediaDevices.getUserMedia({ audio: true }); @@ -20,9 +20,39 @@ // await context.startRendering(); +const SAMPLERATE = 8000; +const LENGTH = 128; -const key = Symbol('key'); +const oac = new OfflineAudioContext(1, LENGTH, SAMPLERATE); -const options = { [key]: 'value' }; - -console.log(key in options); +// var buf = oac.createBuffer(1, LENGTH, SAMPLERATE) +// var bs = new AudioBufferSourceNode(oac); +// var channelData = buf.getChannelData(0); +// for (var i = 0; i < channelData.length; i++) { +// channelData[i] = 1.0; +// } +// bs.buffer = buf; +// bs.start(); // This acquires the content since buf is not null +// for (var i = 0; i < channelData.length; i++) { +// channelData[i] = 0.5; +// } +// // allSamplesAtOne(buf, "reading back"); +// bs.connect(oac.destination); +// const output = await oac.startRendering(); + + +var buf = oac.createBuffer(1, LENGTH, SAMPLERATE) +var bs = new AudioBufferSourceNode(oac); +var channelData = buf.getChannelData(0); +for (var i = 0; i < channelData.length; i++) { + channelData[i] = 1.0; +} +bs.buffer = null; +bs.start(); // This does not acquire the content +bs.buffer = buf; // This does +for (var i = 0; i < channelData.length; i++) { + channelData[i] = 0.5; +} +// allSamplesAtOne(buf, "reading back"); +bs.connect(oac.destination); +const output = await oac.startRendering(); From 9dad8f1dadaa4fa9f99b3c0456811c1def89c8d3 Mon Sep 17 00:00:00 2001 From: b-ma Date: Wed, 13 Mar 2024 11:57:04 +0100 Subject: [PATCH 06/12] chore: link to upstream/main --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e0d46b3f..b43dba62 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,8 +13,8 @@ crate-type = ["cdylib"] napi = {version="2.15", features=["napi9", "tokio_rt"]} napi-derive = "2.15" uuid = {version="1.6.1", features = ["v4", "fast-rng"]} -web-audio-api = "1.0.0-rc.2" -# web-audio-api = { path = "../web-audio-api-rs" } +# web-audio-api = "1.0.0-rc.2" +web-audio-api = { path = "../web-audio-api-rs" } [target.'cfg(all(any(windows, unix), target_arch = "x86_64", not(target_env = "musl")))'.dependencies] mimalloc = {version = "0.1"} From 06739f18e75f2755be3dd09be2510f00d379f93f Mon Sep 17 00:00:00 2001 From: b-ma Date: Fri, 15 Mar 2024 18:57:49 +0100 Subject: [PATCH 07/12] chore: build against upstream --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b43dba62..e0d46b3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,8 +13,8 @@ crate-type = ["cdylib"] napi = {version="2.15", features=["napi9", "tokio_rt"]} napi-derive = "2.15" uuid = {version="1.6.1", features = ["v4", "fast-rng"]} -# web-audio-api = "1.0.0-rc.2" -web-audio-api = { path = "../web-audio-api-rs" } +web-audio-api = "1.0.0-rc.2" +# web-audio-api = { path = "../web-audio-api-rs" } [target.'cfg(all(any(windows, unix), target_arch = "x86_64", not(target_env = "musl")))'.dependencies] mimalloc = {version = "0.1"} From e723b42b6c26d80fb8f821425adf4710521d33aa Mon Sep 17 00:00:00 2001 From: b-ma Date: Sat, 16 Mar 2024 13:33:52 +0100 Subject: [PATCH 08/12] fix: check AudioBuffer in node contructor options --- generator/js/AudioNodes.tmpl.js | 119 +++++++++++++++++++++++-------- generator/rs/audio_nodes.tmpl.rs | 6 -- js/AnalyserNode.js | 20 ++++-- js/AudioBufferSourceNode.js | 38 +++++++--- js/BiquadFilterNode.js | 20 ++++-- js/ChannelMergerNode.js | 20 ++++-- js/ChannelSplitterNode.js | 20 ++++-- js/ConstantSourceNode.js | 22 ++++-- js/ConvolverNode.js | 36 +++++++--- js/DelayNode.js | 20 ++++-- js/DynamicsCompressorNode.js | 20 ++++-- js/GainNode.js | 20 ++++-- js/IIRFilterNode.js | 28 ++++++-- js/MediaStreamAudioSourceNode.js | 24 +++++-- js/OscillatorNode.js | 22 ++++-- js/PannerNode.js | 20 ++++-- js/StereoPannerNode.js | 20 ++++-- js/WaveShaperNode.js | 20 ++++-- tests/AudioBuffer.spec.mjs | 88 +++++++++++++++++++++-- tests/junk.mjs | 33 +++++---- 20 files changed, 453 insertions(+), 163 deletions(-) diff --git a/generator/js/AudioNodes.tmpl.js b/generator/js/AudioNodes.tmpl.js index 99f0d96e..46839107 100644 --- a/generator/js/AudioNodes.tmpl.js +++ b/generator/js/AudioNodes.tmpl.js @@ -4,58 +4,117 @@ const { throwSanitizedError } = require('./lib/errors.js'); const { AudioParam } = require('./AudioParam.js'); const EventTargetMixin = require('./EventTarget.mixin.js'); const AudioNodeMixin = require('./AudioNode.mixin.js'); - -const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); - ${d.parent(d.node) === 'AudioScheduledSourceNode' ? `const AudioScheduledSourceNodeMixin = require('./AudioScheduledSourceNode.mixin.js');`: ``} +const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); + module.exports = (Native${d.name(d.node)}) => { -${d.parent(d.node) === 'AudioScheduledSourceNode' ? ` const EventTarget = EventTargetMixin(Native${d.name(d.node)}, ['ended']); const AudioNode = AudioNodeMixin(EventTarget); +${d.parent(d.node) === 'AudioScheduledSourceNode' ? `\ const AudioScheduledSourceNode = AudioScheduledSourceNodeMixin(AudioNode); - class ${d.name(d.node)} extends AudioScheduledSourceNode { + class ${d.name(d.node)} extends AudioScheduledSourceNode {` : ` + class ${d.name(d.node)} extends AudioNode {` +} constructor(context, options) { - if (options !== undefined && typeof options !== 'object') { - throw new TypeError("Failed to construct '${d.name(d.node)}': argument 2 is not of type '${d.name(d.node).replace("Node", "Options")}'") + ${(function() { + // handle argument 2: options + const options = d.constructor(d.node).arguments[1]; + const optionsType = d.memberType(options); + const optionsIdl = d.findInTree(optionsType); + let checkOptions = ` + if (options !== undefined) { + if (typeof options !== 'object') { + throw new TypeError("Failed to construct '${d.name(d.node)}': argument 2 is not of type '${optionsType}'") + } + `; + + checkOptions += optionsIdl.members.map(member => { + // @todo - improve checks + // cf. https://github.com/jsdom/webidl-conversions + const optionName = d.name(member); + const type = d.memberType(member); + const required = member.required; + const nullable = member.idlType.nullable; + const defaultValue = member.default; // null or object + let checkMember = ''; + + if (required) { + checkMember += ` + if (options && !('${optionName}' in options)) { + throw new Error("Failed to read the '${optionName}'' property from ${optionsType}: Required member is undefined.") + } + ` + } + + // d.debug(member); + switch (type) { + case 'AudioBuffer': { + checkMember += ` + if ('${optionName}' in options && options.${optionName} !== null && !(kNativeAudioBuffer in options.${optionName} )) { + throw new TypeError("Failed to set the 'buffer' property on 'AudioBufferSourceNode': Failed to convert value to 'AudioBuffer'"); + } + // unwrap napi audio buffer + options.${optionName} = options.${optionName}[kNativeAudioBuffer]; + `; + break; + } + } + + return checkMember; + }).join(''); + + checkOptions += ` } + `; + + return checkOptions; + }())} super(context, options); - // EventTargetMixin has been called so EventTargetMixin[kDispatchEvent] is - // bound to this, then we can safely finalize event target initialization - super.__initEventTarget__(); -${d.audioParams(d.node).map(param => { - return ` - this.${d.name(param)} = new AudioParam(this.${d.name(param)});`; -}).join('')} - } -`: ` - const EventTarget = EventTargetMixin(Native${d.name(d.node)}); - const AudioNode = AudioNodeMixin(EventTarget); - class ${d.name(d.node)} extends AudioNode { - constructor(context, options) { - if (options !== undefined && typeof options !== 'object') { - throw new TypeError("Failed to construct '${d.name(d.node)}': argument 2 is not of type '${d.name(d.node).replace("Node", "Options")}'") + ${(function() { + // handle special options cases + const options = d.constructor(d.node).arguments[1]; + const optionsType = d.memberType(options); + const optionsIdl = d.findInTree(optionsType); + + return optionsIdl.members.map(member => { + // at this point all type checks have been done, so it is safe to just manipulate the options + const optionName = d.name(member); + const type = d.memberType(member); + // for audio buffer, we need to keep the wrapper around + if (type === 'AudioBuffer') { + return ` + if (options && '${optionName}' in options) { + this[kAudioBuffer] = options.${optionName}; } + `; + } + }).join(''); + }())} - super(context, options); -${d.audioParams(d.node).map(param => { - return ` + ${d.parent(d.node) === 'AudioScheduledSourceNode' ? ` + // EventTargetMixin constructor has been called so EventTargetMixin[kDispatchEvent] + // is bound to this, then we can safely finalize event target initialization + super.__initEventTarget__();` : ``} + + ${d.audioParams(d.node).map(param => { + return ` this.${d.name(param)} = new AudioParam(this.${d.name(param)});`; -}).join('')} + }).join('')} } -`} + // getters ${d.attributes(d.node).map(attr => { switch (d.memberType(attr)) { case 'AudioBuffer': { return ` get ${d.name(attr)}() { - if (this[kNativeAudioBuffer]) { - return this[kNativeAudioBuffer]; + if (this[kAudioBuffer]) { + return this[kAudioBuffer]; } else { return null; } @@ -92,7 +151,7 @@ ${d.attributes(d.node).filter(attr => !attr.readonly).map(attr => { throwSanitizedError(err); } - this[kNativeAudioBuffer] = value; + this[kAudioBuffer] = value; } `; break; diff --git a/generator/rs/audio_nodes.tmpl.rs b/generator/rs/audio_nodes.tmpl.rs index dcaa77df..3abcfdf4 100644 --- a/generator/rs/audio_nodes.tmpl.rs +++ b/generator/rs/audio_nodes.tmpl.rs @@ -133,16 +133,10 @@ fn constructor(ctx: CallContext) -> Result { // ---------------------------------------------- // parse options // ---------------------------------------------- - if (index == 0) { // index 0 is always AudioContext return; } - if (d.constructor(d.node).arguments.length != 2) { - console.log(d.node.name, 'constructor has arguments.length != 2'); - return ``; - } - const arg = d.constructor(d.node).arguments[1]; const argIdlType = d.memberType(arg); const argumentIdl = d.findInTree(argIdlType); diff --git a/js/AnalyserNode.js b/js/AnalyserNode.js index 74cd7fa1..803f4960 100644 --- a/js/AnalyserNode.js +++ b/js/AnalyserNode.js @@ -24,23 +24,31 @@ const { AudioParam } = require('./AudioParam.js'); const EventTargetMixin = require('./EventTarget.mixin.js'); const AudioNodeMixin = require('./AudioNode.mixin.js'); -const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); - +const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); module.exports = (NativeAnalyserNode) => { - - const EventTarget = EventTargetMixin(NativeAnalyserNode); + const EventTarget = EventTargetMixin(NativeAnalyserNode, ['ended']); const AudioNode = AudioNodeMixin(EventTarget); class AnalyserNode extends AudioNode { constructor(context, options) { - if (options !== undefined && typeof options !== 'object') { - throw new TypeError("Failed to construct 'AnalyserNode': argument 2 is not of type 'AnalyserOptions'") + + if (options !== undefined) { + if (typeof options !== 'object') { + throw new TypeError("Failed to construct 'AnalyserNode': argument 2 is not of type 'AnalyserOptions'") + } + } + super(context, options); + + + + + } // getters diff --git a/js/AudioBufferSourceNode.js b/js/AudioBufferSourceNode.js index 5a0496a1..69a25614 100644 --- a/js/AudioBufferSourceNode.js +++ b/js/AudioBufferSourceNode.js @@ -23,28 +23,46 @@ const { throwSanitizedError } = require('./lib/errors.js'); const { AudioParam } = require('./AudioParam.js'); const EventTargetMixin = require('./EventTarget.mixin.js'); const AudioNodeMixin = require('./AudioNode.mixin.js'); +const AudioScheduledSourceNodeMixin = require('./AudioScheduledSourceNode.mixin.js'); const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); -const AudioScheduledSourceNodeMixin = require('./AudioScheduledSourceNode.mixin.js'); - module.exports = (NativeAudioBufferSourceNode) => { - const EventTarget = EventTargetMixin(NativeAudioBufferSourceNode, ['ended']); const AudioNode = AudioNodeMixin(EventTarget); const AudioScheduledSourceNode = AudioScheduledSourceNodeMixin(AudioNode); class AudioBufferSourceNode extends AudioScheduledSourceNode { constructor(context, options) { - if (options !== undefined && typeof options !== 'object') { - throw new TypeError("Failed to construct 'AudioBufferSourceNode': argument 2 is not of type 'AudioBufferSourceOptions'") + + if (options !== undefined) { + if (typeof options !== 'object') { + throw new TypeError("Failed to construct 'AudioBufferSourceNode': argument 2 is not of type 'AudioBufferSourceOptions'") + } + + if ('buffer' in options && options.buffer !== null && !(kNativeAudioBuffer in options.buffer )) { + throw new TypeError("Failed to set the 'buffer' property on 'AudioBufferSourceNode': Failed to convert value to 'AudioBuffer'"); + } + // unwrap napi audio buffer + options.buffer = options.buffer[kNativeAudioBuffer]; + } + super(context, options); - // EventTargetMixin has been called so EventTargetMixin[kDispatchEvent] is - // bound to this, then we can safely finalize event target initialization + + + if (options && 'buffer' in options) { + this[kAudioBuffer] = options.buffer; + } + + + + // EventTargetMixin constructor has been called so EventTargetMixin[kDispatchEvent] + // is bound to this, then we can safely finalize event target initialization super.__initEventTarget__(); + this.playbackRate = new AudioParam(this.playbackRate); this.detune = new AudioParam(this.detune); } @@ -52,8 +70,8 @@ module.exports = (NativeAudioBufferSourceNode) => { // getters get buffer() { - if (this[kNativeAudioBuffer]) { - return this[kNativeAudioBuffer]; + if (this[kAudioBuffer]) { + return this[kAudioBuffer]; } else { return null; } @@ -87,7 +105,7 @@ module.exports = (NativeAudioBufferSourceNode) => { throwSanitizedError(err); } - this[kNativeAudioBuffer] = value; + this[kAudioBuffer] = value; } set loop(value) { diff --git a/js/BiquadFilterNode.js b/js/BiquadFilterNode.js index d924fd40..ec6f687f 100644 --- a/js/BiquadFilterNode.js +++ b/js/BiquadFilterNode.js @@ -24,23 +24,31 @@ const { AudioParam } = require('./AudioParam.js'); const EventTargetMixin = require('./EventTarget.mixin.js'); const AudioNodeMixin = require('./AudioNode.mixin.js'); -const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); - +const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); module.exports = (NativeBiquadFilterNode) => { - - const EventTarget = EventTargetMixin(NativeBiquadFilterNode); + const EventTarget = EventTargetMixin(NativeBiquadFilterNode, ['ended']); const AudioNode = AudioNodeMixin(EventTarget); class BiquadFilterNode extends AudioNode { constructor(context, options) { - if (options !== undefined && typeof options !== 'object') { - throw new TypeError("Failed to construct 'BiquadFilterNode': argument 2 is not of type 'BiquadFilterOptions'") + + if (options !== undefined) { + if (typeof options !== 'object') { + throw new TypeError("Failed to construct 'BiquadFilterNode': argument 2 is not of type 'BiquadFilterOptions'") + } + } + super(context, options); + + + + + this.frequency = new AudioParam(this.frequency); this.detune = new AudioParam(this.detune); this.Q = new AudioParam(this.Q); diff --git a/js/ChannelMergerNode.js b/js/ChannelMergerNode.js index a04a35b9..c4247c59 100644 --- a/js/ChannelMergerNode.js +++ b/js/ChannelMergerNode.js @@ -24,23 +24,31 @@ const { AudioParam } = require('./AudioParam.js'); const EventTargetMixin = require('./EventTarget.mixin.js'); const AudioNodeMixin = require('./AudioNode.mixin.js'); -const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); - +const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); module.exports = (NativeChannelMergerNode) => { - - const EventTarget = EventTargetMixin(NativeChannelMergerNode); + const EventTarget = EventTargetMixin(NativeChannelMergerNode, ['ended']); const AudioNode = AudioNodeMixin(EventTarget); class ChannelMergerNode extends AudioNode { constructor(context, options) { - if (options !== undefined && typeof options !== 'object') { - throw new TypeError("Failed to construct 'ChannelMergerNode': argument 2 is not of type 'ChannelMergerOptions'") + + if (options !== undefined) { + if (typeof options !== 'object') { + throw new TypeError("Failed to construct 'ChannelMergerNode': argument 2 is not of type 'ChannelMergerOptions'") + } + } + super(context, options); + + + + + } // getters diff --git a/js/ChannelSplitterNode.js b/js/ChannelSplitterNode.js index ebf77cad..0de13f88 100644 --- a/js/ChannelSplitterNode.js +++ b/js/ChannelSplitterNode.js @@ -24,23 +24,31 @@ const { AudioParam } = require('./AudioParam.js'); const EventTargetMixin = require('./EventTarget.mixin.js'); const AudioNodeMixin = require('./AudioNode.mixin.js'); -const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); - +const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); module.exports = (NativeChannelSplitterNode) => { - - const EventTarget = EventTargetMixin(NativeChannelSplitterNode); + const EventTarget = EventTargetMixin(NativeChannelSplitterNode, ['ended']); const AudioNode = AudioNodeMixin(EventTarget); class ChannelSplitterNode extends AudioNode { constructor(context, options) { - if (options !== undefined && typeof options !== 'object') { - throw new TypeError("Failed to construct 'ChannelSplitterNode': argument 2 is not of type 'ChannelSplitterOptions'") + + if (options !== undefined) { + if (typeof options !== 'object') { + throw new TypeError("Failed to construct 'ChannelSplitterNode': argument 2 is not of type 'ChannelSplitterOptions'") + } + } + super(context, options); + + + + + } // getters diff --git a/js/ConstantSourceNode.js b/js/ConstantSourceNode.js index b88ebfb0..a82d9017 100644 --- a/js/ConstantSourceNode.js +++ b/js/ConstantSourceNode.js @@ -23,28 +23,36 @@ const { throwSanitizedError } = require('./lib/errors.js'); const { AudioParam } = require('./AudioParam.js'); const EventTargetMixin = require('./EventTarget.mixin.js'); const AudioNodeMixin = require('./AudioNode.mixin.js'); +const AudioScheduledSourceNodeMixin = require('./AudioScheduledSourceNode.mixin.js'); const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); -const AudioScheduledSourceNodeMixin = require('./AudioScheduledSourceNode.mixin.js'); - module.exports = (NativeConstantSourceNode) => { - const EventTarget = EventTargetMixin(NativeConstantSourceNode, ['ended']); const AudioNode = AudioNodeMixin(EventTarget); const AudioScheduledSourceNode = AudioScheduledSourceNodeMixin(AudioNode); class ConstantSourceNode extends AudioScheduledSourceNode { constructor(context, options) { - if (options !== undefined && typeof options !== 'object') { - throw new TypeError("Failed to construct 'ConstantSourceNode': argument 2 is not of type 'ConstantSourceOptions'") + + if (options !== undefined) { + if (typeof options !== 'object') { + throw new TypeError("Failed to construct 'ConstantSourceNode': argument 2 is not of type 'ConstantSourceOptions'") + } + } + super(context, options); - // EventTargetMixin has been called so EventTargetMixin[kDispatchEvent] is - // bound to this, then we can safely finalize event target initialization + + + + + // EventTargetMixin constructor has been called so EventTargetMixin[kDispatchEvent] + // is bound to this, then we can safely finalize event target initialization super.__initEventTarget__(); + this.offset = new AudioParam(this.offset); } diff --git a/js/ConvolverNode.js b/js/ConvolverNode.js index e7f96c81..ce4f3dcd 100644 --- a/js/ConvolverNode.js +++ b/js/ConvolverNode.js @@ -24,30 +24,48 @@ const { AudioParam } = require('./AudioParam.js'); const EventTargetMixin = require('./EventTarget.mixin.js'); const AudioNodeMixin = require('./AudioNode.mixin.js'); -const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); - +const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); module.exports = (NativeConvolverNode) => { - - const EventTarget = EventTargetMixin(NativeConvolverNode); + const EventTarget = EventTargetMixin(NativeConvolverNode, ['ended']); const AudioNode = AudioNodeMixin(EventTarget); class ConvolverNode extends AudioNode { constructor(context, options) { - if (options !== undefined && typeof options !== 'object') { - throw new TypeError("Failed to construct 'ConvolverNode': argument 2 is not of type 'ConvolverOptions'") + + if (options !== undefined) { + if (typeof options !== 'object') { + throw new TypeError("Failed to construct 'ConvolverNode': argument 2 is not of type 'ConvolverOptions'") + } + + if ('buffer' in options && options.buffer !== null && !(kNativeAudioBuffer in options.buffer )) { + throw new TypeError("Failed to set the 'buffer' property on 'AudioBufferSourceNode': Failed to convert value to 'AudioBuffer'"); + } + // unwrap napi audio buffer + options.buffer = options.buffer[kNativeAudioBuffer]; + } + super(context, options); + + if (options && 'buffer' in options) { + this[kAudioBuffer] = options.buffer; + } + + + + + } // getters get buffer() { - if (this[kNativeAudioBuffer]) { - return this[kNativeAudioBuffer]; + if (this[kAudioBuffer]) { + return this[kAudioBuffer]; } else { return null; } @@ -73,7 +91,7 @@ module.exports = (NativeConvolverNode) => { throwSanitizedError(err); } - this[kNativeAudioBuffer] = value; + this[kAudioBuffer] = value; } set normalize(value) { diff --git a/js/DelayNode.js b/js/DelayNode.js index f9b97e40..fd27141f 100644 --- a/js/DelayNode.js +++ b/js/DelayNode.js @@ -24,23 +24,31 @@ const { AudioParam } = require('./AudioParam.js'); const EventTargetMixin = require('./EventTarget.mixin.js'); const AudioNodeMixin = require('./AudioNode.mixin.js'); -const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); - +const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); module.exports = (NativeDelayNode) => { - - const EventTarget = EventTargetMixin(NativeDelayNode); + const EventTarget = EventTargetMixin(NativeDelayNode, ['ended']); const AudioNode = AudioNodeMixin(EventTarget); class DelayNode extends AudioNode { constructor(context, options) { - if (options !== undefined && typeof options !== 'object') { - throw new TypeError("Failed to construct 'DelayNode': argument 2 is not of type 'DelayOptions'") + + if (options !== undefined) { + if (typeof options !== 'object') { + throw new TypeError("Failed to construct 'DelayNode': argument 2 is not of type 'DelayOptions'") + } + } + super(context, options); + + + + + this.delayTime = new AudioParam(this.delayTime); } diff --git a/js/DynamicsCompressorNode.js b/js/DynamicsCompressorNode.js index c4535678..1bd4e0cf 100644 --- a/js/DynamicsCompressorNode.js +++ b/js/DynamicsCompressorNode.js @@ -24,23 +24,31 @@ const { AudioParam } = require('./AudioParam.js'); const EventTargetMixin = require('./EventTarget.mixin.js'); const AudioNodeMixin = require('./AudioNode.mixin.js'); -const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); - +const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); module.exports = (NativeDynamicsCompressorNode) => { - - const EventTarget = EventTargetMixin(NativeDynamicsCompressorNode); + const EventTarget = EventTargetMixin(NativeDynamicsCompressorNode, ['ended']); const AudioNode = AudioNodeMixin(EventTarget); class DynamicsCompressorNode extends AudioNode { constructor(context, options) { - if (options !== undefined && typeof options !== 'object') { - throw new TypeError("Failed to construct 'DynamicsCompressorNode': argument 2 is not of type 'DynamicsCompressorOptions'") + + if (options !== undefined) { + if (typeof options !== 'object') { + throw new TypeError("Failed to construct 'DynamicsCompressorNode': argument 2 is not of type 'DynamicsCompressorOptions'") + } + } + super(context, options); + + + + + this.threshold = new AudioParam(this.threshold); this.knee = new AudioParam(this.knee); this.ratio = new AudioParam(this.ratio); diff --git a/js/GainNode.js b/js/GainNode.js index 459007f9..da4e3773 100644 --- a/js/GainNode.js +++ b/js/GainNode.js @@ -24,23 +24,31 @@ const { AudioParam } = require('./AudioParam.js'); const EventTargetMixin = require('./EventTarget.mixin.js'); const AudioNodeMixin = require('./AudioNode.mixin.js'); -const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); - +const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); module.exports = (NativeGainNode) => { - - const EventTarget = EventTargetMixin(NativeGainNode); + const EventTarget = EventTargetMixin(NativeGainNode, ['ended']); const AudioNode = AudioNodeMixin(EventTarget); class GainNode extends AudioNode { constructor(context, options) { - if (options !== undefined && typeof options !== 'object') { - throw new TypeError("Failed to construct 'GainNode': argument 2 is not of type 'GainOptions'") + + if (options !== undefined) { + if (typeof options !== 'object') { + throw new TypeError("Failed to construct 'GainNode': argument 2 is not of type 'GainOptions'") + } + } + super(context, options); + + + + + this.gain = new AudioParam(this.gain); } diff --git a/js/IIRFilterNode.js b/js/IIRFilterNode.js index ae9a6407..3a34922e 100644 --- a/js/IIRFilterNode.js +++ b/js/IIRFilterNode.js @@ -24,23 +24,39 @@ const { AudioParam } = require('./AudioParam.js'); const EventTargetMixin = require('./EventTarget.mixin.js'); const AudioNodeMixin = require('./AudioNode.mixin.js'); -const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); - +const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); module.exports = (NativeIIRFilterNode) => { - - const EventTarget = EventTargetMixin(NativeIIRFilterNode); + const EventTarget = EventTargetMixin(NativeIIRFilterNode, ['ended']); const AudioNode = AudioNodeMixin(EventTarget); class IIRFilterNode extends AudioNode { constructor(context, options) { - if (options !== undefined && typeof options !== 'object') { - throw new TypeError("Failed to construct 'IIRFilterNode': argument 2 is not of type 'IIRFilterOptions'") + + if (options !== undefined) { + if (typeof options !== 'object') { + throw new TypeError("Failed to construct 'IIRFilterNode': argument 2 is not of type 'IIRFilterOptions'") + } + + if (options && !('feedforward' in options)) { + throw new Error("Failed to read the 'feedforward'' property from IIRFilterOptions: Required member is undefined.") + } + + if (options && !('feedback' in options)) { + throw new Error("Failed to read the 'feedback'' property from IIRFilterOptions: Required member is undefined.") + } + } + super(context, options); + + + + + } // getters diff --git a/js/MediaStreamAudioSourceNode.js b/js/MediaStreamAudioSourceNode.js index 6d8c3b1a..5f94adb6 100644 --- a/js/MediaStreamAudioSourceNode.js +++ b/js/MediaStreamAudioSourceNode.js @@ -24,23 +24,35 @@ const { AudioParam } = require('./AudioParam.js'); const EventTargetMixin = require('./EventTarget.mixin.js'); const AudioNodeMixin = require('./AudioNode.mixin.js'); -const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); - +const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); module.exports = (NativeMediaStreamAudioSourceNode) => { - - const EventTarget = EventTargetMixin(NativeMediaStreamAudioSourceNode); + const EventTarget = EventTargetMixin(NativeMediaStreamAudioSourceNode, ['ended']); const AudioNode = AudioNodeMixin(EventTarget); class MediaStreamAudioSourceNode extends AudioNode { constructor(context, options) { - if (options !== undefined && typeof options !== 'object') { - throw new TypeError("Failed to construct 'MediaStreamAudioSourceNode': argument 2 is not of type 'MediaStreamAudioSourceOptions'") + + if (options !== undefined) { + if (typeof options !== 'object') { + throw new TypeError("Failed to construct 'MediaStreamAudioSourceNode': argument 2 is not of type 'MediaStreamAudioSourceOptions'") + } + + if (options && !('mediaStream' in options)) { + throw new Error("Failed to read the 'mediaStream'' property from MediaStreamAudioSourceOptions: Required member is undefined.") + } + } + super(context, options); + + + + + } // getters diff --git a/js/OscillatorNode.js b/js/OscillatorNode.js index ebaf7c9f..36f585c6 100644 --- a/js/OscillatorNode.js +++ b/js/OscillatorNode.js @@ -23,28 +23,36 @@ const { throwSanitizedError } = require('./lib/errors.js'); const { AudioParam } = require('./AudioParam.js'); const EventTargetMixin = require('./EventTarget.mixin.js'); const AudioNodeMixin = require('./AudioNode.mixin.js'); +const AudioScheduledSourceNodeMixin = require('./AudioScheduledSourceNode.mixin.js'); const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); -const AudioScheduledSourceNodeMixin = require('./AudioScheduledSourceNode.mixin.js'); - module.exports = (NativeOscillatorNode) => { - const EventTarget = EventTargetMixin(NativeOscillatorNode, ['ended']); const AudioNode = AudioNodeMixin(EventTarget); const AudioScheduledSourceNode = AudioScheduledSourceNodeMixin(AudioNode); class OscillatorNode extends AudioScheduledSourceNode { constructor(context, options) { - if (options !== undefined && typeof options !== 'object') { - throw new TypeError("Failed to construct 'OscillatorNode': argument 2 is not of type 'OscillatorOptions'") + + if (options !== undefined) { + if (typeof options !== 'object') { + throw new TypeError("Failed to construct 'OscillatorNode': argument 2 is not of type 'OscillatorOptions'") + } + } + super(context, options); - // EventTargetMixin has been called so EventTargetMixin[kDispatchEvent] is - // bound to this, then we can safely finalize event target initialization + + + + + // EventTargetMixin constructor has been called so EventTargetMixin[kDispatchEvent] + // is bound to this, then we can safely finalize event target initialization super.__initEventTarget__(); + this.frequency = new AudioParam(this.frequency); this.detune = new AudioParam(this.detune); } diff --git a/js/PannerNode.js b/js/PannerNode.js index 4cab41e2..accad7cb 100644 --- a/js/PannerNode.js +++ b/js/PannerNode.js @@ -24,23 +24,31 @@ const { AudioParam } = require('./AudioParam.js'); const EventTargetMixin = require('./EventTarget.mixin.js'); const AudioNodeMixin = require('./AudioNode.mixin.js'); -const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); - +const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); module.exports = (NativePannerNode) => { - - const EventTarget = EventTargetMixin(NativePannerNode); + const EventTarget = EventTargetMixin(NativePannerNode, ['ended']); const AudioNode = AudioNodeMixin(EventTarget); class PannerNode extends AudioNode { constructor(context, options) { - if (options !== undefined && typeof options !== 'object') { - throw new TypeError("Failed to construct 'PannerNode': argument 2 is not of type 'PannerOptions'") + + if (options !== undefined) { + if (typeof options !== 'object') { + throw new TypeError("Failed to construct 'PannerNode': argument 2 is not of type 'PannerOptions'") + } + } + super(context, options); + + + + + this.positionX = new AudioParam(this.positionX); this.positionY = new AudioParam(this.positionY); this.positionZ = new AudioParam(this.positionZ); diff --git a/js/StereoPannerNode.js b/js/StereoPannerNode.js index 839c69f2..4f27707a 100644 --- a/js/StereoPannerNode.js +++ b/js/StereoPannerNode.js @@ -24,23 +24,31 @@ const { AudioParam } = require('./AudioParam.js'); const EventTargetMixin = require('./EventTarget.mixin.js'); const AudioNodeMixin = require('./AudioNode.mixin.js'); -const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); - +const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); module.exports = (NativeStereoPannerNode) => { - - const EventTarget = EventTargetMixin(NativeStereoPannerNode); + const EventTarget = EventTargetMixin(NativeStereoPannerNode, ['ended']); const AudioNode = AudioNodeMixin(EventTarget); class StereoPannerNode extends AudioNode { constructor(context, options) { - if (options !== undefined && typeof options !== 'object') { - throw new TypeError("Failed to construct 'StereoPannerNode': argument 2 is not of type 'StereoPannerOptions'") + + if (options !== undefined) { + if (typeof options !== 'object') { + throw new TypeError("Failed to construct 'StereoPannerNode': argument 2 is not of type 'StereoPannerOptions'") + } + } + super(context, options); + + + + + this.pan = new AudioParam(this.pan); } diff --git a/js/WaveShaperNode.js b/js/WaveShaperNode.js index 6d2367b1..303970f2 100644 --- a/js/WaveShaperNode.js +++ b/js/WaveShaperNode.js @@ -24,23 +24,31 @@ const { AudioParam } = require('./AudioParam.js'); const EventTargetMixin = require('./EventTarget.mixin.js'); const AudioNodeMixin = require('./AudioNode.mixin.js'); -const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); - +const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js'); module.exports = (NativeWaveShaperNode) => { - - const EventTarget = EventTargetMixin(NativeWaveShaperNode); + const EventTarget = EventTargetMixin(NativeWaveShaperNode, ['ended']); const AudioNode = AudioNodeMixin(EventTarget); class WaveShaperNode extends AudioNode { constructor(context, options) { - if (options !== undefined && typeof options !== 'object') { - throw new TypeError("Failed to construct 'WaveShaperNode': argument 2 is not of type 'WaveShaperOptions'") + + if (options !== undefined) { + if (typeof options !== 'object') { + throw new TypeError("Failed to construct 'WaveShaperNode': argument 2 is not of type 'WaveShaperOptions'") + } + } + super(context, options); + + + + + } // getters diff --git a/tests/AudioBuffer.spec.mjs b/tests/AudioBuffer.spec.mjs index ebceef63..aa6f6b9d 100644 --- a/tests/AudioBuffer.spec.mjs +++ b/tests/AudioBuffer.spec.mjs @@ -1,7 +1,13 @@ import path from 'node:path'; import fs from 'node:fs'; import { assert } from 'chai'; -import { AudioBuffer, AudioContext, OfflineAudioContext } from '../index.mjs'; +import { + AudioBuffer, + AudioBufferSourceNode, + AudioContext, + ConvolverNode, + OfflineAudioContext +} from '../index.mjs'; describe('# AudioBuffer', () => { @@ -101,8 +107,8 @@ describe('# AudioBuffer', () => { }); }); - describe(`AudioBufferSourceNode.buffer`, () => { - it(`should work properly`, async () => { + describe(`buffer attribute`, () => { + it(`AudioBufferSourceNode`, async () => { const audioContext = new AudioContext(); const pathname = path.join('examples', 'samples', 'sample.wav'); @@ -117,11 +123,83 @@ describe('# AudioBuffer', () => { assert.deepEqual(src.buffer, audioBuffer); src.start(audioContext.currentTime); - src.stop(audioContext.currentTime + 0.3); + src.stop(audioContext.currentTime + 0.1); - await new Promise(resolve => setTimeout(resolve, 400)); + await new Promise(resolve => setTimeout(resolve, 200)); await audioContext.close(); }); + + it(`ConvolverNode`, async () => { + const audioContext = new AudioContext(); + + const pathname = path.join('examples', 'samples', 'sample.wav'); + const buffer = fs.readFileSync(pathname).buffer; + const audioBuffer = await audioContext.decodeAudioData(buffer); + + const convolver = audioContext.createConvolver(); + convolver.buffer = audioBuffer; + convolver.connect(audioContext.destination); + + const src = audioContext.createBufferSource(); + // should retrieve native audio buffer to native buffer source node + src.buffer = audioBuffer; + src.connect(convolver); + + assert.deepEqual(src.buffer, audioBuffer); + + src.start(audioContext.currentTime); + src.stop(audioContext.currentTime + 0.1); + + await new Promise(resolve => setTimeout(resolve, 200)); + await audioContext.close(); + }); + }); + + describe(`buffer in options`, () => { + it.only(`AudioBufferSourceNode`, async () => { + const audioContext = new AudioContext(); + + const pathname = path.join('examples', 'samples', 'sample.wav'); + const buffer = fs.readFileSync(pathname).buffer; + const audioBuffer = await audioContext.decodeAudioData(buffer); + + // should retrieve native audio buffer to native buffer source node + const src = new AudioBufferSourceNode(audioContext, { buffer: audioBuffer }); + src.connect(audioContext.destination); + + assert.deepEqual(src.buffer, audioBuffer); + + src.start(audioContext.currentTime); + src.stop(audioContext.currentTime + 0.1); + + await new Promise(resolve => setTimeout(resolve, 200)); + await audioContext.close(); + }); + + // it.only(`ConvolverNode`, async () => { + // const audioContext = new AudioContext(); + + // const pathname = path.join('examples', 'samples', 'sample.wav'); + // const buffer = fs.readFileSync(pathname).buffer; + // const audioBuffer = await audioContext.decodeAudioData(buffer); + + // const convolver = audioContext.createConvolver(); + // convolver.buffer = audioBuffer; + // convolver.connect(audioContext.destination); + + // const src = audioContext.createBufferSource(); + // // should retrieve native audio buffer to native buffer source node + // src.buffer = audioBuffer; + // src.connect(convolver); + + // assert.deepEqual(src.buffer, audioBuffer); + + // src.start(audioContext.currentTime); + // src.stop(audioContext.currentTime + 0.1); + + // await new Promise(resolve => setTimeout(resolve, 200)); + // await audioContext.close(); + // }); }); }); diff --git a/tests/junk.mjs b/tests/junk.mjs index 67def1aa..a6e2cf27 100644 --- a/tests/junk.mjs +++ b/tests/junk.mjs @@ -1,4 +1,4 @@ -import { AudioBufferSourceNode, AnalyserNode, AudioContext, GainNode, OfflineAudioContext, StereoPannerNode, PeriodicWave, MediaStreamAudioSourceNode, mediaDevices } from '../index.mjs'; +import { AudioBuffer, AudioBufferSourceNode, AnalyserNode, AudioContext, DelayNode, GainNode, OfflineAudioContext, StereoPannerNode, PeriodicWave, MediaStreamAudioSourceNode, mediaDevices } from '../index.mjs'; // const mediaStream = await mediaDevices.getUserMedia({ audio: true }); @@ -40,19 +40,18 @@ const oac = new OfflineAudioContext(1, LENGTH, SAMPLERATE); // bs.connect(oac.destination); // const output = await oac.startRendering(); - -var buf = oac.createBuffer(1, LENGTH, SAMPLERATE) -var bs = new AudioBufferSourceNode(oac); -var channelData = buf.getChannelData(0); -for (var i = 0; i < channelData.length; i++) { - channelData[i] = 1.0; -} -bs.buffer = null; -bs.start(); // This does not acquire the content -bs.buffer = buf; // This does -for (var i = 0; i < channelData.length; i++) { - channelData[i] = 0.5; -} -// allSamplesAtOne(buf, "reading back"); -bs.connect(oac.destination); -const output = await oac.startRendering(); +let off = new OfflineAudioContext(1, 512, 48000); +let b = new AudioBuffer({sampleRate: off.sampleRate, length: 1}); +b.getChannelData(0)[0] = 1; +let impulse = new AudioBufferSourceNode(off, {buffer: b}); +impulse.start(0); +// This delayTime of 64 samples MUST be clamped to 128 samples when +// in a cycle. +let delay = new DelayNode(off, {delayTime: 64 / 48000}); +let fb = new GainNode(off); +impulse.connect(fb).connect(delay).connect(fb).connect(off.destination); + +off.startRendering().then((b) => { + // return Promise.resolve(b.getChannelData(0)); + console.log(b.getChannelData(0)); +}) From 56f37253084a47b888a30d5af0b8ff6fc3747ce9 Mon Sep 17 00:00:00 2001 From: b-ma Date: Sat, 16 Mar 2024 14:04:17 +0100 Subject: [PATCH 09/12] fix: do not pollute options object --- generator/js/AudioNodes.tmpl.js | 3 ++- js/AudioBufferSourceNode.js | 3 ++- js/ConvolverNode.js | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/generator/js/AudioNodes.tmpl.js b/generator/js/AudioNodes.tmpl.js index 46839107..067993cc 100644 --- a/generator/js/AudioNodes.tmpl.js +++ b/generator/js/AudioNodes.tmpl.js @@ -56,7 +56,8 @@ ${d.parent(d.node) === 'AudioScheduledSourceNode' ? `\ if ('${optionName}' in options && options.${optionName} !== null && !(kNativeAudioBuffer in options.${optionName} )) { throw new TypeError("Failed to set the 'buffer' property on 'AudioBufferSourceNode': Failed to convert value to 'AudioBuffer'"); } - // unwrap napi audio buffer + // unwrap napi audio buffer, clone the options object as it might be reused + options = Object.assign({}, options); options.${optionName} = options.${optionName}[kNativeAudioBuffer]; `; break; diff --git a/js/AudioBufferSourceNode.js b/js/AudioBufferSourceNode.js index 69a25614..38ff6af8 100644 --- a/js/AudioBufferSourceNode.js +++ b/js/AudioBufferSourceNode.js @@ -43,7 +43,8 @@ module.exports = (NativeAudioBufferSourceNode) => { if ('buffer' in options && options.buffer !== null && !(kNativeAudioBuffer in options.buffer )) { throw new TypeError("Failed to set the 'buffer' property on 'AudioBufferSourceNode': Failed to convert value to 'AudioBuffer'"); } - // unwrap napi audio buffer + // unwrap napi audio buffer, clone the options object as it might be reused + options = Object.assign({}, options); options.buffer = options.buffer[kNativeAudioBuffer]; } diff --git a/js/ConvolverNode.js b/js/ConvolverNode.js index ce4f3dcd..b889ad86 100644 --- a/js/ConvolverNode.js +++ b/js/ConvolverNode.js @@ -42,7 +42,8 @@ module.exports = (NativeConvolverNode) => { if ('buffer' in options && options.buffer !== null && !(kNativeAudioBuffer in options.buffer )) { throw new TypeError("Failed to set the 'buffer' property on 'AudioBufferSourceNode': Failed to convert value to 'AudioBuffer'"); } - // unwrap napi audio buffer + // unwrap napi audio buffer, clone the options object as it might be reused + options = Object.assign({}, options); options.buffer = options.buffer[kNativeAudioBuffer]; } From 0d5c060ff55db5e011d12e09b43d9f9d0608748f Mon Sep 17 00:00:00 2001 From: b-ma Date: Sat, 16 Mar 2024 17:18:02 +0100 Subject: [PATCH 10/12] fix: do not try to unwrap null --- generator/js/AudioNodes.tmpl.js | 15 +++++++++----- js/AudioBufferSourceNode.js | 15 +++++++++----- js/ConvolverNode.js | 15 +++++++++----- tests/junk.mjs | 35 +++------------------------------ 4 files changed, 33 insertions(+), 47 deletions(-) diff --git a/generator/js/AudioNodes.tmpl.js b/generator/js/AudioNodes.tmpl.js index 067993cc..5949d4da 100644 --- a/generator/js/AudioNodes.tmpl.js +++ b/generator/js/AudioNodes.tmpl.js @@ -53,12 +53,17 @@ ${d.parent(d.node) === 'AudioScheduledSourceNode' ? `\ switch (type) { case 'AudioBuffer': { checkMember += ` - if ('${optionName}' in options && options.${optionName} !== null && !(kNativeAudioBuffer in options.${optionName} )) { - throw new TypeError("Failed to set the 'buffer' property on 'AudioBufferSourceNode': Failed to convert value to 'AudioBuffer'"); + if ('${optionName}' in options) { + if (options.${optionName} !== null) { + if (!(kNativeAudioBuffer in options.${optionName})) { + throw new TypeError("Failed to set the 'buffer' property on 'AudioBufferSourceNode': Failed to convert value to 'AudioBuffer'"); + } + + // unwrap napi audio buffer, clone the options object as it might be reused + options = Object.assign({}, options); + options.${optionName} = options.${optionName}[kNativeAudioBuffer]; + } } - // unwrap napi audio buffer, clone the options object as it might be reused - options = Object.assign({}, options); - options.${optionName} = options.${optionName}[kNativeAudioBuffer]; `; break; } diff --git a/js/AudioBufferSourceNode.js b/js/AudioBufferSourceNode.js index 38ff6af8..06ae3542 100644 --- a/js/AudioBufferSourceNode.js +++ b/js/AudioBufferSourceNode.js @@ -40,12 +40,17 @@ module.exports = (NativeAudioBufferSourceNode) => { throw new TypeError("Failed to construct 'AudioBufferSourceNode': argument 2 is not of type 'AudioBufferSourceOptions'") } - if ('buffer' in options && options.buffer !== null && !(kNativeAudioBuffer in options.buffer )) { - throw new TypeError("Failed to set the 'buffer' property on 'AudioBufferSourceNode': Failed to convert value to 'AudioBuffer'"); + if ('buffer' in options) { + if (options.buffer !== null) { + if (!(kNativeAudioBuffer in options.buffer)) { + throw new TypeError("Failed to set the 'buffer' property on 'AudioBufferSourceNode': Failed to convert value to 'AudioBuffer'"); + } + + // unwrap napi audio buffer, clone the options object as it might be reused + options = Object.assign({}, options); + options.buffer = options.buffer[kNativeAudioBuffer]; + } } - // unwrap napi audio buffer, clone the options object as it might be reused - options = Object.assign({}, options); - options.buffer = options.buffer[kNativeAudioBuffer]; } diff --git a/js/ConvolverNode.js b/js/ConvolverNode.js index b889ad86..f6834002 100644 --- a/js/ConvolverNode.js +++ b/js/ConvolverNode.js @@ -39,12 +39,17 @@ module.exports = (NativeConvolverNode) => { throw new TypeError("Failed to construct 'ConvolverNode': argument 2 is not of type 'ConvolverOptions'") } - if ('buffer' in options && options.buffer !== null && !(kNativeAudioBuffer in options.buffer )) { - throw new TypeError("Failed to set the 'buffer' property on 'AudioBufferSourceNode': Failed to convert value to 'AudioBuffer'"); + if ('buffer' in options) { + if (options.buffer !== null) { + if (!(kNativeAudioBuffer in options.buffer)) { + throw new TypeError("Failed to set the 'buffer' property on 'AudioBufferSourceNode': Failed to convert value to 'AudioBuffer'"); + } + + // unwrap napi audio buffer, clone the options object as it might be reused + options = Object.assign({}, options); + options.buffer = options.buffer[kNativeAudioBuffer]; + } } - // unwrap napi audio buffer, clone the options object as it might be reused - options = Object.assign({}, options); - options.buffer = options.buffer[kNativeAudioBuffer]; } diff --git a/tests/junk.mjs b/tests/junk.mjs index a6e2cf27..349d391e 100644 --- a/tests/junk.mjs +++ b/tests/junk.mjs @@ -1,4 +1,4 @@ -import { AudioBuffer, AudioBufferSourceNode, AnalyserNode, AudioContext, DelayNode, GainNode, OfflineAudioContext, StereoPannerNode, PeriodicWave, MediaStreamAudioSourceNode, mediaDevices } from '../index.mjs'; +import { AudioBuffer, AudioBufferSourceNode, AnalyserNode, AudioContext, ConvolverNode, DelayNode, GainNode, OfflineAudioContext, StereoPannerNode, PeriodicWave, MediaStreamAudioSourceNode, mediaDevices } from '../index.mjs'; // const mediaStream = await mediaDevices.getUserMedia({ audio: true }); @@ -23,35 +23,6 @@ import { AudioBuffer, AudioBufferSourceNode, AnalyserNode, AudioContext, DelayNo const SAMPLERATE = 8000; const LENGTH = 128; -const oac = new OfflineAudioContext(1, LENGTH, SAMPLERATE); +const context = new OfflineAudioContext(1, LENGTH, SAMPLERATE); -// var buf = oac.createBuffer(1, LENGTH, SAMPLERATE) -// var bs = new AudioBufferSourceNode(oac); -// var channelData = buf.getChannelData(0); -// for (var i = 0; i < channelData.length; i++) { -// channelData[i] = 1.0; -// } -// bs.buffer = buf; -// bs.start(); // This acquires the content since buf is not null -// for (var i = 0; i < channelData.length; i++) { -// channelData[i] = 0.5; -// } -// // allSamplesAtOne(buf, "reading back"); -// bs.connect(oac.destination); -// const output = await oac.startRendering(); - -let off = new OfflineAudioContext(1, 512, 48000); -let b = new AudioBuffer({sampleRate: off.sampleRate, length: 1}); -b.getChannelData(0)[0] = 1; -let impulse = new AudioBufferSourceNode(off, {buffer: b}); -impulse.start(0); -// This delayTime of 64 samples MUST be clamped to 128 samples when -// in a cycle. -let delay = new DelayNode(off, {delayTime: 64 / 48000}); -let fb = new GainNode(off); -impulse.connect(fb).connect(delay).connect(fb).connect(off.destination); - -off.startRendering().then((b) => { - // return Promise.resolve(b.getChannelData(0)); - console.log(b.getChannelData(0)); -}) +const node3 = new ConvolverNode(context, {"buffer":null,"disableNormalization":false}) From f8be9043cc307fa83446b3da45ccdb993e18b0f1 Mon Sep 17 00:00:00 2001 From: b-ma Date: Sun, 17 Mar 2024 12:34:35 +0100 Subject: [PATCH 11/12] fix: improve AudioNode options handling --- generator/js/AudioNodes.tmpl.js | 16 +++++++++------- js/AnalyserNode.js | 4 ++++ js/AudioBuffer.js | 8 ++++---- js/AudioBufferSourceNode.js | 15 +++++++++------ js/BiquadFilterNode.js | 4 ++++ js/ChannelMergerNode.js | 4 ++++ js/ChannelSplitterNode.js | 4 ++++ js/ConstantSourceNode.js | 4 ++++ js/ConvolverNode.js | 15 +++++++++------ js/DelayNode.js | 4 ++++ js/DynamicsCompressorNode.js | 4 ++++ js/GainNode.js | 4 ++++ js/IIRFilterNode.js | 4 ++++ js/MediaStreamAudioSourceNode.js | 4 ++++ js/OscillatorNode.js | 4 ++++ js/PannerNode.js | 4 ++++ js/StereoPannerNode.js | 4 ++++ js/WaveShaperNode.js | 4 ++++ tests/AudioBuffer.spec.mjs | 3 +++ tests/junk.mjs | 13 +++++++++++-- 20 files changed, 101 insertions(+), 25 deletions(-) diff --git a/generator/js/AudioNodes.tmpl.js b/generator/js/AudioNodes.tmpl.js index 5949d4da..a6d87479 100644 --- a/generator/js/AudioNodes.tmpl.js +++ b/generator/js/AudioNodes.tmpl.js @@ -19,6 +19,10 @@ ${d.parent(d.node) === 'AudioScheduledSourceNode' ? `\ class ${d.name(d.node)} extends AudioNode {` } constructor(context, options) { + // keep a handle to the original object, if we need to manipulate the + // options before passing them to NAPI + const originalOptions = Object.assign({}, options); + ${(function() { // handle argument 2: options const options = d.constructor(d.node).arguments[1]; @@ -91,11 +95,13 @@ ${d.parent(d.node) === 'AudioScheduledSourceNode' ? `\ // at this point all type checks have been done, so it is safe to just manipulate the options const optionName = d.name(member); const type = d.memberType(member); - // for audio buffer, we need to keep the wrapper around if (type === 'AudioBuffer') { return ` + // keep the wrapper AudioBuffer wrapperaround + this[kAudioBuffer] = null; + if (options && '${optionName}' in options) { - this[kAudioBuffer] = options.${optionName}; + this[kAudioBuffer] = originalOptions.${optionName}; } `; } @@ -119,11 +125,7 @@ ${d.attributes(d.node).map(attr => { case 'AudioBuffer': { return ` get ${d.name(attr)}() { - if (this[kAudioBuffer]) { - return this[kAudioBuffer]; - } else { - return null; - } + return this[kAudioBuffer]; } `; break; diff --git a/js/AnalyserNode.js b/js/AnalyserNode.js index 803f4960..6fabc4ac 100644 --- a/js/AnalyserNode.js +++ b/js/AnalyserNode.js @@ -33,6 +33,10 @@ module.exports = (NativeAnalyserNode) => { class AnalyserNode extends AudioNode { constructor(context, options) { + // keep a handle to the original object, if we need to manipulate the + // options before passing them to NAPI + const originalOptions = Object.assign({}, options); + if (options !== undefined) { if (typeof options !== 'object') { diff --git a/js/AudioBuffer.js b/js/AudioBuffer.js index dcadb0d7..630b5fb6 100644 --- a/js/AudioBuffer.js +++ b/js/AudioBuffer.js @@ -6,15 +6,15 @@ const kAudioBuffer = Symbol('node-web-audio-api:audio-buffer'); module.exports.AudioBuffer = (NativeAudioBuffer) => { class AudioBuffer { constructor(options) { + if (typeof options !== 'object') { + throw new TypeError("Failed to construct 'AudioBuffer': argument 1 is not of type 'AudioBufferOptions'"); + } + if (kNativeAudioBuffer in options) { // internal constructor for `startRendering` and `decodeAudioData` cases this[kNativeAudioBuffer] = options[kNativeAudioBuffer]; } else { // regular public constructor - if (typeof options !== 'object') { - throw new TypeError("Failed to construct 'AudioBuffer': argument 1 is not of type 'AudioBufferOptions'"); - } - try { this[kNativeAudioBuffer] = new NativeAudioBuffer(options); } catch (err) { diff --git a/js/AudioBufferSourceNode.js b/js/AudioBufferSourceNode.js index 06ae3542..364b9c42 100644 --- a/js/AudioBufferSourceNode.js +++ b/js/AudioBufferSourceNode.js @@ -34,6 +34,10 @@ module.exports = (NativeAudioBufferSourceNode) => { class AudioBufferSourceNode extends AudioScheduledSourceNode { constructor(context, options) { + // keep a handle to the original object, if we need to manipulate the + // options before passing them to NAPI + const originalOptions = Object.assign({}, options); + if (options !== undefined) { if (typeof options !== 'object') { @@ -58,8 +62,11 @@ module.exports = (NativeAudioBufferSourceNode) => { super(context, options); + // keep the wrapper AudioBuffer wrapperaround + this[kAudioBuffer] = null; + if (options && 'buffer' in options) { - this[kAudioBuffer] = options.buffer; + this[kAudioBuffer] = originalOptions.buffer; } @@ -76,11 +83,7 @@ module.exports = (NativeAudioBufferSourceNode) => { // getters get buffer() { - if (this[kAudioBuffer]) { - return this[kAudioBuffer]; - } else { - return null; - } + return this[kAudioBuffer]; } get loop() { diff --git a/js/BiquadFilterNode.js b/js/BiquadFilterNode.js index ec6f687f..fbaff70a 100644 --- a/js/BiquadFilterNode.js +++ b/js/BiquadFilterNode.js @@ -33,6 +33,10 @@ module.exports = (NativeBiquadFilterNode) => { class BiquadFilterNode extends AudioNode { constructor(context, options) { + // keep a handle to the original object, if we need to manipulate the + // options before passing them to NAPI + const originalOptions = Object.assign({}, options); + if (options !== undefined) { if (typeof options !== 'object') { diff --git a/js/ChannelMergerNode.js b/js/ChannelMergerNode.js index c4247c59..18849b5a 100644 --- a/js/ChannelMergerNode.js +++ b/js/ChannelMergerNode.js @@ -33,6 +33,10 @@ module.exports = (NativeChannelMergerNode) => { class ChannelMergerNode extends AudioNode { constructor(context, options) { + // keep a handle to the original object, if we need to manipulate the + // options before passing them to NAPI + const originalOptions = Object.assign({}, options); + if (options !== undefined) { if (typeof options !== 'object') { diff --git a/js/ChannelSplitterNode.js b/js/ChannelSplitterNode.js index 0de13f88..642ef4f4 100644 --- a/js/ChannelSplitterNode.js +++ b/js/ChannelSplitterNode.js @@ -33,6 +33,10 @@ module.exports = (NativeChannelSplitterNode) => { class ChannelSplitterNode extends AudioNode { constructor(context, options) { + // keep a handle to the original object, if we need to manipulate the + // options before passing them to NAPI + const originalOptions = Object.assign({}, options); + if (options !== undefined) { if (typeof options !== 'object') { diff --git a/js/ConstantSourceNode.js b/js/ConstantSourceNode.js index a82d9017..f814cacc 100644 --- a/js/ConstantSourceNode.js +++ b/js/ConstantSourceNode.js @@ -34,6 +34,10 @@ module.exports = (NativeConstantSourceNode) => { class ConstantSourceNode extends AudioScheduledSourceNode { constructor(context, options) { + // keep a handle to the original object, if we need to manipulate the + // options before passing them to NAPI + const originalOptions = Object.assign({}, options); + if (options !== undefined) { if (typeof options !== 'object') { diff --git a/js/ConvolverNode.js b/js/ConvolverNode.js index f6834002..fa283f74 100644 --- a/js/ConvolverNode.js +++ b/js/ConvolverNode.js @@ -33,6 +33,10 @@ module.exports = (NativeConvolverNode) => { class ConvolverNode extends AudioNode { constructor(context, options) { + // keep a handle to the original object, if we need to manipulate the + // options before passing them to NAPI + const originalOptions = Object.assign({}, options); + if (options !== undefined) { if (typeof options !== 'object') { @@ -57,8 +61,11 @@ module.exports = (NativeConvolverNode) => { super(context, options); + // keep the wrapper AudioBuffer wrapperaround + this[kAudioBuffer] = null; + if (options && 'buffer' in options) { - this[kAudioBuffer] = options.buffer; + this[kAudioBuffer] = originalOptions.buffer; } @@ -70,11 +77,7 @@ module.exports = (NativeConvolverNode) => { // getters get buffer() { - if (this[kAudioBuffer]) { - return this[kAudioBuffer]; - } else { - return null; - } + return this[kAudioBuffer]; } get normalize() { diff --git a/js/DelayNode.js b/js/DelayNode.js index fd27141f..17ba3659 100644 --- a/js/DelayNode.js +++ b/js/DelayNode.js @@ -33,6 +33,10 @@ module.exports = (NativeDelayNode) => { class DelayNode extends AudioNode { constructor(context, options) { + // keep a handle to the original object, if we need to manipulate the + // options before passing them to NAPI + const originalOptions = Object.assign({}, options); + if (options !== undefined) { if (typeof options !== 'object') { diff --git a/js/DynamicsCompressorNode.js b/js/DynamicsCompressorNode.js index 1bd4e0cf..0c66c2ef 100644 --- a/js/DynamicsCompressorNode.js +++ b/js/DynamicsCompressorNode.js @@ -33,6 +33,10 @@ module.exports = (NativeDynamicsCompressorNode) => { class DynamicsCompressorNode extends AudioNode { constructor(context, options) { + // keep a handle to the original object, if we need to manipulate the + // options before passing them to NAPI + const originalOptions = Object.assign({}, options); + if (options !== undefined) { if (typeof options !== 'object') { diff --git a/js/GainNode.js b/js/GainNode.js index da4e3773..daadfe69 100644 --- a/js/GainNode.js +++ b/js/GainNode.js @@ -33,6 +33,10 @@ module.exports = (NativeGainNode) => { class GainNode extends AudioNode { constructor(context, options) { + // keep a handle to the original object, if we need to manipulate the + // options before passing them to NAPI + const originalOptions = Object.assign({}, options); + if (options !== undefined) { if (typeof options !== 'object') { diff --git a/js/IIRFilterNode.js b/js/IIRFilterNode.js index 3a34922e..d4d83046 100644 --- a/js/IIRFilterNode.js +++ b/js/IIRFilterNode.js @@ -33,6 +33,10 @@ module.exports = (NativeIIRFilterNode) => { class IIRFilterNode extends AudioNode { constructor(context, options) { + // keep a handle to the original object, if we need to manipulate the + // options before passing them to NAPI + const originalOptions = Object.assign({}, options); + if (options !== undefined) { if (typeof options !== 'object') { diff --git a/js/MediaStreamAudioSourceNode.js b/js/MediaStreamAudioSourceNode.js index 5f94adb6..a04a704d 100644 --- a/js/MediaStreamAudioSourceNode.js +++ b/js/MediaStreamAudioSourceNode.js @@ -33,6 +33,10 @@ module.exports = (NativeMediaStreamAudioSourceNode) => { class MediaStreamAudioSourceNode extends AudioNode { constructor(context, options) { + // keep a handle to the original object, if we need to manipulate the + // options before passing them to NAPI + const originalOptions = Object.assign({}, options); + if (options !== undefined) { if (typeof options !== 'object') { diff --git a/js/OscillatorNode.js b/js/OscillatorNode.js index 36f585c6..70509a03 100644 --- a/js/OscillatorNode.js +++ b/js/OscillatorNode.js @@ -34,6 +34,10 @@ module.exports = (NativeOscillatorNode) => { class OscillatorNode extends AudioScheduledSourceNode { constructor(context, options) { + // keep a handle to the original object, if we need to manipulate the + // options before passing them to NAPI + const originalOptions = Object.assign({}, options); + if (options !== undefined) { if (typeof options !== 'object') { diff --git a/js/PannerNode.js b/js/PannerNode.js index accad7cb..17ed2132 100644 --- a/js/PannerNode.js +++ b/js/PannerNode.js @@ -33,6 +33,10 @@ module.exports = (NativePannerNode) => { class PannerNode extends AudioNode { constructor(context, options) { + // keep a handle to the original object, if we need to manipulate the + // options before passing them to NAPI + const originalOptions = Object.assign({}, options); + if (options !== undefined) { if (typeof options !== 'object') { diff --git a/js/StereoPannerNode.js b/js/StereoPannerNode.js index 4f27707a..7c867682 100644 --- a/js/StereoPannerNode.js +++ b/js/StereoPannerNode.js @@ -33,6 +33,10 @@ module.exports = (NativeStereoPannerNode) => { class StereoPannerNode extends AudioNode { constructor(context, options) { + // keep a handle to the original object, if we need to manipulate the + // options before passing them to NAPI + const originalOptions = Object.assign({}, options); + if (options !== undefined) { if (typeof options !== 'object') { diff --git a/js/WaveShaperNode.js b/js/WaveShaperNode.js index 303970f2..753d6179 100644 --- a/js/WaveShaperNode.js +++ b/js/WaveShaperNode.js @@ -33,6 +33,10 @@ module.exports = (NativeWaveShaperNode) => { class WaveShaperNode extends AudioNode { constructor(context, options) { + // keep a handle to the original object, if we need to manipulate the + // options before passing them to NAPI + const originalOptions = Object.assign({}, options); + if (options !== undefined) { if (typeof options !== 'object') { diff --git a/tests/AudioBuffer.spec.mjs b/tests/AudioBuffer.spec.mjs index aa6f6b9d..c61dcf1a 100644 --- a/tests/AudioBuffer.spec.mjs +++ b/tests/AudioBuffer.spec.mjs @@ -9,6 +9,9 @@ import { OfflineAudioContext } from '../index.mjs'; +// access private property +import kNativeAudioBuffer from '../' + describe('# AudioBuffer', () => { describe(`## audioContext.createBuffer(numChannels, length, sampleRate)`, () => { diff --git a/tests/junk.mjs b/tests/junk.mjs index 349d391e..67066498 100644 --- a/tests/junk.mjs +++ b/tests/junk.mjs @@ -23,6 +23,15 @@ import { AudioBuffer, AudioBufferSourceNode, AnalyserNode, AudioContext, Convolv const SAMPLERATE = 8000; const LENGTH = 128; -const context = new OfflineAudioContext(1, LENGTH, SAMPLERATE); +const context = new AudioContext({ sampleRate: 1 }); -const node3 = new ConvolverNode(context, {"buffer":null,"disableNormalization":false}) +// const _ = context.createBuffer(1, 1, context.sampleRate); +// console.log(_); +const buffer = new AudioBuffer({ length: 12, sampleRate: context.sampleRate }); + +let node; +try { + node = new ConvolverNode(context, { buffer: null }); +} catch (err) {} + +console.log(node.buffer); From 23a3b6e0287eb1537ab4c2d5c47f1a2d112e1c2c Mon Sep 17 00:00:00 2001 From: b-ma Date: Sun, 17 Mar 2024 15:10:08 +0100 Subject: [PATCH 12/12] fix: properly handle setting buffer to null in AudioNode ctors --- generator/rs/audio_nodes.tmpl.rs | 14 +++++++++++--- src/audio_buffer_source_node.rs | 14 +++++++++++--- src/convolver_node.rs | 14 +++++++++++--- src/oscillator_node.rs | 16 ++++++++++++---- 4 files changed, 45 insertions(+), 13 deletions(-) diff --git a/generator/rs/audio_nodes.tmpl.rs b/generator/rs/audio_nodes.tmpl.rs index 3abcfdf4..74a98639 100644 --- a/generator/rs/audio_nodes.tmpl.rs +++ b/generator/rs/audio_nodes.tmpl.rs @@ -250,10 +250,18 @@ fn constructor(ctx: CallContext) -> Result { // AudioBuffer, PeriodicWave case 'interface': return ` - let some_${simple_slug}_js = options_js.get::<&str, JsObject>("${m.name}")?; + let some_${simple_slug}_js = options_js.get::<&str, JsUnknown>("${m.name}")?; let ${slug} = if let Some(${simple_slug}_js) = some_${simple_slug}_js { - let ${simple_slug}_napi = ctx.env.unwrap::<${d.napiName(idl)}>(&${simple_slug}_js)?; - Some(${simple_slug}_napi.unwrap().clone()) + // nullable options + match ${simple_slug}_js.get_type()? { + ValueType::Object => { + let ${simple_slug}_js = ${simple_slug}_js.coerce_to_object()?; + let ${simple_slug}_napi = ctx.env.unwrap::<${d.napiName(idl)}>(&${simple_slug}_js)?; + Some(${simple_slug}_napi.unwrap().clone()) + }, + ValueType::Null => None, + _ => unreachable!(), + } } else { None }; diff --git a/src/audio_buffer_source_node.rs b/src/audio_buffer_source_node.rs index 3f02a48f..c94ab92e 100644 --- a/src/audio_buffer_source_node.rs +++ b/src/audio_buffer_source_node.rs @@ -146,10 +146,18 @@ fn constructor(ctx: CallContext) -> Result { let options = if let Ok(either_options) = ctx.try_get::(1) { match either_options { Either::A(options_js) => { - let some_buffer_js = options_js.get::<&str, JsObject>("buffer")?; + let some_buffer_js = options_js.get::<&str, JsUnknown>("buffer")?; let buffer = if let Some(buffer_js) = some_buffer_js { - let buffer_napi = ctx.env.unwrap::(&buffer_js)?; - Some(buffer_napi.unwrap().clone()) + // nullable options + match buffer_js.get_type()? { + ValueType::Object => { + let buffer_js = buffer_js.coerce_to_object()?; + let buffer_napi = ctx.env.unwrap::(&buffer_js)?; + Some(buffer_napi.unwrap().clone()) + } + ValueType::Null => None, + _ => unreachable!(), + } } else { None }; diff --git a/src/convolver_node.rs b/src/convolver_node.rs index 25a7333d..a7e60b7c 100644 --- a/src/convolver_node.rs +++ b/src/convolver_node.rs @@ -130,10 +130,18 @@ fn constructor(ctx: CallContext) -> Result { let options = if let Ok(either_options) = ctx.try_get::(1) { match either_options { Either::A(options_js) => { - let some_buffer_js = options_js.get::<&str, JsObject>("buffer")?; + let some_buffer_js = options_js.get::<&str, JsUnknown>("buffer")?; let buffer = if let Some(buffer_js) = some_buffer_js { - let buffer_napi = ctx.env.unwrap::(&buffer_js)?; - Some(buffer_napi.unwrap().clone()) + // nullable options + match buffer_js.get_type()? { + ValueType::Object => { + let buffer_js = buffer_js.coerce_to_object()?; + let buffer_napi = ctx.env.unwrap::(&buffer_js)?; + Some(buffer_napi.unwrap().clone()) + } + ValueType::Null => None, + _ => unreachable!(), + } } else { None }; diff --git a/src/oscillator_node.rs b/src/oscillator_node.rs index 1b1be949..0e5d2f3a 100644 --- a/src/oscillator_node.rs +++ b/src/oscillator_node.rs @@ -166,11 +166,19 @@ fn constructor(ctx: CallContext) -> Result { 0. }; - let some_periodic_wave_js = options_js.get::<&str, JsObject>("periodicWave")?; + let some_periodic_wave_js = options_js.get::<&str, JsUnknown>("periodicWave")?; let periodic_wave = if let Some(periodic_wave_js) = some_periodic_wave_js { - let periodic_wave_napi = - ctx.env.unwrap::(&periodic_wave_js)?; - Some(periodic_wave_napi.unwrap().clone()) + // nullable options + match periodic_wave_js.get_type()? { + ValueType::Object => { + let periodic_wave_js = periodic_wave_js.coerce_to_object()?; + let periodic_wave_napi = + ctx.env.unwrap::(&periodic_wave_js)?; + Some(periodic_wave_napi.unwrap().clone()) + } + ValueType::Null => None, + _ => unreachable!(), + } } else { None };