Skip to content
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

feat: add legacy decodeAudioData signature #60

Merged
merged 4 commits into from
Dec 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .scripts/wpt-mock/XMLHttpRequest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const fs = require('node:fs');
const path = require('node:path');

// to be passed to wtp-runner step
// window.XMLHttpRequest = XMLHttpRequest;
module.exports = function createXMLHttpRequest(basepath) {
return class XMLHttpRequest {
constructor() {
this._pathname;
this.onload;
this.onerror;
this.response;
}

open(_protocol, url) {
this._pathname = url;
}

send() {
let buffer;

try {
const pathname = path.join(basepath, this._pathname);
buffer = fs.readFileSync(pathname).buffer;
} catch (err) {
this.onerror(err);
return;
}

this.response = buffer;
this.onload();
}
}
}
87 changes: 87 additions & 0 deletions .scripts/wpt-mock/wpt-buffer-loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
const path = require('node:path');

const createXMLHttpRequest = require('./XMLHttpRequest.js');
const { OfflineAudioContext } = require('../../index.cjs');

// create a XMLHttpRequest to be passed to the runner
// can be configured to handle the difference between process.cwd() and given path
// window.XMLHttpRequest = createXMLHttpRequest(rootURL (?))
const XMLHttpRequest = createXMLHttpRequest(path.join('examples', 'samples'));
// maybe should be passed to wtp-runner setup too
// window.alert = console.log.bind(console);
const alert = console.log.bind(console);

// this is the BufferLoader from the wpt suite
function BufferLoader(context, urlList, callback) {
this.context = context;
this.urlList = urlList;
this.onload = callback;
this.bufferList = new Array();
this.loadCount = 0;
}

BufferLoader.prototype.loadBuffer = function(url, index) {
// Load buffer asynchronously
var request = new XMLHttpRequest();
request.open("GET", url, true);
request.responseType = "arraybuffer";

var loader = this;

request.onload = function() {
loader.context.decodeAudioData(request.response, decodeSuccessCallback, decodeErrorCallback);
};

request.onerror = function() {
alert('BufferLoader: XHR error');
};

var decodeSuccessCallback = function(buffer) {
loader.bufferList[index] = buffer;
if (++loader.loadCount == loader.urlList.length)
loader.onload(loader.bufferList);
};

var decodeErrorCallback = function() {
alert('decodeErrorCallback: decode error');
};

request.send();
}

BufferLoader.prototype.load = function() {
for (var i = 0; i < this.urlList.length; ++i)
this.loadBuffer(this.urlList[i], i);
}

// ----------------------------------------------
// testing
// ----------------------------------------------

const offlineContext = new OfflineAudioContext({
numberOfChannels: 1,
length: 1,
sampleRate: 48000,
});

const okFiles = [path.join('sample.wav')];
const err1Files = [path.join('corrupt.wav')];
const err2Files = [path.join('donotexists.wav')];

{
// should work
const loader = new BufferLoader(offlineContext, okFiles, audioBuffer => console.log(audioBuffer));
loader.load();
}

{
// should fail - decode error
const loader = new BufferLoader(offlineContext, err1Files, audioBuffer => console.log(audioBuffer));
loader.load();
}

{
// should fail - file not found
const loader = new BufferLoader(offlineContext, err2Files, audioBuffer => console.log(audioBuffer));
loader.load();
}
50 changes: 50 additions & 0 deletions examples/decoding-legacy.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import fs from 'node:fs';
import path from 'node:path';
import { AudioContext, OfflineAudioContext } from '../index.mjs';

const latencyHint = process.env.WEB_AUDIO_LATENCY === 'playback' ? 'playback' : 'interactive';
const audioContext = new AudioContext({ latencyHint });

const offlineContext = new OfflineAudioContext({
numberOfChannels: 1,
length: 1,
sampleRate: audioContext.sampleRate
});

const okFile = path.join('examples', 'samples', 'sample.wav');
const errFile = path.join('examples', 'samples', 'corrupt.wav');

function decodeSuccess(buffer) {
console.log(`decodeSuccess`);
const src = audioContext.createBufferSource();
src.buffer = buffer;
src.connect(audioContext.destination);
src.start();
}

