diff --git a/.scripts/wpt-mock/XMLHttpRequest.js b/.scripts/wpt-mock/XMLHttpRequest.js new file mode 100644 index 00000000..e1ac0b16 --- /dev/null +++ b/.scripts/wpt-mock/XMLHttpRequest.js @@ -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(); + } + } +} diff --git a/.scripts/wpt-mock/wpt-buffer-loader.js b/.scripts/wpt-mock/wpt-buffer-loader.js new file mode 100644 index 00000000..bc916bb7 --- /dev/null +++ b/.scripts/wpt-mock/wpt-buffer-loader.js @@ -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(); +} diff --git a/examples/decoding-legacy.mjs b/examples/decoding-legacy.mjs new file mode 100644 index 00000000..afba8741 --- /dev/null +++ b/examples/decoding-legacy.mjs @@ -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(); diff --git a/js/AudioContext.js b/js/AudioContext.js index 2dbc6ecf..a6d84079 100644 --- a/js/AudioContext.js +++ b/js/AudioContext.js @@ -1,3 +1,5 @@ +const { isFunction } = require('./lib/utils.js'); + let contextId = 0; const kProcessId = Symbol('processId'); @@ -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); + } } } } diff --git a/js/OfflineAudioContext.js b/js/OfflineAudioContext.js index b9552915..fa80c617 100644 --- a/js/OfflineAudioContext.js +++ b/js/OfflineAudioContext.js @@ -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 { @@ -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); + } } } } diff --git a/js/lib/utils.js b/js/lib/utils.js index 61ca06ae..0e4af00d 100644 --- a/js/lib/utils.js +++ b/js/lib/utils.js @@ -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]'; +}