blob: 2f08aa1f589f4c81075d4ea082a19dcebb946169 [file] [log] [blame]
// 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();
}
}