// Copyright 2016 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileOverview This file includes legacy utility functions for the layout * test. */ // How many frames in a WebAudio render quantum. let RENDER_QUANTUM_FRAMES = 128; // Compare two arrays (commonly extracted from buffer.getChannelData()) with // constraints: // options.thresholdSNR: Minimum allowed SNR between the actual and expected // signal. The default value is 10000. // options.thresholdDiffULP: Maximum allowed difference between the actual // and expected signal in ULP(Unit in the last place). The default is 0. // options.thresholdDiffCount: Maximum allowed number of sample differences // which exceeds the threshold. The default is 0. // options.bitDepth: The expected result is assumed to come from an audio // file with this number of bits of precision. The default is 16. function compareBuffersWithConstraints(should, actual, expected, options) { if (!options) options = {}; // Only print out the message if the lengths are different; the // expectation is that they are the same, so don't clutter up the // output. if (actual.length !== expected.length) { should( actual.length === expected.length, 'Length of actual and expected buffers should match') .beTrue(); } let maxError = -1; let diffCount = 0; let errorPosition = -1; let thresholdSNR = (options.thresholdSNR || 10000); let thresholdDiffULP = (options.thresholdDiffULP || 0); let thresholdDiffCount = (options.thresholdDiffCount || 0); // By default, the bit depth is 16. let bitDepth = (options.bitDepth || 16); let scaleFactor = Math.pow(2, bitDepth - 1); let noisePower = 0, signalPower = 0; for (let i = 0; i < actual.length; i++) { let diff = actual[i] - expected[i]; noisePower += diff * diff; signalPower += expected[i] * expected[i]; if (Math.abs(diff) > maxError) { maxError = Math.abs(diff); errorPosition = i; } // The reference file is a 16-bit WAV file, so we will almost never get // an exact match between it and the actual floating-point result. if (Math.abs(diff) > scaleFactor) diffCount++; } let snr = 10 * Math.log10(signalPower / noisePower); let maxErrorULP = maxError * scaleFactor; should(snr, 'SNR').beGreaterThanOrEqualTo(thresholdSNR); should( maxErrorULP, options.prefix + ': Maximum difference (in ulp units (' + bitDepth + '-bits))') .beLessThanOrEqualTo(thresholdDiffULP); should(diffCount, options.prefix + ': Number of differences between results') .beLessThanOrEqualTo(thresholdDiffCount); } // Create an impulse in a buffer of length sampleFrameLength function createImpulseBuffer(context, sampleFrameLength) { let audioBuffer = context.createBuffer(1, sampleFrameLength, context.sampleRate); let n = audioBuffer.length; let dataL = audioBuffer.getChannelData(0); for (let k = 0; k < n; ++k) { dataL[k] = 0; } dataL[0] = 1; return audioBuffer; } // Create a buffer of the given length with a linear ramp having values 0 <= x < // 1. function createLinearRampBuffer(context, sampleFrameLength) { let audioBuffer = context.createBuffer(1, sampleFrameLength, context.sampleRate); let n = audioBuffer.length; let dataL = audioBuffer.getChannelData(0); for (let i = 0; i < n; ++i) dataL[i] = i / n; return audioBuffer; } // Create an AudioBuffer of length |sampleFrameLength| having a constant value // |constantValue|. If |constantValue| is a number, the buffer has one channel // filled with that value. If |constantValue| is an array, the buffer is created // wit a number of channels equal to the length of the array, and channel k is // filled with the k'th element of the |constantValue| array. function createConstantBuffer(context, sampleFrameLength, constantValue) { let channels; let values; if (typeof constantValue === 'number') { channels = 1; values = [constantValue]; } else { channels = constantValue.length; values = constantValue; } let audioBuffer = context.createBuffer(channels, sampleFrameLength, context.sampleRate); let n = audioBuffer.length; for (let c = 0; c < channels; ++c) { let data = audioBuffer.getChannelData(c); for (let i = 0; i < n; ++i) data[i] = values[c]; } return audioBuffer; } // Create a stereo impulse in a buffer of length sampleFrameLength function createStereoImpulseBuffer(context, sampleFrameLength) { let audioBuffer = context.createBuffer(2, sampleFrameLength, context.sampleRate); let n = audioBuffer.length; let dataL = audioBuffer.getChannelData(0); let dataR = audioBuffer.getChannelData(1); for (let k = 0; k < n; ++k) { dataL[k] = 0; dataR[k] = 0; } dataL[0] = 1; dataR[0] = 1; return audioBuffer; } // Convert time (in seconds) to sample frames. function timeToSampleFrame(time, sampleRate) { return Math.floor(0.5 + time * sampleRate); } // Compute the number of sample frames consumed by noteGrainOn with // the specified |grainOffset|, |duration|, and |sampleRate|. function grainLengthInSampleFrames(grainOffset, duration, sampleRate) { let startFrame = timeToSampleFrame(grainOffset, sampleRate); let endFrame = timeToSampleFrame(grainOffset + duration, sampleRate); return endFrame - startFrame; } // True if the number is not an infinity or NaN function isValidNumber(x) { return !isNaN(x) && (x != Infinity) && (x != -Infinity); } // Compute the (linear) signal-to-noise ratio between |actual| and // |expected|. The result is NOT in dB! If the |actual| and // |expected| have different lengths, the shorter length is used. function computeSNR(actual, expected) { let signalPower = 0; let noisePower = 0; let length = Math.min(actual.length, expected.length); for (let k = 0; k < length; ++k) { let diff = actual[k] - expected[k]; signalPower += expected[k] * expected[k]; noisePower += diff * diff; } return signalPower / noisePower; } /** * Asserts that all elements in the given array are equal to the specified value * If the value is NaN, checks that each element in the array is also NaN. * Throws an assertion error if any element does not match the expected value. * * @param {Array} array - The array of numbers to check. * @param {number} value - The constant that each array element should match. * @param {string} [messagePrefix=''] - Optional for assertion error messages. */ function assert_constant_value(array, value, messagePrefix = '') { for (let i = 0; i < array.length; ++i) { if (Number.isNaN(value)) { assert_true( Number.isNaN(array[i]), `${messagePrefix} entry ${i} should be NaN` ); } else { assert_equals( array[i], value, `${messagePrefix} entry ${i} should be ${value}` ); } } } /** * Asserts that two arrays are exactly equal, element by element. * @param {!Array} actual The actual array of values. * @param {!Array} expected The expected array of values. * @param {string} message Description used for assertion failures. */ function assert_array_equals_exact(actual, expected, message) { assert_equals(actual.length, expected.length, 'Buffers must be same length'); for (let i = 0; i < actual.length; ++i) { assert_equals(actual[i], expected[i], `${message} (at index ${i})`); } } /** * Asserts that an array is not a constant array * (i.e., not all values are equal to the given constant). * @param {!Array} array The array to be checked. * @param {number} constantValue The constant value to compare against. * @param {string} message Description used for assertion failures. */ function assert_not_constant_value(array, constantValue, message) { const notAllSame = array.some(value => value !== constantValue); assert_true(notAllSame, message); } /** * Asserts that all elements of an array are exactly equal to a constant value. * @param {!Array} array The array to be checked. * @param {number} constantValue The expected constant value. * @param {string} message Description used for assertion failures. */ function assert_strict_constant_value(array, constantValue, message) { const allSame = array.every(value => value === constantValue); assert_true(allSame, message); } /** * Asserts that all elements of an array are (approximately) equal to a value. * * @param {!Array} array - The array to be checked. * @param {number} constantValue - The expected constant value. * @param {string} message - Description used for assertion failures. * @param {number=} epsilon - Allowed tolerance for floating-point comparison. * Default to 1e-7 */ function assert_array_constant_value( array, constantValue, message, epsilon = 1e-7) { for (let i = 0; i < array.length; ++i) { assert_approx_equals( array[i], constantValue, epsilon, `${message} sample[${i}]`); } } /** * Asserts that two arrays are equal within a given tolerance for each element. * The |threshold| can be: * - A number (absolute epsilon) * - An object with optional {absoluteThreshold, relativeThreshold} * - If omitted, compares with exact equality (epsilon = 0) * * For each element i, we require: * |actual[i] − expected[i]| ≤ max(absoluteThreshold, * |expected[i]|·relativeThreshold) * * @param {!Array|!TypedArray} actual * @param {!Array|!TypedArray} expected * @param {number|{ absoluteThreshold?:number, relativeThreshold?:number }} * [threshold=0] * @param {string} desc */ function assert_array_equal_within_eps( actual, expected, threshold = 0, desc) { assert_equals(actual.length, expected.length, desc + ': length mismatch'); let abs = 0; let rel = 0; if (typeof threshold === 'number') { abs = threshold; } else if (threshold && typeof threshold === 'object') { abs = threshold.absoluteThreshold ?? 0; rel = threshold.relativeThreshold ?? 0; } for (let i = 0; i < actual.length; ++i) { const epsilon = Math.max(abs, Math.abs(expected[i]) * rel); const diff = Math.abs(actual[i] - expected[i]); assert_approx_equals( actual[i], expected[i], epsilon, `${desc} sample[${i}] |${actual[i]} - ${expected[i]}|` + ` = ${diff} > ${epsilon}`); } }