diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /testing/web-platform/tests/webaudio/resources/distance-model-testing.js | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/webaudio/resources/distance-model-testing.js')
-rw-r--r-- | testing/web-platform/tests/webaudio/resources/distance-model-testing.js | 196 |
1 files changed, 196 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webaudio/resources/distance-model-testing.js b/testing/web-platform/tests/webaudio/resources/distance-model-testing.js new file mode 100644 index 0000000000..f8a6cf940a --- /dev/null +++ b/testing/web-platform/tests/webaudio/resources/distance-model-testing.js @@ -0,0 +1,196 @@ +// Use a power of two to eliminate round-off when converting frames to time and +// vice versa. +let sampleRate = 32768; + +// How many panner nodes to create for the test. +let nodesToCreate = 100; + +// Time step when each panner node starts. Make sure it starts on a frame +// boundary. +let timeStep = Math.floor(0.001 * sampleRate) / sampleRate; + +// Make sure we render long enough to get all of our nodes. +let renderLengthSeconds = timeStep * (nodesToCreate + 1); + +// Length of an impulse signal. +let pulseLengthFrames = Math.round(timeStep * sampleRate); + +// Globals to make debugging a little easier. +let context; +let impulse; +let bufferSource; +let panner; +let position; +let time; + +// For the record, these distance formulas were taken from the OpenAL +// spec +// (http://connect.creativelabs.com/openal/Documentation/OpenAL%201.1%20Specification.pdf), +// not the code. The Web Audio spec follows the OpenAL formulas. + +function linearDistance(panner, x, y, z) { + let distance = Math.sqrt(x * x + y * y + z * z); + distance = Math.min(distance, panner.maxDistance); + let rolloff = panner.rolloffFactor; + let gain = + (1 - + rolloff * (distance - panner.refDistance) / + (panner.maxDistance - panner.refDistance)); + + return gain; +} + +function inverseDistance(panner, x, y, z) { + let distance = Math.sqrt(x * x + y * y + z * z); + distance = Math.min(distance, panner.maxDistance); + let rolloff = panner.rolloffFactor; + let gain = panner.refDistance / + (panner.refDistance + rolloff * (distance - panner.refDistance)); + + return gain; +} + +function exponentialDistance(panner, x, y, z) { + let distance = Math.sqrt(x * x + y * y + z * z); + distance = Math.min(distance, panner.maxDistance); + let rolloff = panner.rolloffFactor; + let gain = Math.pow(distance / panner.refDistance, -rolloff); + + return gain; +} + +// Map the distance model to the function that implements the model +let distanceModelFunction = { + 'linear': linearDistance, + 'inverse': inverseDistance, + 'exponential': exponentialDistance +}; + +function createGraph(context, distanceModel, nodeCount) { + bufferSource = new Array(nodeCount); + panner = new Array(nodeCount); + position = new Array(nodeCount); + time = new Array(nodesToCreate); + + impulse = createImpulseBuffer(context, pulseLengthFrames); + + // Create all the sources and panners. + // + // We MUST use the EQUALPOWER panning model so that we can easily + // figure out the gain introduced by the panner. + // + // We want to stay in the middle of the panning range, which means + // we want to stay on the z-axis. If we don't, then the effect of + // panning model will be much more complicated. We're not testing + // the panner, but the distance model, so we want the panner effect + // to be simple. + // + // The panners are placed at a uniform intervals between the panner + // reference distance and the panner max distance. The source is + // also started at regular intervals. + for (let k = 0; k < nodeCount; ++k) { + bufferSource[k] = context.createBufferSource(); + bufferSource[k].buffer = impulse; + + panner[k] = context.createPanner(); + panner[k].panningModel = 'equalpower'; + panner[k].distanceModel = distanceModel; + + let distanceStep = + (panner[k].maxDistance - panner[k].refDistance) / nodeCount; + position[k] = distanceStep * k + panner[k].refDistance; + panner[k].setPosition(0, 0, position[k]); + + bufferSource[k].connect(panner[k]); + panner[k].connect(context.destination); + + time[k] = k * timeStep; + bufferSource[k].start(time[k]); + } +} + +// distanceModel should be the distance model string like +// "linear", "inverse", or "exponential". +function createTestAndRun(context, distanceModel, should) { + // To test the distance models, we create a number of panners at + // uniformly spaced intervals on the z-axis. Each of these are + // started at equally spaced time intervals. After rendering the + // signals, we examine where each impulse is located and the + // attenuation of the impulse. The attenuation is compared + // against our expected attenuation. + + createGraph(context, distanceModel, nodesToCreate); + + return context.startRendering().then( + buffer => checkDistanceResult(buffer, distanceModel, should)); +} + +// The gain caused by the EQUALPOWER panning model, if we stay on the +// z axis, with the default orientations. +function equalPowerGain() { + return Math.SQRT1_2; +} + +function checkDistanceResult(renderedBuffer, model, should) { + renderedData = renderedBuffer.getChannelData(0); + + // The max allowed error between the actual gain and the expected + // value. This is determined experimentally. Set to 0 to see + // what the actual errors are. + let maxAllowedError = 2.2720e-6; + + let success = true; + + // Number of impulses we found in the rendered result. + let impulseCount = 0; + + // Maximum relative error in the gain of the impulses. + let maxError = 0; + + // Array of locations of the impulses that were not at the + // expected location. (Contains the actual and expected frame + // of the impulse.) + let impulsePositionErrors = new Array(); + + // Step through the rendered data to find all the non-zero points + // so we can find where our distance-attenuated impulses are. + // These are tested against the expected attenuations at that + // distance. + for (let k = 0; k < renderedData.length; ++k) { + if (renderedData[k] != 0) { + // Convert from string to index. + let distanceFunction = distanceModelFunction[model]; + let expected = + distanceFunction(panner[impulseCount], 0, 0, position[impulseCount]); + + // Adjust for the center-panning of the EQUALPOWER panning + // model that we're using. + expected *= equalPowerGain(); + + let error = Math.abs(renderedData[k] - expected) / Math.abs(expected); + + maxError = Math.max(maxError, Math.abs(error)); + + should(renderedData[k]).beCloseTo(expected, {threshold: maxAllowedError}); + + // Keep track of any impulses that aren't where we expect them + // to be. + let expectedOffset = timeToSampleFrame(time[impulseCount], sampleRate); + if (k != expectedOffset) { + impulsePositionErrors.push({actual: k, expected: expectedOffset}); + } + ++impulseCount; + } + } + should(impulseCount, 'Number of impulses').beEqualTo(nodesToCreate); + + should(maxError, 'Max error in distance gains') + .beLessThanOrEqualTo(maxAllowedError); + + // Display any timing errors that we found. + if (impulsePositionErrors.length > 0) { + let actual = impulsePositionErrors.map(x => x.actual); + let expected = impulsePositionErrors.map(x => x.expected); + should(actual, 'Actual impulse positions found').beEqualToArray(expected); + } +} |