From eb581bfb4d249b205acd88d2173c2fa0e65c9ac5 Mon Sep 17 00:00:00 2001 From: b-ma Date: Thu, 28 Dec 2023 18:18:41 +0100 Subject: [PATCH 1/4] feat: add legacy decodeAudioData signature --- js/AudioContext.js | 22 +++++++++++++++++++--- js/OfflineAudioContext.js | 21 +++++++++++++++++---- js/lib/utils.js | 5 +++++ 3 files changed, 41 insertions(+), 7 deletions(-) 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]'; +} From 3e816a44dd660432571de7cb3adc42d2fc7c5513 Mon Sep 17 00:00:00 2001 From: b-ma Date: Thu, 28 Dec 2023 18:44:49 +0100 Subject: [PATCH 2/4] wtp: add mock for XMLHttpRequest --- .scripts/wpt-mock/XMLHttpRequest.js | 39 ++++++++++++ .scripts/wpt-mock/wpt-buffer-loader.js | 83 ++++++++++++++++++++++++++ examples/decoding-legacy.mjs | 50 ++++++++++++++++ 3 files changed, 172 insertions(+) create mode 100644 .scripts/wpt-mock/XMLHttpRequest.js create mode 100644 .scripts/wpt-mock/wpt-buffer-loader.js create mode 100644 examples/decoding-legacy.mjs diff --git a/.scripts/wpt-mock/XMLHttpRequest.js b/.scripts/wpt-mock/XMLHttpRequest.js new file mode 100644 index 00000000..20786a8e --- /dev/null +++ b/.scripts/wpt-mock/XMLHttpRequest.js @@ -0,0 +1,39 @@ +const fs = require('node:fs'); +// to be passed to wtp-runner step +// window.XMLHttpRequest = XMLHttpRequest; +class XMLHttpRequest { + constructor() { + this._pathname; + this._onload; + this._onerror; + this.response; + } + + open(_protocol, url) { + this._pathname = url; + } + + send() { + let buffer; + + try { + buffer = fs.readFileSync(this._pathname).buffer; + } catch (err) { + this._onerror(err); + return; + } + + this.response = buffer; + this._onload(); + } + + set onload(func) { + this._onload = func; + } + + set onerror(func) { + this._onerror = func; + } +} + +module.exports = XMLHttpRequest; diff --git a/.scripts/wpt-mock/wpt-buffer-loader.js b/.scripts/wpt-mock/wpt-buffer-loader.js new file mode 100644 index 00000000..f21adbd6 --- /dev/null +++ b/.scripts/wpt-mock/wpt-buffer-loader.js @@ -0,0 +1,83 @@ +const path = require('node:path'); + +const XMLHttpRequest = require('./XMLHttpRequest.js'); +const { OfflineAudioContext } = require('../../index.cjs'); + +// 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 okFile = [path.join('examples', 'samples', 'sample.wav')]; +const err1File = [path.join('examples', 'samples', 'corrupt.wav')]; +const err2File = [path.join('examples', 'samples', 'donotexists.wav')]; + +{ + // should work + const loader = new BufferLoader(offlineContext, okFile, audioBuffer => console.log(audioBuffer)); + loader.load(); +} + +{ + // should fail - decode error + const loader = new BufferLoader(offlineContext, err1File, audioBuffer => console.log(audioBuffer)); + loader.load(); +} + +{ + // should fail - file not found + const loader = new BufferLoader(offlineContext, err2File, 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(); From 5f67bc2cf2778de55b334d8a8eb44e0a1badf31b Mon Sep 17 00:00:00 2001 From: b-ma Date: Thu, 28 Dec 2023 19:18:14 +0100 Subject: [PATCH 3/4] refactor: simplify XMLHttpRequest mock --- .scripts/wpt-mock/XMLHttpRequest.js | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/.scripts/wpt-mock/XMLHttpRequest.js b/.scripts/wpt-mock/XMLHttpRequest.js index 20786a8e..d18773b1 100644 --- a/.scripts/wpt-mock/XMLHttpRequest.js +++ b/.scripts/wpt-mock/XMLHttpRequest.js @@ -4,8 +4,8 @@ const fs = require('node:fs'); class XMLHttpRequest { constructor() { this._pathname; - this._onload; - this._onerror; + this.onload; + this.onerror; this.response; } @@ -19,20 +19,12 @@ class XMLHttpRequest { try { buffer = fs.readFileSync(this._pathname).buffer; } catch (err) { - this._onerror(err); + this.onerror(err); return; } this.response = buffer; - this._onload(); - } - - set onload(func) { - this._onload = func; - } - - set onerror(func) { - this._onerror = func; + this.onload(); } } From 3fe7daa696451b456cd1c00d2d2ee58b4e4fd0f1 Mon Sep 17 00:00:00 2001 From: b-ma Date: Thu, 28 Dec 2023 19:29:16 +0100 Subject: [PATCH 4/4] fix: allow to configure XMLHttpRequest root --- .scripts/wpt-mock/XMLHttpRequest.js | 47 ++++++++++++++------------ .scripts/wpt-mock/wpt-buffer-loader.js | 18 ++++++---- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/.scripts/wpt-mock/XMLHttpRequest.js b/.scripts/wpt-mock/XMLHttpRequest.js index d18773b1..e1ac0b16 100644 --- a/.scripts/wpt-mock/XMLHttpRequest.js +++ b/.scripts/wpt-mock/XMLHttpRequest.js @@ -1,31 +1,34 @@ const fs = require('node:fs'); +const path = require('node:path'); + // to be passed to wtp-runner step // window.XMLHttpRequest = XMLHttpRequest; -class XMLHttpRequest { - constructor() { - this._pathname; - this.onload; - this.onerror; - this.response; - } +module.exports = function createXMLHttpRequest(basepath) { + return class XMLHttpRequest { + constructor() { + this._pathname; + this.onload; + this.onerror; + this.response; + } - open(_protocol, url) { - this._pathname = url; - } + open(_protocol, url) { + this._pathname = url; + } - send() { - let buffer; + send() { + let buffer; - try { - buffer = fs.readFileSync(this._pathname).buffer; - } catch (err) { - this.onerror(err); - return; - } + try { + const pathname = path.join(basepath, this._pathname); + buffer = fs.readFileSync(pathname).buffer; + } catch (err) { + this.onerror(err); + return; + } - this.response = buffer; - this.onload(); + this.response = buffer; + this.onload(); + } } } - -module.exports = XMLHttpRequest; diff --git a/.scripts/wpt-mock/wpt-buffer-loader.js b/.scripts/wpt-mock/wpt-buffer-loader.js index f21adbd6..bc916bb7 100644 --- a/.scripts/wpt-mock/wpt-buffer-loader.js +++ b/.scripts/wpt-mock/wpt-buffer-loader.js @@ -1,8 +1,12 @@ const path = require('node:path'); -const XMLHttpRequest = require('./XMLHttpRequest.js'); +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); @@ -60,24 +64,24 @@ const offlineContext = new OfflineAudioContext({ sampleRate: 48000, }); -const okFile = [path.join('examples', 'samples', 'sample.wav')]; -const err1File = [path.join('examples', 'samples', 'corrupt.wav')]; -const err2File = [path.join('examples', 'samples', 'donotexists.wav')]; +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, okFile, audioBuffer => console.log(audioBuffer)); + const loader = new BufferLoader(offlineContext, okFiles, audioBuffer => console.log(audioBuffer)); loader.load(); } { // should fail - decode error - const loader = new BufferLoader(offlineContext, err1File, audioBuffer => console.log(audioBuffer)); + const loader = new BufferLoader(offlineContext, err1Files, audioBuffer => console.log(audioBuffer)); loader.load(); } { // should fail - file not found - const loader = new BufferLoader(offlineContext, err2File, audioBuffer => console.log(audioBuffer)); + const loader = new BufferLoader(offlineContext, err2Files, audioBuffer => console.log(audioBuffer)); loader.load(); }