Skip to content

Commit defbfa1

Browse files
authored
Merge pull request #90 from ircam-ismm/feat/audiobuffer-proxy
Feat: wrap AudioBuffer with JS proxy
2 parents ff70f84 + 23a3b6e commit defbfa1

39 files changed

+1166
-443
lines changed

generator/index.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,10 @@ const utils = {
181181

182182
return camelcase(idl.name, { pascalCase: true, preserveConsecutiveUppercase: true });
183183
},
184+
185+
debug(value) {
186+
console.log(JSON.stringify(value, null, 2));
187+
}
184188
};
185189

186190
let audioNodes = [];

generator/js/AudioNodes.tmpl.js

Lines changed: 156 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,74 +7,191 @@ const AudioNodeMixin = require('./AudioNode.mixin.js');
77
${d.parent(d.node) === 'AudioScheduledSourceNode' ?
88
`const AudioScheduledSourceNodeMixin = require('./AudioScheduledSourceNode.mixin.js');`: ``}
99

10+
const { kNativeAudioBuffer, kAudioBuffer } = require('./AudioBuffer.js');
11+
1012
module.exports = (Native${d.name(d.node)}) => {
11-
${d.parent(d.node) === 'AudioScheduledSourceNode' ? `
1213
const EventTarget = EventTargetMixin(Native${d.name(d.node)}, ['ended']);
1314
const AudioNode = AudioNodeMixin(EventTarget);
15+
${d.parent(d.node) === 'AudioScheduledSourceNode' ? `\
1416
const AudioScheduledSourceNode = AudioScheduledSourceNodeMixin(AudioNode);
1517
16-
class ${d.name(d.node)} extends AudioScheduledSourceNode {
18+
class ${d.name(d.node)} extends AudioScheduledSourceNode {` : `
19+
class ${d.name(d.node)} extends AudioNode {`
20+
}
1721
constructor(context, options) {
18-
if (options !== undefined && typeof options !== 'object') {
19-
throw new TypeError("Failed to construct '${d.name(d.node)}': argument 2 is not of type '${d.name(d.node).replace("Node", "Options")}'")
22+
// keep a handle to the original object, if we need to manipulate the
23+
// options before passing them to NAPI
24+
const originalOptions = Object.assign({}, options);
25+
26+
${(function() {
27+
// handle argument 2: options
28+
const options = d.constructor(d.node).arguments[1];
29+
const optionsType = d.memberType(options);
30+
const optionsIdl = d.findInTree(optionsType);
31+
let checkOptions = `
32+
if (options !== undefined) {
33+
if (typeof options !== 'object') {
34+
throw new TypeError("Failed to construct '${d.name(d.node)}': argument 2 is not of type '${optionsType}'")
35+
}
36+
`;
37+
38+
checkOptions += optionsIdl.members.map(member => {
39+
// @todo - improve checks
40+
// cf. https://github.com/jsdom/webidl-conversions
41+
const optionName = d.name(member);
42+
const type = d.memberType(member);
43+
const required = member.required;
44+
const nullable = member.idlType.nullable;
45+
const defaultValue = member.default; // null or object
46+
let checkMember = '';
47+
48+
if (required) {
49+
checkMember += `
50+
if (options && !('${optionName}' in options)) {
51+
throw new Error("Failed to read the '${optionName}'' property from ${optionsType}: Required member is undefined.")
52+
}
53+
`
54+
}
55+
56+
// d.debug(member);
57+
switch (type) {
58+
case 'AudioBuffer': {
59+
checkMember += `
60+
if ('${optionName}' in options) {
61+
if (options.${optionName} !== null) {
62+
if (!(kNativeAudioBuffer in options.${optionName})) {
63+
throw new TypeError("Failed to set the 'buffer' property on 'AudioBufferSourceNode': Failed to convert value to 'AudioBuffer'");
64+
}
65+
66+
// unwrap napi audio buffer, clone the options object as it might be reused
67+
options = Object.assign({}, options);
68+
options.${optionName} = options.${optionName}[kNativeAudioBuffer];
69+
}
70+
}
71+
`;
72+
break;
73+
}
74+
}
75+
76+
return checkMember;
77+
}).join('');
78+
79+
checkOptions += `
2080
}
81+
`;
82+
83+
return checkOptions;
84+
}())}
2185

2286
super(context, options);
23-
// EventTargetMixin has been called so EventTargetMixin[kDispatchEvent] is
24-
// bound to this, then we can safely finalize event target initialization
25-
super.__initEventTarget__();
26-
${d.audioParams(d.node).map(param => {
27-
return `
28-
this.${d.name(param)} = new AudioParam(this.${d.name(param)});`;
29-
}).join('')}
30-
}
31-
`: `
32-
const EventTarget = EventTargetMixin(Native${d.name(d.node)});
33-
const AudioNode = AudioNodeMixin(EventTarget);
3487

35-
class ${d.name(d.node)} extends AudioNode {
36-
constructor(context, options) {
37-
if (options !== undefined && typeof options !== 'object') {
38-
throw new TypeError("Failed to construct '${d.name(d.node)}': argument 2 is not of type '${d.name(d.node).replace("Node", "Options")}'")
88+
${(function() {
89+
// handle special options cases
90+
const options = d.constructor(d.node).arguments[1];
91+
const optionsType = d.memberType(options);
92+
const optionsIdl = d.findInTree(optionsType);
93+
94+
return optionsIdl.members.map(member => {
95+
// at this point all type checks have been done, so it is safe to just manipulate the options
96+
const optionName = d.name(member);
97+
const type = d.memberType(member);
98+
if (type === 'AudioBuffer') {
99+
return `
100+
// keep the wrapper AudioBuffer wrapperaround
101+
this[kAudioBuffer] = null;
102+
103+
if (options && '${optionName}' in options) {
104+
this[kAudioBuffer] = originalOptions.${optionName};
39105
}
106+
`;
107+
}
108+
}).join('');
109+
}())}
40110

41-
super(context, options);
42-
${d.audioParams(d.node).map(param => {
43-
return `
111+
${d.parent(d.node) === 'AudioScheduledSourceNode' ? `
112+
// EventTargetMixin constructor has been called so EventTargetMixin[kDispatchEvent]
113+
// is bound to this, then we can safely finalize event target initialization
114+
super.__initEventTarget__();` : ``}
115+
116+
${d.audioParams(d.node).map(param => {
117+
return `
44118
this.${d.name(param)} = new AudioParam(this.${d.name(param)});`;
45-
}).join('')}
119+
}).join('')}
46120
}
47-
`}
121+
48122
// getters
49123
${d.attributes(d.node).map(attr => {
50-
return `
124+
switch (d.memberType(attr)) {
125+
case 'AudioBuffer': {
126+
return `
127+
get ${d.name(attr)}() {
128+
return this[kAudioBuffer];
129+
}
130+
`;
131+
break;
132+
}
133+
default: {
134+
return `
51135
get ${d.name(attr)}() {
52136
return super.${d.name(attr)};
53137
}
54-
`}).join('')}
138+
`;
139+
break;
140+
}
141+
}
142+
}).join('')}
55143
// setters
56144
${d.attributes(d.node).filter(attr => !attr.readonly).map(attr => {
57-
return `
145+
switch (d.memberType(attr)) {
146+
case 'AudioBuffer': {
147+
return `
148+
// @todo - should be able to set to null afterward
149+
set ${d.name(attr)}(value) {
150+
if (value === null) {
151+
return;
152+
} else if (!(kNativeAudioBuffer in value)) {
153+
throw new TypeError("Failed to set the 'buffer' property on 'AudioBufferSourceNode': Failed to convert value to 'AudioBuffer'");
154+
}
155+
156+
try {
157+
super.${d.name(attr)} = value[kNativeAudioBuffer];
158+
} catch (err) {
159+
throwSanitizedError(err);
160+
}
161+
162+
this[kAudioBuffer] = value;
163+
}
164+
`;
165+
break;
166+
}
167+
default: {
168+
return `
58169
set ${d.name(attr)}(value) {
59170
try {
60171
super.${d.name(attr)} = value;
61172
} catch (err) {
62173
throwSanitizedError(err);
63174
}
64175
}
65-
`}).join('')}
176+
`;
177+
break;
178+
}
179+
}
180+
}).join('')}
181+
66182
// methods
67-
${d.methods(d.node, false).reduce((acc, method) => {
68-
// dedup method names
69-
if (!acc.find(i => d.name(i) === d.name(method))) {
70-
acc.push(method)
71-
}
72-
return acc;
73-
}, [])
74-
// filter AudioScheduledSourceNode methods to prevent re-throwing errors
75-
.filter(method => d.name(method) !== 'start' && d.name(method) !== 'stop')
76-
.map(method => {
77-
return `
183+
${d.methods(d.node, false)
184+
.reduce((acc, method) => {
185+
// dedup method names
186+
if (!acc.find(i => d.name(i) === d.name(method))) {
187+
acc.push(method)
188+
}
189+
return acc;
190+
}, [])
191+
// filter AudioScheduledSourceNode methods to prevent re-throwing errors
192+
.filter(method => d.name(method) !== 'start' && d.name(method) !== 'stop')
193+
.map(method => {
194+
return `
78195
${d.name(method)}(...args) {
79196
try {
80197
return super.${d.name(method)}(...args);

generator/js/BaseAudioContext.mixin.tmpl.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
const { AudioDestinationNode } = require('./AudioDestinationNode.js');
22
const { isFunction } = require('./lib/utils.js');
3+
const { kNativeAudioBuffer } = require('./AudioBuffer.js');
34

45
module.exports = (superclass, bindings) => {
56
const {
67
${d.nodes.map(n => ` ${d.name(n)},`).join('\n')}
8+
AudioBuffer,
79
PeriodicWave,
810
} = bindings;
911

@@ -24,7 +26,8 @@ ${d.nodes.map(n => ` ${d.name(n)},`).join('\n')}
2426
}
2527

2628
try {
27-
const audioBuffer = super.decodeAudioData(audioData);
29+
const nativeAudioBuffer = super.decodeAudioData(audioData);
30+
const audioBuffer = new AudioBuffer({ [kNativeAudioBuffer]: nativeAudioBuffer });
2831

2932
if (isFunction(decodeSuccessCallback)) {
3033
decodeSuccessCallback(audioBuffer);
@@ -40,6 +43,10 @@ ${d.nodes.map(n => ` ${d.name(n)},`).join('\n')}
4043
}
4144
}
4245

46+
createBuffer(numberOfChannels, length, sampleRate) {
47+
return new AudioBuffer({ numberOfChannels, length, sampleRate });
48+
}
49+
4350
createPeriodicWave(real, imag) {
4451
return new PeriodicWave(this, { real, imag });
4552
}

generator/js/monkey-patch.tmpl.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ ${d.nodes.map((node) => {
77
nativeBinding.${d.name(node)} = require('./${d.name(node)}.js')(nativeBinding.${d.name(node)});`
88
}).join('')}
99

