| // Use a power of two to eliminate round-off when converting frames to time and |
| // vice versa. |
| let sampleRate = 32768; |
| |
| let numberOfChannels = 1; |
| |
| // Time step when each panner node starts. Make sure it starts on a frame |
| // boundary. |
| let timeStep = Math.floor(0.001 * sampleRate) / sampleRate; |
| |
| // Length of the impulse signal. |
| let pulseLengthFrames = Math.round(timeStep * sampleRate); |
| |
| // How many panner nodes to create for the test |
| let nodesToCreate = 100; |
| |
| // Be sure we render long enough for all of our nodes. |
| let renderLengthSeconds = timeStep * (nodesToCreate + 1); |
| |
| // These are global mostly for debugging. |
| let context; |
| let impulse; |
| let bufferSource; |
| let panner; |
| let position; |
| let time; |
| |
| let renderedBuffer; |
| let renderedLeft; |
| let renderedRight; |
| |
| function createGraph(context, nodeCount, positionSetter) { |
| 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. |
| let angleStep = Math.PI / (nodeCount - 1); |
| |
| if (numberOfChannels == 2) { |
| impulse = createStereoImpulseBuffer(context, pulseLengthFrames); |
| } else |
| impulse = createImpulseBuffer(context, pulseLengthFrames); |
| |
| 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 = 'linear'; |
| |
| let angle = angleStep * k; |
| position[k] = {angle: angle, x: Math.cos(angle), z: Math.sin(angle)}; |
| positionSetter(panner[k], position[k].x, 0, 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, should, nodeCount, numberOfSourceChannels, positionSetter) { |
| numberOfChannels = numberOfSourceChannels; |
| |
| createGraph(context, nodeCount, positionSetter); |
| |
| return context.startRendering().then(buffer => checkResult(buffer, should)); |
| } |
| |
| // 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) { |
| let azimuth = angleToAzimuth(angle); |
| |
| if (numberOfChannels == 1) { |
| let panPosition = (azimuth + 90) / 180; |
| |
| let gainL = Math.cos(0.5 * Math.PI * panPosition); |
| let gainR = Math.sin(0.5 * Math.PI * panPosition); |
| |
| return {left: gainL, right: gainR}; |
| } else { |
| if (azimuth <= 0) { |
| let panPosition = (azimuth + 90) / 90; |
| |
| let gainL = 1 + Math.cos(0.5 * Math.PI * panPosition); |
| let gainR = Math.sin(0.5 * Math.PI * panPosition); |
| |
| return {left: gainL, right: gainR}; |
| } else { |
| let panPosition = azimuth / 90; |
| |
| let gainL = Math.cos(0.5 * Math.PI * panPosition); |
| let gainR = 1 + Math.sin(0.5 * Math.PI * panPosition); |
| |
| return {left: gainL, right: gainR}; |
| } |
| } |
| } |
| |
| function checkResult(renderedBuffer, should) { |
| 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. |
| let maxAllowedError = 1.1597e-6; |
| |
| let success = true; |
| |
| // Number of impulses found in the rendered result. |
| let impulseCount = 0; |
| |
| // Max (relative) error and the index of the maxima for the left |
| // and right channels. |
| let maxErrorL = 0; |
| let maxErrorIndexL = 0; |
| let maxErrorR = 0; |
| let maxErrorIndexR = 0; |
| |
| // Number of impulses that don't match our expected locations. |
| let timeCount = 0; |
| |
| // Locations of where the impulses aren't at the expected locations. |
| let timeErrors = new Array(); |
| |
| for (let 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. |
| let pannerGain = equalPowerGain(position[impulseCount].angle); |
| let expectedL = pannerGain.left; |
| let expectedR = pannerGain.right; |
| |
| // Absolute error in the gain. |
| let errorL = Math.abs(renderedLeft[k] - expectedL); |
| let 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. |
| let expectedOffset = timeToSampleFrame(time[impulseCount], sampleRate); |
| if (k != expectedOffset) { |
| timeErrors[timeCount] = {actual: k, expected: expectedOffset}; |
| ++timeCount; |
| } |
| ++impulseCount; |
| } |
| } |
| |
| should(impulseCount, 'Number of impulses found').beEqualTo(nodesToCreate); |
| |
| should( |
| timeErrors.map(x => x.actual), |
| 'Offsets of impulses at the wrong position') |
| .beEqualToArray(timeErrors.map(x => x.expected)); |
| |
| should(maxErrorL, 'Error in left channel gain values') |
| .beLessThanOrEqualTo(maxAllowedError); |
| |
| should(maxErrorR, 'Error in right channel gain values') |
| .beLessThanOrEqualTo(maxAllowedError); |
| } |