function decodeError(err) {
console.log(`decodeError callback: ${err.message}`);
}

{
// audioContext decode success
const okArrayBuffer = fs.readFileSync(okFile).buffer;
audioContext.decodeAudioData(okArrayBuffer, decodeSuccess, decodeError);
// audioContext decode error
const errArrayBuffer = fs.readFileSync(errFile).buffer;
audioContext.decodeAudioData(errArrayBuffer, decodeSuccess, decodeError);
}

await new Promise(resolve => setTimeout(resolve, 3000));

{
// offlineContext decode success
const okArrayBuffer = fs.readFileSync(okFile).buffer;
offlineContext.decodeAudioData(okArrayBuffer, decodeSuccess, decodeError);
// offlineContext decode error
const errArrayBuffer = fs.readFileSync(errFile).buffer;
offlineContext.decodeAudioData(errArrayBuffer, decodeSuccess, decodeError);
}

await new Promise(resolve => setTimeout(resolve, 3000));
await audioContext.close();
22 changes: 19 additions & 3 deletions js/AudioContext.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const { isFunction } = require('./lib/utils.js');

let contextId = 0;

const kProcessId = Symbol('processId');
Expand Down Expand Up @@ -45,15 +47,29 @@ module.exports = function(NativeAudioContext) {
}
}

decodeAudioData(audioData) {
// This is not exactly what the spec says, but if we reject the promise
// when `decodeErrorCallback` is present the program will crash in an
// unexpected manner
// cf. https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-decodeaudiodata
decodeAudioData(audioData, decodeSuccessCallback, decodeErrorCallback) {
if (!(audioData instanceof ArrayBuffer)) {
throw new TypeError(`Failed to execute 'decodeAudioData': parameter 1 is not of type 'ArrayBuffer'`);
}

try {
const audioBuffer = super.decodeAudioData(audioData);
return Promise.resolve(audioBuffer);

if (isFunction(decodeSuccessCallback)) {
decodeSuccessCallback(audioBuffer);
} else {
return Promise.resolve(audioBuffer);
}
} catch (err) {
return Promise.reject(err);
if (isFunction(decodeErrorCallback)) {
decodeErrorCallback(err);
} else {
return Promise.reject(err);
}
}
}
}
Expand Down
21 changes: 17 additions & 4 deletions js/OfflineAudioContext.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const { NotSupportedError } = require('./lib/errors.js');
const { isPlainObject, isPositiveInt, isPositiveNumber } = require('./lib/utils.js');
const { isFunction, isPlainObject, isPositiveInt, isPositiveNumber } = require('./lib/utils.js');

module.exports = function patchOfflineAudioContext(NativeOfflineAudioContext) {
class OfflineAudioContext extends NativeOfflineAudioContext {
Expand Down Expand Up @@ -36,16 +36,29 @@ module.exports = function patchOfflineAudioContext(NativeOfflineAudioContext) {
}
}

decodeAudioData(audioData) {
// This is not exactly what the spec says, but if we reject the promise
// when `decodeErrorCallback` is present the program will crash in an
// unexpected manner
// cf. https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-decodeaudiodata
decodeAudioData(audioData, decodeSuccessCallback, decodeErrorCallback) {
if (!(audioData instanceof ArrayBuffer)) {
throw new TypeError(`Failed to execute 'decodeAudioData': parameter 1 is not of type 'ArrayBuffer'`);
}

try {
const audioBuffer = super.decodeAudioData(audioData);
return Promise.resolve(audioBuffer);

if (isFunction(decodeSuccessCallback)) {
decodeSuccessCallback(audioBuffer);
} else {
return Promise.resolve(audioBuffer);
}
} catch (err) {
return Promise.reject(err);
if (isFunction(decodeErrorCallback)) {
decodeErrorCallback(err);
} else {
return Promise.reject(err);
}
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions js/lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ exports.isPositiveInt = function isPositiveInt(n) {
exports.isPositiveNumber = function isPositiveNumber(n) {
return Number(n) === n && 0 < n;
};

exports.isFunction = function isFunction(val) {
return Object.prototype.toString.call(val) == '[object Function]' ||
Object.prototype.toString.call(val) == '[object AsyncFunction]';
}