panner-model-testing.js (7515B)
1 // Use a power of two to eliminate round-off when converting frames to time and 2 // vice versa. 3 let sampleRate = 32768; 4 5 let numberOfChannels = 1; 6 7 // Time step when each panner node starts. Make sure it starts on a frame 8 // boundary. 9 let timeStep = Math.floor(0.001 * sampleRate) / sampleRate; 10 11 // Length of the impulse signal. 12 let pulseLengthFrames = Math.round(timeStep * sampleRate); 13 14 // How many panner nodes to create for the test 15 let nodesToCreate = 100; 16 17 // Be sure we render long enough for all of our nodes. 18 let renderLengthSeconds = timeStep * (nodesToCreate + 1); 19 20 // These are global mostly for debugging. 21 let context; 22 let impulse; 23 let bufferSource; 24 let panner; 25 let position; 26 let time; 27 28 let renderedBuffer; 29 let renderedLeft; 30 let renderedRight; 31 32 function createGraph(context, nodeCount, positionSetter) { 33 bufferSource = new Array(nodeCount); 34 panner = new Array(nodeCount); 35 position = new Array(nodeCount); 36 time = new Array(nodeCount); 37 // Angle between panner locations. (nodeCount - 1 because we want 38 // to include both 0 and 180 deg. 39 let angleStep = Math.PI / (nodeCount - 1); 40 41 if (numberOfChannels == 2) { 42 impulse = createStereoImpulseBuffer(context, pulseLengthFrames); 43 } else 44 impulse = createImpulseBuffer(context, pulseLengthFrames); 45 46 for (let k = 0; k < nodeCount; ++k) { 47 bufferSource[k] = context.createBufferSource(); 48 bufferSource[k].buffer = impulse; 49 50 panner[k] = context.createPanner(); 51 panner[k].panningModel = 'equalpower'; 52 panner[k].distanceModel = 'linear'; 53 54 let angle = angleStep * k; 55 position[k] = {angle: angle, x: Math.cos(angle), z: Math.sin(angle)}; 56 positionSetter(panner[k], position[k].x, 0, position[k].z); 57 58 bufferSource[k].connect(panner[k]); 59 panner[k].connect(context.destination); 60 61 // Start the source 62 time[k] = k * timeStep; 63 bufferSource[k].start(time[k]); 64 } 65 } 66 67 function createTestAndRun( 68 context, should, nodeCount, numberOfSourceChannels, positionSetter) { 69 numberOfChannels = numberOfSourceChannels; 70 71 createGraph(context, nodeCount, positionSetter); 72 73 return context.startRendering().then(buffer => checkResult(buffer, should)); 74 } 75 76 async function createTestAndRun_W3CTH( 77 context, nodeCount, numberOfSourceChannels, positionSetter) { 78 numberOfChannels = numberOfSourceChannels; 79 createGraph(context, nodeCount, positionSetter); 80 const renderedBuffer = await context.startRendering(); 81 return checkResult_W3CTH(renderedBuffer); 82 } 83 84 // Map our position angle to the azimuth angle (in degrees). 85 // 86 // An angle of 0 corresponds to an azimuth of 90 deg; pi, to -90 deg. 87 function angleToAzimuth(angle) { 88 return 90 - angle * 180 / Math.PI; 89 } 90 91 // The gain caused by the EQUALPOWER panning model 92 function equalPowerGain(angle) { 93 let azimuth = angleToAzimuth(angle); 94 95 if (numberOfChannels == 1) { 96 let panPosition = (azimuth + 90) / 180; 97 98 let gainL = Math.cos(0.5 * Math.PI * panPosition); 99 let gainR = Math.sin(0.5 * Math.PI * panPosition); 100 101 return {left: gainL, right: gainR}; 102 } else { 103 if (azimuth <= 0) { 104 let panPosition = (azimuth + 90) / 90; 105 106 let gainL = 1 + Math.cos(0.5 * Math.PI * panPosition); 107 let gainR = Math.sin(0.5 * Math.PI * panPosition); 108 109 return {left: gainL, right: gainR}; 110 } else { 111 let panPosition = azimuth / 90; 112 113 let gainL = Math.cos(0.5 * Math.PI * panPosition); 114 let gainR = 1 + Math.sin(0.5 * Math.PI * panPosition); 115 116 return {left: gainL, right: gainR}; 117 } 118 } 119 } 120 121 function checkResult(renderedBuffer, should) { 122 renderedLeft = renderedBuffer.getChannelData(0); 123 renderedRight = renderedBuffer.getChannelData(1); 124 125 // The max error we allow between the rendered impulse and the 126 // expected value. This value is experimentally determined. Set 127 // to 0 to make the test fail to see what the actual error is. 128 let maxAllowedError = 1.1597e-6; 129 130 let success = true; 131 132 // Number of impulses found in the rendered result. 133 let impulseCount = 0; 134 135 // Max (relative) error and the index of the maxima for the left 136 // and right channels. 137 let maxErrorL = 0; 138 let maxErrorIndexL = 0; 139 let maxErrorR = 0; 140 let maxErrorIndexR = 0; 141 142 // Number of impulses that don't match our expected locations. 143 let timeCount = 0; 144 145 // Locations of where the impulses aren't at the expected locations. 146 let timeErrors = new Array(); 147 148 for (let k = 0; k < renderedLeft.length; ++k) { 149 // We assume that the left and right channels start at the same instant. 150 if (renderedLeft[k] != 0 || renderedRight[k] != 0) { 151 // The expected gain for the left and right channels. 152 let pannerGain = equalPowerGain(position[impulseCount].angle); 153 let expectedL = pannerGain.left; 154 let expectedR = pannerGain.right; 155 156 // Absolute error in the gain. 157 let errorL = Math.abs(renderedLeft[k] - expectedL); 158 let errorR = Math.abs(renderedRight[k] - expectedR); 159 160 if (Math.abs(errorL) > maxErrorL) { 161 maxErrorL = Math.abs(errorL); 162 maxErrorIndexL = impulseCount; 163 } 164 if (Math.abs(errorR) > maxErrorR) { 165 maxErrorR = Math.abs(errorR); 166 maxErrorIndexR = impulseCount; 167 } 168 169 // Keep track of the impulses that didn't show up where we 170 // expected them to be. 171 let expectedOffset = timeToSampleFrame(time[impulseCount], sampleRate); 172 if (k != expectedOffset) { 173 timeErrors[timeCount] = {actual: k, expected: expectedOffset}; 174 ++timeCount; 175 } 176 ++impulseCount; 177 } 178 } 179 180 should(impulseCount, 'Number of impulses found').beEqualTo(nodesToCreate); 181 182 should( 183 timeErrors.map(x => x.actual), 184 'Offsets of impulses at the wrong position') 185 .beEqualToArray(timeErrors.map(x => x.expected)); 186 187 should(maxErrorL, 'Error in left channel gain values') 188 .beLessThanOrEqualTo(maxAllowedError); 189 190 should(maxErrorR, 'Error in right channel gain values') 191 .beLessThanOrEqualTo(maxAllowedError); 192 } 193 194 function checkResult_W3CTH(renderedBuffer) { 195 // The max error we allow between the rendered impulse and the 196 // expected value. This value is experimentally determined. Set 197 // to 0 to make the test fail to see what the actual error is. 198 renderedLeft = renderedBuffer.getChannelData(0); 199 renderedRight = renderedBuffer.getChannelData(1); 200 201 const maxAllowedError = 1.1597e-6; 202 let impulseCount = 0; 203 let maxErrorL = 0; 204 let maxErrorR = 0; 205 const timeErrors = []; 206 207 for (let k = 0; k < renderedLeft.length; ++k) { 208 if (renderedLeft[k] !== 0 || renderedRight[k] !== 0) { 209 const {left: expectedL, right: expectedR} = 210 equalPowerGain(position[impulseCount].angle); 211 212 maxErrorL = Math.max(maxErrorL, Math.abs(renderedLeft[k] - expectedL)); 213 maxErrorR = Math.max(maxErrorR, Math.abs(renderedRight[k] - expectedR)); 214 215 const expectedOffset = 216 timeToSampleFrame(time[impulseCount], sampleRate); 217 if (k !== expectedOffset) { 218 timeErrors.push({actual: k, expected: expectedOffset}); 219 } 220 221 ++impulseCount; 222 } 223 } 224 225 assert_equals( 226 impulseCount, 227 nodesToCreate, 228 'Number of impulses found'); 229 230 assert_array_equals( 231 timeErrors.map(e => e.actual), 232 timeErrors.map(e => e.expected), 233 'Offsets of impulses at the wrong position'); 234 235 assert_less_than_equal(maxErrorL, maxAllowedError, 236 `Left-channel gain error ${maxErrorL} > ${maxAllowedError}`); 237 assert_less_than_equal(maxErrorR, maxAllowedError, 238 `Right-channel gain error ${maxErrorR} > ${maxAllowedError}`); 239 }