// Helpers for Web Audio tests function expectException(func, exceptionCode) { var threw = false; try { func(); } catch (ex) { threw = true; is(ex.constructor.name, "DOMException", "Expect a DOM exception"); is(ex.code, exceptionCode, "Expect the correct exception code"); } ok(threw, "The exception was thrown"); } function expectNoException(func) { var threw = false; try { func(); } catch (ex) { threw = true; } ok(!threw, "An exception was not thrown"); } function expectTypeError(func) { var threw = false; try { func(); } catch (ex) { threw = true; ok(ex instanceof TypeError, "Expect a TypeError"); } ok(threw, "The exception was thrown"); } function expectRejectedPromise(that, func, exceptionName) { var promise = that[func](); ok(promise instanceof Promise, "Expect a Promise"); promise .then(function (res) { ok(false, "Promise resolved when it should have been rejected."); }) .catch(function (err) { is( err.name, exceptionName, "Promise correctly reject with " + exceptionName ); }); } 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) { is(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; } } } is( 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) { is( got.numberOfChannels, expected.numberOfChannels, "Correct number of buffer channels" ); return; } if (got.length != expected.length) { is(got.length, expected.length, "Correct buffer length"); return; } if (got.sampleRate != expected.sampleRate) { is(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 ); } } /** * Compute the root mean square (RMS, * ) of a channel of a slice * (defined by `start` and `end`) of an AudioBuffer. * * This is useful to detect that a buffer is noisy or silent. */ function rms(audiobuffer, channel = 0, start = 0, end = audiobuffer.length) { var buffer = audiobuffer.getChannelData(channel); var rms = 0; for (var i = start; i < end; i++) { rms += buffer[i] * buffer[i]; } rms /= buffer.length; rms = Math.sqrt(rms); return rms; } function getEmptyBuffer(context, length) { return context.createBuffer( gTest.numberOfChannels, length, context.sampleRate ); } /** * This function assumes that the test file 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. */ function runTest() { function done() { SimpleTest.finish(); } SimpleTest.waitForExplicitFinish(); 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) { is( expectedBuffers[i].numberOfChannels, gTest.numberOfChannels, "Correct number of channels for expected buffer " + i ); expectedFrames += expectedBuffers[i].length; } if (gTest.length && gTest.createExpectedBuffers) { is(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, 0 ); nodeToInspect.connect(sp); sp.onaudioprocess = function (e) { var expectedBuffer = expectedBuffers.shift(); testLength += expectedBuffer.length; compareBuffers(e.inputBuffer, expectedBuffer); if (!expectedBuffers.length) { 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(); is( 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(); } }); } if (document.readyState !== "complete") { addLoadEvent(runTestFunction); } else { runTestFunction(); } }