Skip to content

Commit

Permalink
Merge pull request #123 from ircam-ismm/feat/render-capacity
Browse files Browse the repository at this point in the history
Feat - AudioRenderCapacity
  • Loading branch information
b-ma authored May 8, 2024
2 parents b8a6b55 + f70162b commit 63aafce
Show file tree
Hide file tree
Showing 15 changed files with 355 additions and 20 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ crate-type = ["cdylib"]
[dependencies]
napi = {version="2.15", features=["napi9", "tokio_rt"]}
napi-derive = "2.15"
web-audio-api = "=0.44.0"
web-audio-api = "=0.45.0"
# web-audio-api = { path = "../web-audio-api-rs" }

[target.'cfg(all(any(windows, unix), target_arch = "x86_64", not(target_env = "musl")))'.dependencies]
Expand Down
10 changes: 5 additions & 5 deletions examples/convolution.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import { AudioContext, ConvolverNode } from '../index.mjs';
const latencyHint = process.env.WEB_AUDIO_LATENCY === 'playback' ? 'playback' : 'interactive';
const audioContext = new AudioContext({ latencyHint });

// let cap = audioContext.render_capacity();
// cap.onupdate(|e| println!("{:?}", e));
// cap.start(AudioRenderCapacityOptions {
// update_interval: 1.,
// });
audioContext.renderCapacity.addEventListener('update', e => {
const { timestamp, averageLoad, peakLoad, underrunRatio } = e;
console.log('AudioRenderCapacityEvent:', { timestamp, averageLoad, peakLoad, underrunRatio });
});
audioContext.renderCapacity.start({ updateInterval: 1.5 });