10-
// @todo - wrap AudioBuffer interface as well
1110
nativeBinding.PeriodicWave = require('./PeriodicWave.js')(nativeBinding.PeriodicWave);
11+
nativeBinding.AudioBuffer = require('./AudioBuffer.js').AudioBuffer(nativeBinding.AudioBuffer);
1212

1313
nativeBinding.AudioContext = require('./AudioContext.js')(nativeBinding);
1414
nativeBinding.OfflineAudioContext = require('./OfflineAudioContext.js')(nativeBinding);
1515

16-
// find a way to make the constructor private
16+
// @todo - make the constructor private
1717
nativeBinding.AudioParam = require('./AudioParam.js').AudioParam;
1818
nativeBinding.AudioDestinationNode = require('./AudioDestinationNode.js').AudioDestinationNode;
1919

generator/rs/audio_nodes.tmpl.rs

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -133,16 +133,10 @@ fn constructor(ctx: CallContext) -> Result<JsUndefined> {
133133
// ----------------------------------------------
134134
// parse options
135135
// ----------------------------------------------
136-
137136
if (index == 0) { // index 0 is always AudioContext
138137
return;
139138
}
140139

141-
if (d.constructor(d.node).arguments.length != 2) {
142-
console.log(d.node.name, 'constructor has arguments.length != 2');
143-
return ``;
144-
}
145-
146140
const arg = d.constructor(d.node).arguments[1];
147141
const argIdlType = d.memberType(arg);
148142
const argumentIdl = d.findInTree(argIdlType);
@@ -256,10 +250,18 @@ fn constructor(ctx: CallContext) -> Result<JsUndefined> {
256250
// AudioBuffer, PeriodicWave
257251
case 'interface':
258252
return `
259-
let some_${simple_slug}_js = options_js.get::<&str, JsObject>("${m.name}")?;
253+
let some_${simple_slug}_js = options_js.get::<&str, JsUnknown>("${m.name}")?;
260254
let ${slug} = if let Some(${simple_slug}_js) = some_${simple_slug}_js {
261-
let ${simple_slug}_napi = ctx.env.unwrap::<${d.napiName(idl)}>(&${simple_slug}_js)?;
262-
Some(${simple_slug}_napi.unwrap().clone())
255+
// nullable options
256+
match ${simple_slug}_js.get_type()? {
257+
ValueType::Object => {
258+
let ${simple_slug}_js = ${simple_slug}_js.coerce_to_object()?;
259+
let ${simple_slug}_napi = ctx.env.unwrap::<${d.napiName(idl)}>(&${simple_slug}_js)?;
260+
Some(${simple_slug}_napi.unwrap().clone())
261+
},
262+
ValueType::Null => None,
263+
_ => unreachable!(),
264+
}
263265
} else {
264266
None
265267
};
@@ -913,7 +915,8 @@ fn set_${d.slug(attr)}(ctx: CallContext) -> Result<JsUndefined> {
913915
}
914916
`;
915917
break
916-
case 'interface':
918+
case 'interface': // AudioBuffer
919+
console.log(attr);
917920
return `
918921
#[js_function(1)]
919922
fn set_${d.slug(attr)}(ctx: CallContext) -> Result<JsUndefined> {

0 commit comments

Comments
 (0)