diff options
Diffstat (limited to 'testing/web-platform/tests/webaudio/js')
3 files changed, 349 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webaudio/js/buffer-loader.js b/testing/web-platform/tests/webaudio/js/buffer-loader.js new file mode 100644 index 0000000000..453dc4a521 --- /dev/null +++ b/testing/web-platform/tests/webaudio/js/buffer-loader.js @@ -0,0 +1,44 @@ +/* Taken from + https://raw.github.com/WebKit/webkit/master/LayoutTests/webaudio/resources/buffer-loader.js */ + +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); +} diff --git a/testing/web-platform/tests/webaudio/js/helpers.js b/testing/web-platform/tests/webaudio/js/helpers.js new file mode 100644 index 0000000000..413c72051b --- /dev/null +++ b/testing/web-platform/tests/webaudio/js/helpers.js @@ -0,0 +1,250 @@ +/* + Returns an array (typed or not), of the passed array with removed trailing and ending + zero-valued elements + */ +function trimEmptyElements(array) { + var start = 0; + var end = array.length; + + while (start < array.length) { + if (array[start] !== 0) { + break; + } + start++; + } + + while (end > 0) { + end--; + if (array[end] !== 0) { + break; + } + } + return array.subarray(start, end); +} + + +function fuzzyCompare(a, b) { + return Math.abs(a - b) < 9e-3; +} + +function compareChannels(buf1, buf2, + /*optional*/ length, + /*optional*/ sourceOffset, + /*optional*/ destOffset, + /*optional*/ skipLengthCheck) { + if (!skipLengthCheck) { + assert_equals(buf1.length, buf2.length, "Channels must have the same length"); + } + sourceOffset = sourceOffset || 0; + destOffset = destOffset || 0; + if (length == undefined) { + length = buf1.length - sourceOffset; + } + var difference = 0; + var maxDifference = 0; + var firstBadIndex = -1; + for (var i = 0; i < length; ++i) { + if (!fuzzyCompare(buf1[i + sourceOffset], buf2[i + destOffset])) { + difference++; + maxDifference = Math.max(maxDifference, Math.abs(buf1[i + sourceOffset] - buf2[i + destOffset])); + if (firstBadIndex == -1) { + firstBadIndex = i; + } + } + }; + + assert_equals(difference, 0, "maxDifference: " + maxDifference + + ", first bad index: " + firstBadIndex + " with test-data offset " + + sourceOffset + " and expected-data offset " + destOffset + + "; corresponding values " + buf1[firstBadIndex + sourceOffset] + " and " + + buf2[firstBadIndex + destOffset] + " --- differences"); +} + +function compareBuffers(got, expected) { + if (got.numberOfChannels != expected.numberOfChannels) { + assert_equals(got.numberOfChannels, expected.numberOfChannels, + "Correct number of buffer channels"); + return; + } + if (got.length != expected.length) { + assert_equals(got.length, expected.length, + "Correct buffer length"); + return; + } + if (got.sampleRate != expected.sampleRate) { + assert_equals(got.sampleRate, expected.sampleRate, + "Correct sample rate"); + return; + } + + for (var i = 0; i < got.numberOfChannels; ++i) { + compareChannels(got.getChannelData(i), expected.getChannelData(i), + got.length, 0, 0, true); + } +} + +/** + * This function assumes that the test is a "single page test" [0], and defines a + * single gTest variable with the following properties and methods: + * + * + numberOfChannels: optional property which specifies the number of channels + * in the output. The default value is 2. + * + createGraph: mandatory method which takes a context object and does + * everything needed in order to set up the Web Audio graph. + * This function returns the node to be inspected. + * + createGraphAsync: async version of createGraph. This function takes + * a callback which should be called with an argument + * set to the node to be inspected when the callee is + * ready to proceed with the test. Either this function + * or createGraph must be provided. + * + createExpectedBuffers: optional method which takes a context object and + * returns either one expected buffer or an array of + * them, designating what is expected to be observed + * in the output. If omitted, the output is expected + * to be silence. All buffers must have the same + * length, which must be a bufferSize supported by + * ScriptProcessorNode. This function is guaranteed + * to be called before createGraph. + * + length: property equal to the total number of frames which we are waiting + * to see in the output, mandatory if createExpectedBuffers is not + * provided, in which case it must be a bufferSize supported by + * ScriptProcessorNode (256, 512, 1024, 2048, 4096, 8192, or 16384). + * If createExpectedBuffers is provided then this must be equal to + * the number of expected buffers * the expected buffer length. + * + * + skipOfflineContextTests: optional. when true, skips running tests on an offline + * context by circumventing testOnOfflineContext. + * + * [0]: https://web-platform-tests.org/writing-tests/testharness-api.html#single-page-tests + */ +function runTest(name) +{ + function runTestFunction () { + if (!gTest.numberOfChannels) { + gTest.numberOfChannels = 2; // default + } + + var testLength; + + function runTestOnContext(context, callback, testOutput) { + if (!gTest.createExpectedBuffers) { + // Assume that the output is silence + var expectedBuffers = getEmptyBuffer(context, gTest.length); + } else { + var expectedBuffers = gTest.createExpectedBuffers(context); + } + if (!(expectedBuffers instanceof Array)) { + expectedBuffers = [expectedBuffers]; + } + var expectedFrames = 0; + for (var i = 0; i < expectedBuffers.length; ++i) { + assert_equals(expectedBuffers[i].numberOfChannels, gTest.numberOfChannels, + "Correct number of channels for expected buffer " + i); + expectedFrames += expectedBuffers[i].length; + } + if (gTest.length && gTest.createExpectedBuffers) { + assert_equals(expectedFrames, + gTest.length, "Correct number of expected frames"); + } + + if (gTest.createGraphAsync) { + gTest.createGraphAsync(context, function(nodeToInspect) { + testOutput(nodeToInspect, expectedBuffers, callback); + }); + } else { + testOutput(gTest.createGraph(context), expectedBuffers, callback); + } + } + + function testOnNormalContext(callback) { + function testOutput(nodeToInspect, expectedBuffers, callback) { + testLength = 0; + var sp = context.createScriptProcessor(expectedBuffers[0].length, gTest.numberOfChannels, 1); + nodeToInspect.connect(sp).connect(context.destination); + sp.onaudioprocess = function(e) { + var expectedBuffer = expectedBuffers.shift(); + testLength += expectedBuffer.length; + compareBuffers(e.inputBuffer, expectedBuffer); + if (expectedBuffers.length == 0) { + sp.onaudioprocess = null; + callback(); + } + }; + } + var context = new AudioContext(); + runTestOnContext(context, callback, testOutput); + } + + function testOnOfflineContext(callback, sampleRate) { + function testOutput(nodeToInspect, expectedBuffers, callback) { + nodeToInspect.connect(context.destination); + context.oncomplete = function(e) { + var samplesSeen = 0; + while (expectedBuffers.length) { + var expectedBuffer = expectedBuffers.shift(); + assert_equals(e.renderedBuffer.numberOfChannels, expectedBuffer.numberOfChannels, + "Correct number of input buffer channels"); + for (var i = 0; i < e.renderedBuffer.numberOfChannels; ++i) { + compareChannels(e.renderedBuffer.getChannelData(i), + expectedBuffer.getChannelData(i), + expectedBuffer.length, + samplesSeen, + undefined, + true); + } + samplesSeen += expectedBuffer.length; + } + callback(); + }; + context.startRendering(); + } + + var context = new OfflineAudioContext(gTest.numberOfChannels, testLength, sampleRate); + runTestOnContext(context, callback, testOutput); + } + + testOnNormalContext(function() { + if (!gTest.skipOfflineContextTests) { + testOnOfflineContext(function() { + testOnOfflineContext(done, 44100); + }, 48000); + } else { + done(); + } + }); + }; + + runTestFunction(); +} + +// Simpler than audit.js, but still logs the message. Requires +// `setup("explicit_done": true)` if testing code that runs after the "load" +// event. +function equals(a, b, msg) { + test(function() { + assert_equals(a, b); + }, msg); +} +function is_true(a, msg) { + test(function() { + assert_true(a); + }, msg); +} + +// This allows writing AudioWorkletProcessor code in the same file as the rest +// of the test, for quick one off AudioWorkletProcessor testing. +function URLFromScriptsElements(ids) +{ + var scriptTexts = []; + for (let id of ids) { + + const e = document.querySelector("script#"+id) + if (!e) { + throw id+" is not the id of a <script> tag"; + } + scriptTexts.push(e.innerText); + } + const blob = new Blob(scriptTexts, {type: "application/javascript"}); + + return URL.createObjectURL(blob); +} diff --git a/testing/web-platform/tests/webaudio/js/worklet-recorder.js b/testing/web-platform/tests/webaudio/js/worklet-recorder.js new file mode 100644 index 0000000000..913ab742aa --- /dev/null +++ b/testing/web-platform/tests/webaudio/js/worklet-recorder.js @@ -0,0 +1,55 @@ +/** + * @class RecorderProcessor + * @extends AudioWorkletProcessor + * + * A simple recorder AudioWorkletProcessor. Returns the recorded buffer to the + * node when recording is finished. + */ +class RecorderProcessor extends AudioWorkletProcessor { + /** + * @param {*} options + * @param {number} options.duration A duration to record in seconds. + * @param {number} options.channelCount A channel count to record. + */ + constructor(options) { + super(); + this._createdAt = currentTime; + this._elapsed = 0; + this._recordDuration = options.duration || 1; + this._recordChannelCount = options.channelCount || 1; + this._recordBufferLength = sampleRate * this._recordDuration; + this._recordBuffer = []; + for (let i = 0; i < this._recordChannelCount; ++i) { + this._recordBuffer[i] = new Float32Array(this._recordBufferLength); + } + } + + process(inputs, outputs) { + if (this._recordBufferLength <= currentFrame) { + this.port.postMessage({ + type: 'recordfinished', + recordBuffer: this._recordBuffer + }); + this.port.close(); + return false; + } + + // Records the incoming data from |inputs| and also bypasses the data to + // |outputs|. + const input = inputs[0]; + const output = outputs[0]; + for (let channel = 0; channel < input.length; ++channel) { + const inputChannel = input[channel]; + const outputChannel = output[channel]; + outputChannel.set(inputChannel); + + const buffer = this._recordBuffer[channel]; + const capacity = buffer.length - currentFrame; + buffer.set(inputChannel.slice(0, capacity), currentFrame); + } + + return true; + } +} + +registerProcessor('recorder-processor', RecorderProcessor); |