let StereoPannerTest = (function() { // Constants let PI_OVER_TWO = Math.PI * 0.5; // Use a power of two to eliminate any round-off when converting frames to // time. let gSampleRate = 32768; // Time step when each panner node starts. Make sure this is on a frame boundary. let gTimeStep = Math.floor(0.001 * gSampleRate) / gSampleRate; // How many panner nodes to create for the test let gNodesToCreate = 100; // Total render length for all of our nodes. let gRenderLength = gTimeStep * (gNodesToCreate + 1) + gSampleRate; // Calculates channel gains based on equal power panning model. // See: http://webaudio.github.io/web-audio-api/#panning-algorithm function getChannelGain(pan, numberOfChannels) { // The internal panning clips the pan value between -1, 1. pan = Math.min(Math.max(pan, -1), 1); let gainL, gainR; // Consider number of channels and pan value's polarity. if (numberOfChannels == 1) { let panRadian = (pan * 0.5 + 0.5) * PI_OVER_TWO; gainL = Math.cos(panRadian); gainR = Math.sin(panRadian); } else { let panRadian = (pan <= 0 ? pan + 1 : pan) * PI_OVER_TWO; if (pan <= 0) { gainL = 1 + Math.cos(panRadian); gainR = Math.sin(panRadian); } else { gainL = Math.cos(panRadian); gainR = 1 + Math.sin(panRadian); } } return {gainL: gainL, gainR: gainR}; } /** * Test implementation class. * @param {Object} options Test options * @param {Object} options.description Test description * @param {Object} options.numberOfInputChannels Number of input channels */ function Test(should, options) { // Primary test flag. this.success = true; this.should = should; this.context = null; this.prefix = options.prefix; this.numberOfInputChannels = (options.numberOfInputChannels || 1); switch (this.numberOfInputChannels) { case 1: this.description = 'Test for mono input'; break; case 2: this.description = 'Test for stereo input'; break; } // Onset time position of each impulse. this.onsets = []; // Pan position value of each impulse. this.panPositions = []; // Locations of where the impulses aren't at the expected locations. this.errors = []; // The index of the current impulse being verified. this.impulseIndex = 0; // 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. this.maxAllowedError = 1.284318e-7; // Max (absolute) error and the index of the maxima for the left // and right channels. this.maxErrorL = 0; this.maxErrorR = 0; this.maxErrorIndexL = 0; this.maxErrorIndexR = 0; // The maximum value to use for panner pan value. The value will range from // -panLimit to +panLimit. this.panLimit = 1.0625; } Test.prototype.init = function() { this.context = new OfflineAudioContext(2, gRenderLength, gSampleRate); }; // Prepare an audio graph for testing. Create multiple impulse generators and // panner nodes, then play them sequentially while varying the pan position. Test.prototype.prepare = function() { let impulse; let impulseLength = Math.round(gTimeStep * gSampleRate); let sources = []; let panners = []; // Moves the pan value for each panner by pan step unit from -2 to 2. // This is to check if the internal panning value is clipped properly. let panStep = (2 * this.panLimit) / (gNodesToCreate - 1); if (this.numberOfInputChannels === 1) { impulse = createImpulseBuffer(this.context, impulseLength); } else { impulse = createStereoImpulseBuffer(this.context, impulseLength); } for (let i = 0; i < gNodesToCreate; i++) { sources[i] = this.context.createBufferSource(); panners[i] = this.context.createStereoPanner(); sources[i].connect(panners[i]); panners[i].connect(this.context.destination); sources[i].buffer = impulse; panners[i].pan.value = this.panPositions[i] = panStep * i - this.panLimit; // Store the onset time position of impulse. this.onsets[i] = gTimeStep * i; sources[i].start(this.onsets[i]); } }; Test.prototype.verify = function() { let chanL = this.renderedBufferL; let chanR = this.renderedBufferR; for (let i = 0; i < chanL.length; i++) { // Left and right channels must start at the same instant. if (chanL[i] !== 0 || chanR[i] !== 0) { // Get amount of error between actual and expected gain. let expected = getChannelGain( this.panPositions[this.impulseIndex], this.numberOfInputChannels); let errorL = Math.abs(chanL[i] - expected.gainL); let errorR = Math.abs(chanR[i] - expected.gainR); if (errorL > this.maxErrorL) { this.maxErrorL = errorL; this.maxErrorIndexL = this.impulseIndex; } if (errorR > this.maxErrorR) { this.maxErrorR = errorR; this.maxErrorIndexR = this.impulseIndex; } // Keep track of the impulses that didn't show up where we expected // them to be. let expectedOffset = timeToSampleFrame(this.onsets[this.impulseIndex], gSampleRate); if (i != expectedOffset) { this.errors.push({actual: i, expected: expectedOffset}); } this.impulseIndex++; } } }; Test.prototype.showResult = function() { this.should(this.impulseIndex, this.prefix + 'Number of impulses found') .beEqualTo(gNodesToCreate); this.should( this.errors.length, this.prefix + 'Number of impulse at the wrong offset') .beEqualTo(0); this.should(this.maxErrorL, this.prefix + 'Left channel error magnitude') .beLessThanOrEqualTo(this.maxAllowedError); this.should(this.maxErrorR, this.prefix + 'Right channel error magnitude') .beLessThanOrEqualTo(this.maxAllowedError); }; Test.prototype.run = function() { this.init(); this.prepare(); return this.context.startRendering().then(renderedBuffer => { this.renderedBufferL = renderedBuffer.getChannelData(0); this.renderedBufferR = renderedBuffer.getChannelData(1); this.verify(); this.showResult(); }); }; return { create: function(should, options) { return new Test(should, options); } }; })();