stereopanner-testing.js (6569B)
1 let StereoPannerTest = (function() { 2 3 // Constants 4 let PI_OVER_TWO = Math.PI * 0.5; 5 6 // Use a power of two to eliminate any round-off when converting frames to 7 // time. 8 let gSampleRate = 32768; 9 10 // Time step when each panner node starts. Make sure this is on a frame boundary. 11 let gTimeStep = Math.floor(0.001 * gSampleRate) / gSampleRate; 12 13 // How many panner nodes to create for the test 14 let gNodesToCreate = 100; 15 16 // Total render length for all of our nodes. 17 let gRenderLength = gTimeStep * (gNodesToCreate + 1) + gSampleRate; 18 19 // Calculates channel gains based on equal power panning model. 20 // See: http://webaudio.github.io/web-audio-api/#panning-algorithm 21 function getChannelGain(pan, numberOfChannels) { 22 // The internal panning clips the pan value between -1, 1. 23 pan = Math.min(Math.max(pan, -1), 1); 24 let gainL, gainR; 25 // Consider number of channels and pan value's polarity. 26 if (numberOfChannels == 1) { 27 let panRadian = (pan * 0.5 + 0.5) * PI_OVER_TWO; 28 gainL = Math.cos(panRadian); 29 gainR = Math.sin(panRadian); 30 } else { 31 let panRadian = (pan <= 0 ? pan + 1 : pan) * PI_OVER_TWO; 32 if (pan <= 0) { 33 gainL = 1 + Math.cos(panRadian); 34 gainR = Math.sin(panRadian); 35 } else { 36 gainL = Math.cos(panRadian); 37 gainR = 1 + Math.sin(panRadian); 38 } 39 } 40 return {gainL: gainL, gainR: gainR}; 41 } 42 43 44 /** 45 * Test implementation class. 46 * @param {Object} options Test options 47 * @param {Object} options.description Test description 48 * @param {Object} options.numberOfInputChannels Number of input channels 49 */ 50 function Test(should, options) { 51 // Primary test flag. 52 this.success = true; 53 54 this.should = should; 55 this.context = null; 56 this.prefix = options.prefix; 57 this.numberOfInputChannels = (options.numberOfInputChannels || 1); 58 switch (this.numberOfInputChannels) { 59 case 1: 60 this.description = 'Test for mono input'; 61 break; 62 case 2: 63 this.description = 'Test for stereo input'; 64 break; 65 } 66 67 // Onset time position of each impulse. 68 this.onsets = []; 69 70 // Pan position value of each impulse. 71 this.panPositions = []; 72 73 // Locations of where the impulses aren't at the expected locations. 74 this.errors = []; 75 76 // The index of the current impulse being verified. 77 this.impulseIndex = 0; 78 79 // The max error we allow between the rendered impulse and the 80 // expected value. This value is experimentally determined. Set 81 // to 0 to make the test fail to see what the actual error is. 82 this.maxAllowedError = 1.284318e-7; 83 84 // Max (absolute) error and the index of the maxima for the left 85 // and right channels. 86 this.maxErrorL = 0; 87 this.maxErrorR = 0; 88 this.maxErrorIndexL = 0; 89 this.maxErrorIndexR = 0; 90 91 // The maximum value to use for panner pan value. The value will range from 92 // -panLimit to +panLimit. 93 this.panLimit = 1.0625; 94 } 95 96 97 Test.prototype.init = function() { 98 this.context = new OfflineAudioContext(2, gRenderLength, gSampleRate); 99 }; 100 101 // Prepare an audio graph for testing. Create multiple impulse generators and 102 // panner nodes, then play them sequentially while varying the pan position. 103 Test.prototype.prepare = function() { 104 let impulse; 105 let impulseLength = Math.round(gTimeStep * gSampleRate); 106 let sources = []; 107 let panners = []; 108 109 // Moves the pan value for each panner by pan step unit from -2 to 2. 110 // This is to check if the internal panning value is clipped properly. 111 let panStep = (2 * this.panLimit) / (gNodesToCreate - 1); 112 113 if (this.numberOfInputChannels === 1) { 114 impulse = createImpulseBuffer(this.context, impulseLength); 115 } else { 116 impulse = createStereoImpulseBuffer(this.context, impulseLength); 117 } 118 119 for (let i = 0; i < gNodesToCreate; i++) { 120 sources[i] = this.context.createBufferSource(); 121 panners[i] = this.context.createStereoPanner(); 122 sources[i].connect(panners[i]); 123 panners[i].connect(this.context.destination); 124 sources[i].buffer = impulse; 125 panners[i].pan.value = this.panPositions[i] = panStep * i - this.panLimit; 126 127 // Store the onset time position of impulse. 128 this.onsets[i] = gTimeStep * i; 129 130 sources[i].start(this.onsets[i]); 131 } 132 }; 133 134 135 Test.prototype.verify = function() { 136 let chanL = this.renderedBufferL; 137 let chanR = this.renderedBufferR; 138 for (let i = 0; i < chanL.length; i++) { 139 // Left and right channels must start at the same instant. 140 if (chanL[i] !== 0 || chanR[i] !== 0) { 141 // Get amount of error between actual and expected gain. 142 let expected = getChannelGain( 143 this.panPositions[this.impulseIndex], this.numberOfInputChannels); 144 let errorL = Math.abs(chanL[i] - expected.gainL); 145 let errorR = Math.abs(chanR[i] - expected.gainR); 146 147 if (errorL > this.maxErrorL) { 148 this.maxErrorL = errorL; 149 this.maxErrorIndexL = this.impulseIndex; 150 } 151 if (errorR > this.maxErrorR) { 152 this.maxErrorR = errorR; 153 this.maxErrorIndexR = this.impulseIndex; 154 } 155 156 // Keep track of the impulses that didn't show up where we expected 157 // them to be. 158 let expectedOffset = 159 timeToSampleFrame(this.onsets[this.impulseIndex], gSampleRate); 160 if (i != expectedOffset) { 161 this.errors.push({actual: i, expected: expectedOffset}); 162 } 163 164 this.impulseIndex++; 165 } 166 } 167 }; 168 169 170 Test.prototype.showResult = function() { 171 this.should(this.impulseIndex, this.prefix + 'Number of impulses found') 172 .beEqualTo(gNodesToCreate); 173 174 this.should( 175 this.errors.length, 176 this.prefix + 'Number of impulse at the wrong offset') 177 .beEqualTo(0); 178 179 this.should(this.maxErrorL, this.prefix + 'Left channel error magnitude') 180 .beLessThanOrEqualTo(this.maxAllowedError); 181 182 this.should(this.maxErrorR, this.prefix + 'Right channel error magnitude') 183 .beLessThanOrEqualTo(this.maxAllowedError); 184 }; 185 186 Test.prototype.run = function() { 187 188 this.init(); 189 this.prepare(); 190 191 return this.context.startRendering().then(renderedBuffer => { 192 this.renderedBufferL = renderedBuffer.getChannelData(0); 193 this.renderedBufferR = renderedBuffer.getChannelData(1); 194 this.verify(); 195 this.showResult(); 196 }); 197 }; 198 199 return { 200 create: function(should, options) { 201 return new Test(should, options); 202 } 203 }; 204 205 })();