var sampleRate = 48000.0; var numberOfChannels = 1; // Time step when each panner node starts. var timeStep = 0.001; // Length of the impulse signal. var pulseLengthFrames = Math.round(timeStep * sampleRate); // How many panner nodes to create for the test var nodesToCreate = 100; // Be sure we render long enough for all of our nodes. var renderLengthSeconds = timeStep * (nodesToCreate + 1); // These are global mostly for debugging. var context; var impulse; var bufferSource; var panner; var position; var time; var renderedBuffer; var renderedLeft; var renderedRight; function createGraph(context, nodeCount) { bufferSource = new Array(nodeCount); panner = new Array(nodeCount); position = new Array(nodeCount); time = new Array(nodeCount); // Angle between panner locations. (nodeCount - 1 because we want // to include both 0 and 180 deg. var angleStep = Math.PI / (nodeCount - 1); if (numberOfChannels == 2) { impulse = createStereoImpulseBuffer(context, pulseLengthFrames); } else impulse = createImpulseBuffer(context, pulseLengthFrames); 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 = "linear"; var angle = angleStep * k; position[k] = {angle : angle, x : Math.cos(angle), z : Math.sin(angle)}; panner[k].positionX.value = position[k].x; panner[k].positionZ.value = position[k].z; bufferSource[k].connect(panner[k]); panner[k].connect(context.destination); // Start the source time[k] = k * timeStep; bufferSource[k].start(time[k]); } } function createTestAndRun(context, nodeCount, numberOfSourceChannels) { numberOfChannels = numberOfSourceChannels; createGraph(context, nodeCount); context.oncomplete = checkResult; context.startRendering(); } // Map our position angle to the azimuth angle (in degrees). // // An angle of 0 corresponds to an azimuth of 90 deg; pi, to -90 deg. function angleToAzimuth(angle) { return 90 - angle * 180 / Math.PI; } // The gain caused by the EQUALPOWER panning model function equalPowerGain(angle) { var azimuth = angleToAzimuth(angle); if (numberOfChannels == 1) { var panPosition = (azimuth + 90) / 180; var gainL = Math.cos(0.5 * Math.PI * panPosition); var gainR = Math.sin(0.5 * Math.PI * panPosition); return { left : gainL, right : gainR }; } else { if (azimuth <= 0) { var panPosition = (azimuth + 90) / 90; var gainL = 1 + Math.cos(0.5 * Math.PI * panPosition); var gainR = Math.sin(0.5 * Math.PI * panPosition); return { left : gainL, right : gainR }; } else { var panPosition = azimuth / 90; var gainL = Math.cos(0.5 * Math.PI * panPosition); var gainR = 1 + Math.sin(0.5 * Math.PI * panPosition); return { left : gainL, right : gainR }; } } } function checkResult(event) { renderedBuffer = event.renderedBuffer; renderedLeft = renderedBuffer.getChannelData(0); renderedRight = renderedBuffer.getChannelData(1); // The max error we allow between the rendered impulse and the // expected value. This value is experimentally determined. Set // to 0 to make the test fail to see what the actual error is. var maxAllowedError = 1.3e-6; var success = true; // Number of impulses found in the rendered result. var impulseCount = 0; // Max (relative) error and the index of the maxima for the left // and right channels. var maxErrorL = 0; var maxErrorIndexL = 0; var maxErrorR = 0; var maxErrorIndexR = 0; // Number of impulses that don't match our expected locations. var timeCount = 0; // Locations of where the impulses aren't at the expected locations. var timeErrors = new Array(); for (var k = 0; k < renderedLeft.length; ++k) { // We assume that the left and right channels start at the same instant. if (renderedLeft[k] != 0 || renderedRight[k] != 0) { // The expected gain for the left and right channels. var pannerGain = equalPowerGain(position[impulseCount].angle); var expectedL = pannerGain.left; var expectedR = pannerGain.right; // Absolute error in the gain. var errorL = Math.abs(renderedLeft[k] - expectedL); var errorR = Math.abs(renderedRight[k] - expectedR); if (Math.abs(errorL) > maxErrorL) { maxErrorL = Math.abs(errorL); maxErrorIndexL = impulseCount; } if (Math.abs(errorR) > maxErrorR) { maxErrorR = Math.abs(errorR); maxErrorIndexR = impulseCount; } // Keep track of the impulses that didn't show up where we // expected them to be. var expectedOffset = timeToSampleFrame(time[impulseCount], sampleRate); if (k != expectedOffset) { timeErrors[timeCount] = { actual : k, expected : expectedOffset}; ++timeCount; } ++impulseCount; } } if (impulseCount == nodesToCreate) { testPassed("Number of impulses matches the number of panner nodes."); } else { testFailed("Number of impulses is incorrect. (Found " + impulseCount + " but expected " + nodesToCreate + ")"); success = false; } if (timeErrors.length > 0) { success = false; testFailed(timeErrors.length + " timing errors found in " + nodesToCreate + " panner nodes."); for (var k = 0; k < timeErrors.length; ++k) { testFailed("Impulse at sample " + timeErrors[k].actual + " but expected " + timeErrors[k].expected); } } else { testPassed("All impulses at expected offsets."); } if (maxErrorL <= maxAllowedError) { testPassed("Left channel gain values are correct."); } else { testFailed("Left channel gain values are incorrect. Max error = " + maxErrorL + " at time " + time[maxErrorIndexL] + " (threshold = " + maxAllowedError + ")"); success = false; } if (maxErrorR <= maxAllowedError) { testPassed("Right channel gain values are correct."); } else { testFailed("Right channel gain values are incorrect. Max error = " + maxErrorR + " at time " + time[maxErrorIndexR] + " (threshold = " + maxAllowedError + ")"); success = false; } if (success) { testPassed("EqualPower panner test passed"); } else { testFailed("EqualPower panner test failed"); } finishJSTest(); }