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