Skip to content

Feat: wrap AudioBuffer with JS proxy #90

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Mar 17, 2024
4 changes: 4 additions & 0 deletions generator/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
Expand Down
195 changes: 156 additions & 39 deletions generator/js/AudioNodes.tmpl.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,74 +7,191 @@ const AudioNodeMixin = require('./AudioNode.mixin.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")}'")
// 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];
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) {
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];
}
}
`;
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);
if (type === 'AudioBuffer') {
return `
// keep the wrapper AudioBuffer wrapperaround
this[kAudioBuffer] = null;

if (options && '${optionName}' in options) {
this[kAudioBuffer] = originalOptions.${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 => {
return `
switch (d.memberType(attr)) {
case 'AudioBuffer': {
return `
get ${d.name(attr)}() {
return this[kAudioBuffer];
}
`;
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 `
// @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) {
throwSanitizedError(err);
}

this[kAudioBuffer] = value;
}
`;
break;
}
default: {
return `
set ${d.name(attr)}(value) {
try {
super.${d.name(attr)} = value;
} catch (err) {
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);
Expand Down
9 changes: 8 additions & 1 deletion generator/js/BaseAudioContext.mixin.tmpl.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
const { AudioDestinationNode } = require('./AudioDestinationNode.js');
const { isFunction } = require('./lib/utils.js');
const { kNativeAudioBuffer } = require('./AudioBuffer.js');

module.exports = (superclass, bindings) => {
const {
${d.nodes.map(n => ` ${d.name(n)},`).join('\n')}
AudioBuffer,
PeriodicWave,
} = bindings;

Expand All @@ -24,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);
Expand All @@ -40,6 +43,10 @@ ${d.nodes.map(n => ` ${d.name(n)},`).join('\n')}
}
}

createBuffer(numberOfChannels, length, sampleRate) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to have this constructor option supported

return new AudioBuffer({ numberOfChannels, length, sampleRate });
}

createPeriodicWave(real, imag) {
return new PeriodicWave(this, { real, imag });
}
Expand Down
4 changes: 2 additions & 2 deletions generator/js/monkey-patch.tmpl.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +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').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;

Expand Down
23 changes: 13 additions & 10 deletions generator/rs/audio_nodes.tmpl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,16 +133,10 @@ fn constructor(ctx: CallContext) -> Result<JsUndefined> {
// ----------------------------------------------
// 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);
Expand Down Expand Up @@ -256,10 +250,18 @@ fn constructor(ctx: CallContext) -> Result<JsUndefined> {
// 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
};
Expand Down Expand Up @@ -913,7 +915,8 @@ fn set_${d.slug(attr)}(ctx: CallContext) -> Result<JsUndefined> {
}
`;
break
case 'interface':
case 'interface': // AudioBuffer
console.log(attr);
return `
#[js_function(1)]
fn set_${d.slug(attr)}(ctx: CallContext) -> Result<JsUndefined> {
Expand Down
Loading
Loading