1var sampleRate = 44100.0;
2
3var renderLengthSeconds = 8;
4var pulseLengthSeconds = 1;
5var pulseLengthFrames = pulseLengthSeconds * sampleRate;
6
7// How many panner nodes to create for the test
8var nodesToCreate = 100;
9
10// Time step when each panner node starts.
11var timeStep = 0.05;
12
13// Globals to make debugging a little easier.
14var context;
15var impulse;
16var bufferSource;
17var panner;
18var position;
19var time;
20
21function createImpulseBuffer(context, sampleFrameLength) {
22 var audioBuffer = context.createBuffer(1, sampleFrameLength, context.sampleRate);
23 var n = audioBuffer.length;
24 var data = audioBuffer.getChannelData(0);
25
26 for (var k = 0; k < n; ++k) {
27 data[k] = 0;
28 }
29 data[0] = 1;
30
31 return audioBuffer;
32}
33
34// For the record, these distance formulas were taken from the OpenAL
35// spec, not the code.
36function linearDistance(panner, x, y, z) {
37 var distance = Math.sqrt(x * x + y * y + z * z);
38 distance = Math.min(distance, panner.maxDistance);
39 var rolloff = panner.rolloffFactor;
40 var gain = (1 - rolloff * (distance - panner.refDistance) / (panner.maxDistance - panner.refDistance));
41
42 return gain;
43}
44
45function exponentialDistance(panner, x, y, z) {
46 var distance = Math.sqrt(x * x + y * y + z * z);
47 distance = Math.min(distance, panner.maxDistance);
48 var rolloff = panner.rolloffFactor;
49 var gain = Math.pow(distance / panner.refDistance, -rolloff);
50
51 return gain;
52}
53
54function inverseDistance(panner, x, y, z) {
55 var distance = Math.sqrt(x * x + y * y + z * z);
56 distance = Math.min(distance, panner.maxDistance);
57 var rolloff = panner.rolloffFactor;
58 var gain = panner.refDistance / (panner.refDistance + rolloff * (distance - panner.refDistance));
59
60 return gain;
61}
62
63// This array must be arranged in the numeric order of the distance
64// model values.
65var distanceModelFunction = [linearDistance, inverseDistance, exponentialDistance];
66
67function createGraph(context, distanceModel, nodeCount) {
68 // Create all the sources, which are impulses.
69 bufferSource = new Array(nodeCount);
70 for (var k = 0; k < nodeCount; ++k) {
71 bufferSource[k] = context.createBufferSource();
72 impulse = createImpulseBuffer(context, pulseLengthFrames);
73 bufferSource[k].buffer = impulse;
74 }
75
76 // Create all the panners.
77 //
78 // We MUST use the EQUALPOWER panning model so that we can easily
79 // figure out the gain introduced by the panner.
80 //
81 // We want to stay in the middle of the panning range, which means
82 // we want to stay on the z-axis. If we don't, then the effect of
83 // panning model will be much more complicated. We're not testing
84 // the panner, but the distance model, so we want the panner effect
85 // to be simple.
86 panner = new Array(nodeCount);
87 position = new Array(nodeCount);
88 for (var k = 0; k < nodeCount; ++k) {
89 panner[k] = context.createPanner();
90 panner[k].panningModel = panner.EQUALPOWER;
91 panner[k].distanceModel = distanceModel;
92 // The distance is uniform spacing between the max and ref
93 // distance.
94 var distanceStep = (panner[k].maxDistance - panner[k].refDistance) / nodeCount;
95 position[k] = distanceStep * k + panner[k].refDistance;
96 panner[k].setPosition(0, 0, position[k]);
97 }
98
99 // Connect the sources and panners.
100 for (var k = 0; k < nodeCount; ++k) {
101 bufferSource[k].connect(panner[k]);
102 panner[k].connect(context.destination);
103 }
104}
105
106function startSources() {
107 // Turn on each source at uniform intervals.
108 time = new Array(nodesToCreate);
109 for (var k = 0; k < nodesToCreate; ++k) {
110 time[k] = k * timeStep;
111 bufferSource[k].noteOn(time[k]);
112 }
113}
114
115// distanceModel should be the distance model constant like
116// LINEAR_DISTANCE, INVERSE_DISTANCE, and EXPONENTIAL_DISTANCE. The
117// expectedModel is the expected actual numeric value of the constant.
118function createTestAndRun(context, distanceModel, expectedModel, threshold) {
119 createGraph(context, distanceModel, nodesToCreate);
120 startSources();
121
122 context.oncomplete = checkDistanceResult(distanceModel, expectedModel, threshold, false);
123 context.startRendering();
124}
125
126// The gain caused by the EQUALPOWER panning model, if we stay on the
127// z axis, with the default orientations.
128function equalPowerGain() {
129 return Math.SQRT1_2;
130}
131
132function checkDistanceResult(model, expectedModel, threshold, debug) {
133 return function(event) {
134 renderedBuffer = event.renderedBuffer;
135 renderedData = renderedBuffer.getChannelData(0);
136
137 var success = true;
138
139 if (model == expectedModel) {
140 testPassed("Distance model value matched expected value.");
141 } else {
142 testPassed("Distance model value matched expected value.");
143 success = false;
144 }
145
146 // Number of impulses we found in the rendered result.
147 var impulseCount = 0;
148 // Maximum relative error in the gain of the impulses.
149 var maxError = 0;
150 // Number of impulses that were not at the expected location.
151 // (For debugging.)
152 var timeErrorCount = 0;
153 // Array of locations of the impulses that were not at the
154 // expected location. (For debugging.)
155 var timeErrors = new Array();
156
157 // Step through the rendered data to find all the non-zero points
158 // so we can find where our impulses are.
159 for (var k = 0; k < renderedData.length; ++k) {
160 if (renderedData[k] != 0) {
161 var distanceFunction = distanceModelFunction[panner[impulseCount].distanceModel];
162 var expected = distanceFunction(panner[impulseCount], 0, 0, position[impulseCount]);
163 expected *= equalPowerGain();
164
165 var error = Math.abs(renderedData[k] - expected)/Math.abs(expected);
166 if (debug) {
167 console.log(k + ": " + renderedData[k] + " (expected " + expected + ", rel = ", error + ")");
168 }
169 maxError = Math.max(maxError, Math.abs(error));
170 if (k != Math.round(sampleRate * time[impulseCount])) {
171 timeErrors[timeErrorCount] = { actual : k, expected : Math.round(sampleRate * time[impulseCount])};
172 ++timeErrorCount;
173 }
174 ++impulseCount;
175 }
176 }
177
178 if (impulseCount == nodesToCreate) {
179 testPassed("Number of nodes is correct.");
180 } else {
181 testFailed("Number of nodes is incorrect. (Found " + impulseCount + " but expected " + nodesToCreate + ")");
182 success = false;
183 }
184
185 if (maxError <= threshold) {
186 testPassed("Distance gains are correct.");
187 } else {
188 testFailed("Distance gains are incorrect. Max rel error = " + maxError + " (threshold = " + threshold + ")");
189 success = false;
190 }
191
192 // For debugging. It seems some impulses don't show up at exactly
193 // the expected rendered location. Log these to the console if
194 // debugging is true.
195 //
196 // FIXME: File a bug about this.
197 if (debug && timeErrors.length > 0) {
198 console.log(timeErrors.length + " timing errors found");
199 for (var k = 0; k < timeErrors.length; ++k) {
200 console.log("Sample " + timeErrors[k].actual + " but expected " + timeErrors[k].expected);
201 }
202 }
203
204 if (success) {
205 testPassed("Distance test passed for distance model " + model);
206 } else {
207 testFailed("Distance test failed for distance model " + model);
208 }
209
210 finishJSTest();
211 }
212}