| // Use a power of two to eliminate any round-off when converting frames to time. |
| let sampleRate = 32768; |
| |
| // How many panner nodes to create for the test. |
| var 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. |
| var renderLengthSeconds = timeStep * (nodesToCreate + 1); |
| |
| // Length of an impulse signal. |
| var pulseLengthFrames = Math.round(timeStep * sampleRate); |
| |
| // Globals to make debugging a little easier. |
| var context; |
| var impulse; |
| var bufferSource; |
| var panner; |
| var position; |
| var 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) { |
| var distance = Math.sqrt(x * x + y * y + z * z); |
| distance = Math.min(distance, panner.maxDistance); |
| var rolloff = panner.rolloffFactor; |
| var gain = (1 - rolloff * (distance - panner.refDistance) / (panner.maxDistance - panner.refDistance)); |
| |
| return gain; |
| } |
| |
| function inverseDistance(panner, x, y, z) { |
| var distance = Math.sqrt(x * x + y * y + z * z); |
| distance = Math.min(distance, panner.maxDistance); |
| var rolloff = panner.rolloffFactor; |
| var gain = panner.refDistance / (panner.refDistance + rolloff * (distance - panner.refDistance)); |
| |
| return gain; |
| } |
| |
| function exponentialDistance(panner, x, y, z) { |
| var distance = Math.sqrt(x * x + y * y + z * z); |
| distance = Math.min(distance, panner.maxDistance); |
| var rolloff = panner.rolloffFactor; |
| var gain = Math.pow(distance / panner.refDistance, -rolloff); |
| |
| return gain; |
| } |
| |
| // This array must be arranged in the numeric order of the distance |
| // model values. |
| var distanceModelFunction = [linearDistance, inverseDistance, exponentialDistance]; |
| var distanceModelIndex = {"linear": 0, "inverse": 1, "exponential": 2}; |
| |
| 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 (var 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; |
| |
| var 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 constant like |
| // linear, inverse, and exponential. |
| function createTestAndRun(context, distanceModel) { |
| // 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); |
| |
| context.oncomplete = checkDistanceResult(distanceModel); |
| context.startRendering(); |
| } |
| |
| // 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(model) { |
| return function(event) { |
| renderedBuffer = event.renderedBuffer; |
| 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. |
| var maxAllowedError = 2.3e-6; |
| |
| var success = true; |
| |
| // Number of impulses we found in the rendered result. |
| var impulseCount = 0; |
| |
| // Maximum relative error in the gain of the impulses. |
| var maxError = 0; |
| |
| // Array of locations of the impulses that were not at the |
| // expected location. (Contains the actual and expected frame |
| // of the impulse.) |
| var 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 (var k = 0; k < renderedData.length; ++k) { |
| if (renderedData[k] != 0) { |
| // Convert from string to index. |
| var modelIndex = distanceModelIndex[panner[impulseCount].distanceModel]; |
| var distanceFunction = distanceModelFunction[modelIndex]; |
| var expected = distanceFunction(panner[impulseCount], 0, 0, position[impulseCount]); |
| |
| // Adjust for the center-panning of the EQUALPOWER panning |
| // model that we're using. |
| expected *= equalPowerGain(); |
| |
| var error = Math.abs(renderedData[k] - expected) / Math.abs(expected); |
| |
| maxError = Math.max(maxError, Math.abs(error)); |
| |
| // Keep track of any impulses that aren't where we expect them |
| // to be. |
| var expectedOffset = timeToSampleFrame(time[impulseCount], sampleRate); |
| if (k != expectedOffset) { |
| impulsePositionErrors.push({ actual : k, expected : expectedOffset}); |
| } |
| ++impulseCount; |
| } |
| } |
| |
| if (impulseCount == nodesToCreate) { |
| testPassed("Number of impulses found matches number of panner nodes."); |
| } else { |
| testFailed("Number of impulses is incorrect. Found " + impulseCount + " but expected " + nodesToCreate + "."); |
| success = false; |
| } |
| |
| if (maxError <= maxAllowedError) { |
| testPassed("Distance gains are correct."); |
| } else { |
| testFailed("Distance gains are incorrect. Max rel error = " + maxError + " (maxAllowedError = " + maxAllowedError + ")"); |
| success = false; |
| } |
| |
| // Display any timing errors that we found. |
| if (impulsePositionErrors.length > 0) { |
| success = false; |
| testFailed(impulsePositionErrors.length + " timing errors found"); |
| for (var k = 0; k < impulsePositionErrors.length; ++k) { |
| testFailed("Sample at frame " + impulsePositionErrors[k].actual + " but expected " + impulsePositionErrors[k].expected); |
| } |
| } |
| |
| if (success) { |
| testPassed("Distance test passed for distance model " + model); |
| } else { |
| testFailed("Distance test failed for distance model " + model); |
| } |
| |
| finishJSTest(); |
| } |
| } |