const arrayBuffer = fs.readFileSync(path.join('examples', 'samples', 'vocals-dry.wav')).buffer;
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
Expand Down
2 changes: 2 additions & 0 deletions generator/js/index.tmpl.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const {
// events
OfflineAudioCompletionEvent,
AudioProcessingEvent,
AudioRenderCapacityEvent,

// manually written nodes
BaseAudioContext,
Expand All @@ -20,6 +21,7 @@ export const {
AudioParam,
AudioDestinationNode,
AudioListener,
AudioRenderCapacity,

PeriodicWave,
AudioBuffer,
Expand Down
2 changes: 2 additions & 0 deletions generator/js/monkey-patch.tmpl.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module.exports = function monkeyPatch(nativeBinding) {
// --------------------------------------------------------------------------
jsExport.OfflineAudioCompletionEvent = require('./Events').OfflineAudioCompletionEvent;
jsExport.AudioProcessingEvent = require('./Events').AudioProcessingEvent;
jsExport.AudioRenderCapacityEvent = require('./Events').AudioRenderCapacityEvent;
// --------------------------------------------------------------------------
// Create Web Audio API facade
// --------------------------------------------------------------------------
Expand All @@ -23,6 +24,7 @@ ${d.nodes.map((node) => {
jsExport.AudioParam = require('./AudioParam.js');
jsExport.AudioDestinationNode = require('./AudioDestinationNode.js');
jsExport.AudioListener = require('./AudioListener.js');
jsExport.AudioRenderCapacity = require('./AudioRenderCapacity.js');

jsExport.PeriodicWave = require('./PeriodicWave.js')(jsExport, nativeBinding);
jsExport.AudioBuffer = require('./AudioBuffer.js')(jsExport, nativeBinding);
Expand Down
5 changes: 5 additions & 0 deletions generator/rs/lib.tmpl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ mod audio_param;
use crate::audio_param::NapiAudioParam;
mod audio_listener;
use crate::audio_listener::NapiAudioListener;
mod audio_render_capacity;
use crate::audio_render_capacity::NapiAudioRenderCapacity;
mod audio_buffer;
use crate::audio_buffer::NapiAudioBuffer;
mod periodic_wave;
Expand Down Expand Up @@ -98,6 +100,9 @@ fn init(mut exports: JsObject, env: Env) -> Result<()> {
let napi_class = NapiAudioListener::create_js_class(&env)?;
store.set_named_property("AudioListener", napi_class)?;

let napi_class = NapiAudioRenderCapacity::create_js_class(&env)?;
store.set_named_property("AudioRenderCapacity", napi_class)?;

let napi_class = NapiAudioBuffer::create_js_class(&env)?;
store.set_named_property("AudioBuffer", napi_class)?;

Expand Down
2 changes: 2 additions & 0 deletions index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const {
// events
OfflineAudioCompletionEvent,
AudioProcessingEvent,
AudioRenderCapacityEvent,

// manually written nodes
BaseAudioContext,
Expand All @@ -41,6 +42,7 @@ export const {
AudioParam,
AudioDestinationNode,
AudioListener,
AudioRenderCapacity,

PeriodicWave,
AudioBuffer,
Expand Down
20 changes: 12 additions & 8 deletions js/AudioContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ module.exports = function(jsExport, nativeBinding) {

class AudioContext extends jsExport.BaseAudioContext {
#sinkId = '';
#renderCapacity = null;
#onsinkchange = null;

constructor(options = {}) {
if (typeof options !== 'object') {
Expand Down Expand Up @@ -51,18 +53,16 @@ module.exports = function(jsExport, nativeBinding) {
}

if (options.sinkId !== undefined) {
const sinkId = options.sinkId;

if (typeof options.sinkId === 'object') {
// https://webaudio.github.io/web-audio-api/#enumdef-audiosinktype
if (!('type' in options.sinkId) || options.sinkId.type !== 'none') {
throw TypeError(`Failed to construct 'AudioContext': Failed to read the 'sinkId' property from AudioNodeOptions: Failed to read the 'type' property from 'AudioSinkOptions': The provided value (${sinkId.type}) is not a valid enum value of type AudioSinkType.`);
throw TypeError(`Failed to construct 'AudioContext': Failed to read the 'sinkId' property from AudioNodeOptions: Failed to read the 'type' property from 'AudioSinkOptions': The provided value (${options.sinkId.type}) is not a valid enum value of type AudioSinkType.`);
}

targetOptions.sinkId = 'none';
} else {
targetOptions.sinkId = conversions['DOMString'](sinkId, {
context: `Failed to construct 'AudioContext': Failed to read the 'sinkId' property from AudioNodeOptions: Failed to read the 'type' property from 'AudioSinkOptions': The provided value (${sinkId})`,
targetOptions.sinkId = conversions['DOMString'](options.sinkId, {
context: `Failed to construct 'AudioContext': Failed to read the 'sinkId' property from AudioNodeOptions: Failed to read the 'type' property from 'AudioSinkOptions': The provided value (${options.sinkId})`,
});
}
} else {
Expand All @@ -83,6 +83,10 @@ module.exports = function(jsExport, nativeBinding) {
this.#sinkId = options.sinkId;
}

this.#renderCapacity = new jsExport.AudioRenderCapacity({
[kNapiObj]: this[kNapiObj].renderCapacity,
});

// Add function to Napi object to bridge from Rust events to JS EventTarget
this[kNapiObj][kOnStateChange] = (err, rawEvent) => {
if (typeof rawEvent !== 'object' && !('type' in rawEvent)) {
Expand Down Expand Up @@ -160,15 +164,15 @@ module.exports = function(jsExport, nativeBinding) {
throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'AudioContext\'');
}

throw new Error(`AudioContext::renderCapacity is not yet implemented`);
return this.#renderCapacity;
}

get onsinkchange() {
if (!(this instanceof AudioContext)) {
throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'AudioContext\'');
}

return this._sinkchange || null;
return this.#onsinkchange;
}

set onsinkchange(value) {
Expand All @@ -177,7 +181,7 @@ module.exports = function(jsExport, nativeBinding) {
}

if (isFunction(value) || value === null) {
this._sinkchange = value;
this.#onsinkchange = value;
}
}

Expand Down
117 changes: 117 additions & 0 deletions js/AudioRenderCapacity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
const conversions = require('webidl-conversions');

const {
kNapiObj,
kOnUpdate,
} = require('./lib/symbols.js');
const {
kEnumerableProperty,
} = require('./lib/utils.js');
const {
propagateEvent,
} = require('./lib/events.js');
const {
AudioRenderCapacityEvent,
} = require('./Events.js');

class AudioRenderCapacity extends EventTarget {
#onupdate = null;

constructor(options) {
// Make constructor "private"
if (
(typeof options !== 'object')
|| !(kNapiObj in options)
|| options[kNapiObj]['Symbol.toStringTag'] !== 'AudioRenderCapacity'
) {
throw new TypeError('Illegal constructor');
}

super();

this[kNapiObj] = options[kNapiObj];

this[kNapiObj][kOnUpdate] = (err, rawEvent) => {
const event = new AudioRenderCapacityEvent('update', rawEvent);
propagateEvent(this, event);
};

this[kNapiObj].listen_to_events();
}

get onupdate() {
if (!(this instanceof AudioRenderCapacity)) {
throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'AudioRenderCapacity\'');
}

return this.#onupdate;
}

set onupdate(value) {
if (!(this instanceof AudioRenderCapacity)) {
throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'AudioRenderCapacity\'');
}

if (isFunction(value) || value === null) {
this.#onupdate = value;
}
}

start(options = null) {
if (!(this instanceof AudioRenderCapacity)) {
throw new TypeError(`Invalid Invocation: Value of 'this' must be of type 'AudioRenderCapacity'`);
}

let targetOptions = {};

if (typeof options === 'object' && options !== null) {
if (!('updateInterval' in options)) {
throw new TypeError(`Failed to execute 'start' on 'AudioRenderCapacity': Failed to read the 'updateInterval' property on 'AudioRenderCapacityOptions'`);
}

targetOptions.updateInterval = conversions['double'](options.updateInterval, {
context: `Failed to execute 'start' on 'AudioRenderCapacity': Failed to read the 'updateInterval' property on 'AudioRenderCapacityOptions': The provided value ()`
});
} else {
targetOptions.updateInterval = 1;
}

return this[kNapiObj].start(targetOptions);
}

stop() {
if (!(this instanceof AudioRenderCapacity)) {
throw new TypeError(`Invalid Invocation: Value of 'this' must be of type 'AudioRenderCapacity'`);
}

return this[kNapiObj].start();
}
}

Object.defineProperties(AudioRenderCapacity, {
length: {
__proto__: null,
writable: false,
enumerable: false,
configurable: true,
value: 0,
},
});

Object.defineProperties(AudioRenderCapacity.prototype, {
[Symbol.toStringTag]: {
__proto__: null,
writable: false,
enumerable: false,
configurable: true,
value: 'AudioRenderCapacity',
},

onupdate: kEnumerableProperty,
stop: kEnumerableProperty,
stop: kEnumerableProperty,
});

module.exports = AudioRenderCapacity;


69 changes: 66 additions & 3 deletions js/Events.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ class OfflineAudioCompletionEvent extends Event {
constructor(type, eventInitDict) {
super(type);

if (typeof eventInitDict !== 'object' || eventInitDict === null || !('renderedBuffer' in eventInitDict)) {
throw TypeError(`Failed to construct 'OfflineAudioCompletionEvent': Failed to read the 'renderedBuffer' property from 'OfflineAudioCompletionEvent': Required member is undefined.`);
if (
typeof eventInitDict !== 'object'
|| eventInitDict === null
|| !('renderedBuffer' in eventInitDict)
) {
throw TypeError(`Failed to construct 'OfflineAudioCompletionEvent': Invalid 'OfflineAudioCompletionEventInit' dict given`);
}

this.#renderedBuffer = eventInitDict.renderedBuffer;
Expand Down Expand Up @@ -43,7 +47,7 @@ class AudioProcessingEvent extends Event {
|| !('inputBuffer' in eventInitDict)
|| !('outputBuffer' in eventInitDict)
) {
throw TypeError(`Failed to construct 'AudioProcessingEvent': Invalid 'AudioProcessingEventInit' given`);
throw TypeError(`Failed to construct 'AudioProcessingEvent': Invalid 'AudioProcessingEventInit' dict given`);
}

super(type);
Expand Down Expand Up @@ -80,5 +84,64 @@ Object.defineProperties(AudioProcessingEvent.prototype, {
outputBuffer: kEnumerableProperty,
});

class AudioRenderCapacityEvent extends Event {
#timestamp = 0;
#averageLoad = 0;
#peakLoad = 0;
#underrunRatio = 0;

constructor(type, eventInitDict) {
if (
typeof eventInitDict !== 'object'
|| eventInitDict === null
|| !('timestamp' in eventInitDict)
|| !('averageLoad' in eventInitDict)
|| !('peakLoad' in eventInitDict)
|| !('underrunRatio' in eventInitDict)
) {
throw TypeError(`Failed to construct 'AudioRenderCapacityEvent': Invalid 'AudioRenderCapacityEventInit' dict given`);
}

super(type);

this.#timestamp = eventInitDict.timestamp;
this.#averageLoad = eventInitDict.averageLoad;
this.#peakLoad = eventInitDict.peakLoad;
this.#underrunRatio = eventInitDict.underrunRatio;
}

get timestamp() {
return this.#timestamp;
}

get averageLoad() {
return this.#averageLoad;
}

get peakLoad() {
return this.#peakLoad;
}

get underrunRatio() {
return this.#underrunRatio;
}
}

Object.defineProperties(AudioRenderCapacityEvent.prototype, {
[Symbol.toStringTag]: {
__proto__: null,
writable: false,
enumerable: false,
configurable: true,
value: 'AudioRenderCapacityEvent',
},

timestamp: kEnumerableProperty,
averageLoad: kEnumerableProperty,
peakLoad: kEnumerableProperty,
underrunRatio: kEnumerableProperty,
});

module.exports.OfflineAudioCompletionEvent = OfflineAudioCompletionEvent;
module.exports.AudioProcessingEvent = AudioProcessingEvent;
module.exports.AudioRenderCapacityEvent = AudioRenderCapacityEvent;
2 changes: 2 additions & 0 deletions js/lib/symbols.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ module.exports.kOnComplete = Symbol.for('node-web-audio-api:oncomplete');
module.exports.kOnEnded = Symbol.for('node-web-audio-api:onended');
// # ScriptProcessorNode
module.exports.kOnAudioProcess = Symbol.for('node-web-audio-api:onaudioprocess');
// # AudioRenderCapacity
module.exports.kOnUpdate = Symbol.for('node-web-audio-api:onupdate');

2 changes: 2 additions & 0 deletions js/monkey-patch.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ module.exports = function monkeyPatch(nativeBinding) {
// --------------------------------------------------------------------------
jsExport.OfflineAudioCompletionEvent = require('./Events').OfflineAudioCompletionEvent;
jsExport.AudioProcessingEvent = require('./Events').AudioProcessingEvent;
jsExport.AudioRenderCapacityEvent = require('./Events').AudioRenderCapacityEvent;
// --------------------------------------------------------------------------
// Create Web Audio API facade
// --------------------------------------------------------------------------
Expand Down Expand Up @@ -55,6 +56,7 @@ module.exports = function monkeyPatch(nativeBinding) {
jsExport.AudioParam = require('./AudioParam.js');
jsExport.AudioDestinationNode = require('./AudioDestinationNode.js');
jsExport.AudioListener = require('./AudioListener.js');
jsExport.AudioRenderCapacity = require('./AudioRenderCapacity.js');

jsExport.PeriodicWave = require('./PeriodicWave.js')(jsExport, nativeBinding);
jsExport.AudioBuffer = require('./AudioBuffer.js')(jsExport, nativeBinding);
Expand Down
Loading

0 comments on commit 63aafce

Please sign in to comment.