distance-model-testing.js (6728B)
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 // How many panner nodes to create for the test. 6 let nodesToCreate = 100; 7 8 // Time step when each panner node starts. Make sure it starts on a frame 9 // boundary. 10 let timeStep = Math.floor(0.001 * sampleRate) / sampleRate; 11 12 // Make sure we render long enough to get all of our nodes. 13 let renderLengthSeconds = timeStep * (nodesToCreate + 1); 14 15 // Length of an impulse signal. 16 let pulseLengthFrames = Math.round(timeStep * sampleRate); 17 18 // Globals to make debugging a little easier. 19 let context; 20 let impulse; 21 let bufferSource; 22 let panner; 23 let position; 24 let time; 25 26 // For the record, these distance formulas were taken from the OpenAL 27 // spec 28 // (http://connect.creativelabs.com/openal/Documentation/OpenAL%201.1%20Specification.pdf), 29 // not the code. The Web Audio spec follows the OpenAL formulas. 30 31 function linearDistance(panner, x, y, z) { 32 let distance = Math.sqrt(x * x + y * y + z * z); 33 distance = Math.min(distance, panner.maxDistance); 34 let rolloff = panner.rolloffFactor; 35 let gain = 36 (1 - 37 rolloff * (distance - panner.refDistance) / 38 (panner.maxDistance - panner.refDistance)); 39 40 return gain; 41 } 42 43 function inverseDistance(panner, x, y, z) { 44 let distance = Math.sqrt(x * x + y * y + z * z); 45 distance = Math.min(distance, panner.maxDistance); 46 let rolloff = panner.rolloffFactor; 47 let gain = panner.refDistance / 48 (panner.refDistance + rolloff * (distance - panner.refDistance)); 49 50 return gain; 51 } 52 53 function exponentialDistance(panner, x, y, z) { 54 let distance = Math.sqrt(x * x + y * y + z * z); 55 distance = Math.min(distance, panner.maxDistance); 56 let rolloff = panner.rolloffFactor; 57 let gain = Math.pow(distance / panner.refDistance, -rolloff); 58 59 return gain; 60 } 61 62 // Map the distance model to the function that implements the model 63 let distanceModelFunction = { 64 'linear': linearDistance, 65 'inverse': inverseDistance, 66 'exponential': exponentialDistance 67 }; 68 69 function createGraph(context, distanceModel, nodeCount) { 70 bufferSource = new Array(nodeCount); 71 panner = new Array(nodeCount); 72 position = new Array(nodeCount); 73 time = new Array(nodesToCreate); 74 75 impulse = createImpulseBuffer(context, pulseLengthFrames); 76 77 // Create all the sources and panners. 78 // 79 // We MUST use the EQUALPOWER panning model so that we can easily 80 // figure out the gain introduced by the panner. 81 // 82 // We want to stay in the middle of the panning range, which means 83 // we want to stay on the z-axis. If we don't, then the effect of 84 // panning model will be much more complicated. We're not testing 85 // the panner, but the distance model, so we want the panner effect 86 // to be simple. 87 // 88 // The panners are placed at a uniform intervals between the panner 89 // reference distance and the panner max distance. The source is 90 // also started at regular intervals. 91 for (let k = 0; k < nodeCount; ++k) { 92 bufferSource[k] = context.createBufferSource(); 93 bufferSource[k].buffer = impulse; 94 95 panner[k] = context.createPanner(); 96 panner[k].panningModel = 'equalpower'; 97 panner[k].distanceModel = distanceModel; 98 99 let distanceStep = 100 (panner[k].maxDistance - panner[k].refDistance) / nodeCount; 101 position[k] = distanceStep * k + panner[k].refDistance; 102 panner[k].setPosition(0, 0, position[k]); 103 104 bufferSource[k].connect(panner[k]); 105 panner[k].connect(context.destination); 106 107 time[k] = k * timeStep; 108 bufferSource[k].start(time[k]); 109 } 110 } 111 112 // distanceModel should be the distance model string like 113 // "linear", "inverse", or "exponential". 114 function createTestAndRun(context, distanceModel, should) { 115 // To test the distance models, we create a number of panners at 116 // uniformly spaced intervals on the z-axis. Each of these are 117 // started at equally spaced time intervals. After rendering the 118 // signals, we examine where each impulse is located and the 119 // attenuation of the impulse. The attenuation is compared 120 // against our expected attenuation. 121 122 createGraph(context, distanceModel, nodesToCreate); 123 124 return context.startRendering().then( 125 buffer => checkDistanceResult(buffer, distanceModel, should)); 126 } 127 128 // The gain caused by the EQUALPOWER panning model, if we stay on the 129 // z axis, with the default orientations. 130 function equalPowerGain() { 131 return Math.SQRT1_2; 132 } 133 134 function checkDistanceResult(renderedBuffer, model, should) { 135 renderedData = renderedBuffer.getChannelData(0); 136 137 // The max allowed error between the actual gain and the expected 138 // value. This is determined experimentally. Set to 0 to see 139 // what the actual errors are. 140 let maxAllowedError = 2.2720e-6; 141 142 let success = true; 143 144 // Number of impulses we found in the rendered result. 145 let impulseCount = 0; 146 147 // Maximum relative error in the gain of the impulses. 148 let maxError = 0; 149 150 // Array of locations of the impulses that were not at the 151 // expected location. (Contains the actual and expected frame 152 // of the impulse.) 153 let impulsePositionErrors = new Array(); 154 155 // Step through the rendered data to find all the non-zero points 156 // so we can find where our distance-attenuated impulses are. 157 // These are tested against the expected attenuations at that 158 // distance. 159 for (let k = 0; k < renderedData.length; ++k) { 160 if (renderedData[k] != 0) { 161 // Convert from string to index. 162 let distanceFunction = distanceModelFunction[model]; 163 let expected = 164 distanceFunction(panner[impulseCount], 0, 0, position[impulseCount]); 165 166 // Adjust for the center-panning of the EQUALPOWER panning 167 // model that we're using. 168 expected *= equalPowerGain(); 169 170 let error = Math.abs(renderedData[k] - expected) / Math.abs(expected); 171 172 maxError = Math.max(maxError, Math.abs(error)); 173 174 should(renderedData[k]).beCloseTo(expected, {threshold: maxAllowedError}); 175 176 // Keep track of any impulses that aren't where we expect them 177 // to be. 178 let expectedOffset = timeToSampleFrame(time[impulseCount], sampleRate); 179 if (k != expectedOffset) { 180 impulsePositionErrors.push({actual: k, expected: expectedOffset}); 181 } 182 ++impulseCount; 183 } 184 } 185 should(impulseCount, 'Number of impulses').beEqualTo(nodesToCreate); 186 187 should(maxError, 'Max error in distance gains') 188 .beLessThanOrEqualTo(maxAllowedError); 189 190 // Display any timing errors that we found. 191 if (impulsePositionErrors.length > 0) { 192 let actual = impulsePositionErrors.map(x => x.actual); 193 let expected = impulsePositionErrors.map(x => x.expected); 194 should(actual, 'Actual impulse positions found').beEqualToArray(expected); 195 } 196 }