OLD | NEW |
1 <!doctype html> | 1 <!DOCTYPE html> |
2 <html> | 2 <html> |
3 <head> | 3 <head> |
| 4 <title> |
| 5 Test Automation of PannerNode Positions |
| 6 </title> |
4 <script src="../../resources/testharness.js"></script> | 7 <script src="../../resources/testharness.js"></script> |
5 <script src="../../resources/testharnessreport.js"></script> | 8 <script src="../../resources/testharnessreport.js"></script> |
6 <script src="../resources/audit-util.js"></script> | 9 <script src="../resources/audit-util.js"></script> |
7 <script src="../resources/audit.js"></script> | 10 <script src="../resources/audit.js"></script> |
8 <script src="../resources/panner-formulas.js"></script> | 11 <script src="../resources/panner-formulas.js"></script> |
9 <title>Test Automation of PannerNode Positions</title> | |
10 </head> | 12 </head> |
| 13 <body> |
| 14 <script id="layout-test-code"> |
| 15 let sampleRate = 48000; |
| 16 // These tests are quite slow, so don't run for many frames. 256 frames |
| 17 // should be enough to demonstrate that automations are working. |
| 18 let renderFrames = 256; |
| 19 let renderDuration = renderFrames / sampleRate; |
11 | 20 |
12 <body> | 21 let context; |
13 <script> | 22 let panner; |
14 var sampleRate = 48000; | |
15 // These tests are quite slow, so don't run for many frames. 256 frames s
hould be enough to | |
16 // demonstrate that automations are working. | |
17 var renderFrames = 256; | |
18 var renderDuration = renderFrames / sampleRate; | |
19 | 23 |
20 var context; | 24 let audit = Audit.createTaskRunner(); |
21 var panner; | |
22 | 25 |
23 var audit = Audit.createTaskRunner(); | 26 // Set of tests for the panner node with automations applied to the |
| 27 // position of the source. |
| 28 let testConfigs = [ |
| 29 { |
| 30 // Distance model parameters for the panner |
| 31 distanceModel: {model: 'inverse', rolloff: 1}, |
| 32 // Initial location of the source |
| 33 startPosition: [0, 0, 1], |
| 34 // Final position of the source. For this test, we only want to move |
| 35 // on the z axis which |
| 36 // doesn't change the azimuth angle. |
| 37 endPosition: [0, 0, 10000], |
| 38 }, |
| 39 { |
| 40 distanceModel: {model: 'inverse', rolloff: 1}, |
| 41 startPosition: [0, 0, 1], |
| 42 // An essentially random end position, but it should be such that |
| 43 // azimuth angle changes as |
| 44 // we move from the start to the end. |
| 45 endPosition: [20000, 30000, 10000], |
| 46 errorThreshold: [ |
| 47 { |
| 48 // Error threshold for 1-channel case |
| 49 relativeThreshold: 4.8124e-7 |
| 50 }, |
| 51 { |
| 52 // Error threshold for 2-channel case |
| 53 relativeThreshold: 4.3267e-7 |
| 54 } |
| 55 ], |
| 56 }, |
| 57 { |
| 58 distanceModel: {model: 'exponential', rolloff: 1.5}, |
| 59 startPosition: [0, 0, 1], |
| 60 endPosition: [20000, 30000, 10000], |
| 61 errorThreshold: |
| 62 [{relativeThreshold: 5.0783e-7}, {relativeThreshold: 5.2180e-7}] |
| 63 }, |
| 64 { |
| 65 distanceModel: {model: 'linear', rolloff: 1}, |
| 66 startPosition: [0, 0, 1], |
| 67 endPosition: [20000, 30000, 10000], |
| 68 errorThreshold: [ |
| 69 {relativeThreshold: 6.5324e-6}, {relativeThreshold: 6.5756e-6} |
| 70 ] |
| 71 } |
| 72 ]; |
24 | 73 |
25 // Set of tests for the panner node with automations applied to the positi
on of the source. | 74 for (let k = 0; k < testConfigs.length; ++k) { |
26 var testConfigs = [{ | 75 let config = testConfigs[k]; |
27 // Distance model parameters for the panner | 76 let tester = function(c, channelCount) { |
28 distanceModel: { | |
29 model: "inverse", | |
30 rolloff: 1 | |
31 }, | |
32 // Initial location of the source | |
33 startPosition: [0, 0, 1], | |
34 // Final position of the source. For this test, we only want to move on
the z axis which | |
35 // doesn't change the azimuth angle. | |
36 endPosition: [0, 0, 10000], | |
37 }, { | |
38 distanceModel: { | |
39 model: "inverse", | |
40 rolloff: 1 | |
41 }, | |
42 startPosition: [0, 0, 1], | |
43 // An essentially random end position, but it should be such that azimut
h angle changes as | |
44 // we move from the start to the end. | |
45 endPosition: [20000, 30000, 10000], | |
46 errorThreshold: [{ | |
47 // Error threshold for 1-channel case | |
48 relativeThreshold: 4.8124e-7 | |
49 }, { | |
50 // Error threshold for 2-channel case | |
51 relativeThreshold: 4.3267e-7 | |
52 }], | |
53 }, { | |
54 distanceModel: { | |
55 model: "exponential", | |
56 rolloff: 1.5 | |
57 }, | |
58 startPosition: [0, 0, 1], | |
59 endPosition: [20000, 30000, 10000], | |
60 errorThreshold: [{ | |
61 relativeThreshold: 5.0783e-7 | |
62 }, { | |
63 relativeThreshold: 5.2180e-7 | |
64 }] | |
65 }, { | |
66 distanceModel: { | |
67 model: "linear", | |
68 rolloff: 1 | |
69 }, | |
70 startPosition: [0, 0, 1], | |
71 endPosition: [20000, 30000, 10000], | |
72 errorThreshold: [{ | |
73 relativeThreshold: 6.5324e-6 | |
74 }, { | |
75 relativeThreshold: 6.5756e-6 | |
76 }] | |
77 }]; | |
78 | |
79 for (var k = 0; k < testConfigs.length; ++k) { | |
80 var config = testConfigs[k]; | |
81 var tester = function (c, channelCount) { | |
82 return (task, should) => { | 77 return (task, should) => { |
83 runTest(should, c, channelCount) | 78 runTest(should, c, channelCount).then(() => task.done()); |
84 .then(() => task.done()); | |
85 } | 79 } |
86 }; | 80 }; |
87 | 81 |
88 var baseTestName = config.distanceModel.model + " rolloff: " + config.di
stanceModel.rolloff; | 82 let baseTestName = config.distanceModel.model + |
| 83 ' rolloff: ' + config.distanceModel.rolloff; |
89 | 84 |
90 // Define tasks for both 1-channel and 2-channel | 85 // Define tasks for both 1-channel and 2-channel |
91 audit.define(k + ": 1-channel " + baseTestName, tester(config, 1)); | 86 audit.define(k + ': 1-channel ' + baseTestName, tester(config, 1)); |
92 audit.define(k + ": 2-channel " + baseTestName, tester(config, 2)); | 87 audit.define(k + ': 2-channel ' + baseTestName, tester(config, 2)); |
93 } | 88 } |
94 | 89 |
95 audit.run(); | 90 audit.run(); |
96 | 91 |
97 function runTest(should, options, channelCount) { | 92 function runTest(should, options, channelCount) { |
98 // Output has 5 channels: channels 0 and 1 are for the stereo output of
the panner node. | 93 // Output has 5 channels: channels 0 and 1 are for the stereo output of |
99 // Channels 2-5 are the for automation of the x,y,z coordinate so that w
e have actual | 94 // the panner node. Channels 2-5 are the for automation of the x,y,z |
100 // coordinates used for the panner automation. | 95 // coordinate so that we have actual coordinates used for the panner |
| 96 // automation. |
101 context = new OfflineAudioContext(5, renderFrames, sampleRate); | 97 context = new OfflineAudioContext(5, renderFrames, sampleRate); |
102 | 98 |
103 // Stereo source for the panner. | 99 // Stereo source for the panner. |
104 var source = context.createBufferSource(); | 100 let source = context.createBufferSource(); |
105 source.buffer = createConstantBuffer(context, renderFrames, channelCount
== 1 ? 1 : [1, 2]); | 101 source.buffer = createConstantBuffer( |
| 102 context, renderFrames, channelCount == 1 ? 1 : [1, 2]); |
106 | 103 |
107 panner = context.createPanner(); | 104 panner = context.createPanner(); |
108 panner.distanceModel = options.distanceModel.model; | 105 panner.distanceModel = options.distanceModel.model; |
109 panner.rolloffFactor = options.distanceModel.rolloff; | 106 panner.rolloffFactor = options.distanceModel.rolloff; |
110 panner.panningModel = "equalpower"; | 107 panner.panningModel = 'equalpower'; |
111 | 108 |
112 // Source and gain node for the z-coordinate calculation. | 109 // Source and gain node for the z-coordinate calculation. |
113 var dist = context.createBufferSource(); | 110 let dist = context.createBufferSource(); |
114 dist.buffer = createConstantBuffer(context, 1, 1); | 111 dist.buffer = createConstantBuffer(context, 1, 1); |
115 dist.loop = true; | 112 dist.loop = true; |
116 var gainX = context.createGain(); | 113 let gainX = context.createGain(); |
117 var gainY = context.createGain(); | 114 let gainY = context.createGain(); |
118 var gainZ = context.createGain(); | 115 let gainZ = context.createGain(); |
119 dist.connect(gainX); | 116 dist.connect(gainX); |
120 dist.connect(gainY); | 117 dist.connect(gainY); |
121 dist.connect(gainZ); | 118 dist.connect(gainZ); |
122 | 119 |
123 // Set the gain automation to match the z-coordinate automation of the p
anner. | 120 // Set the gain automation to match the z-coordinate automation of the |
| 121 // panner. |
124 | 122 |
125 // End the automation some time before the end of the rendering so we ca
n verify that | 123 // End the automation some time before the end of the rendering so we |
126 // automation has the correct end time and value. | 124 // can verify that automation has the correct end time and value. |
127 var endAutomationTime = 0.75 * renderDuration; | 125 let endAutomationTime = 0.75 * renderDuration; |
128 | 126 |
129 gainX.gain.setValueAtTime(options.startPosition[0], 0); | 127 gainX.gain.setValueAtTime(options.startPosition[0], 0); |
130 gainX.gain.linearRampToValueAtTime(options.endPosition[0], endAutomation
Time); | 128 gainX.gain.linearRampToValueAtTime( |
| 129 options.endPosition[0], endAutomationTime); |
131 gainY.gain.setValueAtTime(options.startPosition[1], 0); | 130 gainY.gain.setValueAtTime(options.startPosition[1], 0); |
132 gainY.gain.linearRampToValueAtTime(options.endPosition[1], endAutomation
Time); | 131 gainY.gain.linearRampToValueAtTime( |
| 132 options.endPosition[1], endAutomationTime); |
133 gainZ.gain.setValueAtTime(options.startPosition[2], 0); | 133 gainZ.gain.setValueAtTime(options.startPosition[2], 0); |
134 gainZ.gain.linearRampToValueAtTime(options.endPosition[2], endAutomation
Time); | 134 gainZ.gain.linearRampToValueAtTime( |
| 135 options.endPosition[2], endAutomationTime); |
135 | 136 |
136 dist.start(); | 137 dist.start(); |
137 | 138 |
138 // Splitter and merger to map the panner output and the z-coordinate aut
omation to the | 139 // Splitter and merger to map the panner output and the z-coordinate |
139 // correct channels in the destination. | 140 // automation to the correct channels in the destination. |
140 var splitter = context.createChannelSplitter(2); | 141 let splitter = context.createChannelSplitter(2); |
141 var merger = context.createChannelMerger(5); | 142 let merger = context.createChannelMerger(5); |
142 | 143 |
143 source.connect(panner); | 144 source.connect(panner); |
144 // Split the output of the panner to separate channels | 145 // Split the output of the panner to separate channels |
145 panner.connect(splitter); | 146 panner.connect(splitter); |
146 | 147 |
147 // Merge the panner outputs and the z-coordinate output to the correct d
estination channels. | 148 // Merge the panner outputs and the z-coordinate output to the correct |
| 149 // destination channels. |
148 splitter.connect(merger, 0, 0); | 150 splitter.connect(merger, 0, 0); |
149 splitter.connect(merger, 1, 1); | 151 splitter.connect(merger, 1, 1); |
150 gainX.connect(merger, 0, 2); | 152 gainX.connect(merger, 0, 2); |
151 gainY.connect(merger, 0, 3); | 153 gainY.connect(merger, 0, 3); |
152 gainZ.connect(merger, 0, 4); | 154 gainZ.connect(merger, 0, 4); |
153 | 155 |
154 merger.connect(context.destination); | 156 merger.connect(context.destination); |
155 | 157 |
156 // Initialize starting point of the panner. | 158 // Initialize starting point of the panner. |
157 panner.positionX.setValueAtTime(options.startPosition[0], 0); | 159 panner.positionX.setValueAtTime(options.startPosition[0], 0); |
158 panner.positionY.setValueAtTime(options.startPosition[1], 0); | 160 panner.positionY.setValueAtTime(options.startPosition[1], 0); |
159 panner.positionZ.setValueAtTime(options.startPosition[2], 0); | 161 panner.positionZ.setValueAtTime(options.startPosition[2], 0); |
160 | 162 |
161 // Automate z coordinate to move away from the listener | 163 // Automate z coordinate to move away from the listener |
162 panner.positionX.linearRampToValueAtTime(options.endPosition[0], 0.75 *
renderDuration); | 164 panner.positionX.linearRampToValueAtTime( |
163 panner.positionY.linearRampToValueAtTime(options.endPosition[1], 0.75 *
renderDuration); | 165 options.endPosition[0], 0.75 * renderDuration); |
164 panner.positionZ.linearRampToValueAtTime(options.endPosition[2], 0.75 *
renderDuration); | 166 panner.positionY.linearRampToValueAtTime( |
| 167 options.endPosition[1], 0.75 * renderDuration); |
| 168 panner.positionZ.linearRampToValueAtTime( |
| 169 options.endPosition[2], 0.75 * renderDuration); |
165 | 170 |
166 source.start(); | 171 source.start(); |
167 | 172 |
168 // Go! | 173 // Go! |
169 return context.startRendering() | 174 return context.startRendering().then(function(renderedBuffer) { |
170 .then(function (renderedBuffer) { | 175 // Get the panner outputs |
171 // Get the panner outputs | 176 let data0 = renderedBuffer.getChannelData(0); |
172 var data0 = renderedBuffer.getChannelData(0); | 177 let data1 = renderedBuffer.getChannelData(1); |
173 var data1 = renderedBuffer.getChannelData(1); | 178 let xcoord = renderedBuffer.getChannelData(2); |
174 var xcoord = renderedBuffer.getChannelData(2); | 179 let ycoord = renderedBuffer.getChannelData(3); |
175 var ycoord = renderedBuffer.getChannelData(3); | 180 let zcoord = renderedBuffer.getChannelData(4); |
176 var zcoord = renderedBuffer.getChannelData(4); | |
177 | 181 |
178 // We're doing a linear ramp on the Z axis with the equalpower panne
r, so the equalpower | 182 // We're doing a linear ramp on the Z axis with the equalpower panner, |
179 // panning gain remains constant. We only need to model the distanc
e effect. | 183 // so the equalpower panning gain remains constant. We only need to |
| 184 // model the distance effect. |
180 | 185 |
181 // Compute the distance gain | 186 // Compute the distance gain |
182 var distanceGain = new Float32Array(xcoord.length);; | 187 let distanceGain = new Float32Array(xcoord.length); |
| 188 ; |
183 | 189 |
184 if (panner.distanceModel === "inverse") { | 190 if (panner.distanceModel === 'inverse') { |
185 for (var k = 0; k < distanceGain.length; ++k) { | 191 for (let k = 0; k < distanceGain.length; ++k) { |
186 distanceGain[k] = inverseDistance(panner, xcoord[k], ycoord[k],
zcoord[k]) | 192 distanceGain[k] = |
187 } | 193 inverseDistance(panner, xcoord[k], ycoord[k], zcoord[k]) |
188 } else if (panner.distanceModel === "linear") { | |
189 for (var k = 0; k < distanceGain.length; ++k) { | |
190 distanceGain[k] = linearDistance(panner, xcoord[k], ycoord[k], z
coord[k]) | |
191 } | |
192 } else if (panner.distanceModel === "exponential") { | |
193 for (var k = 0; k < distanceGain.length; ++k) { | |
194 distanceGain[k] = exponentialDistance(panner, xcoord[k], ycoord[
k], zcoord[k]) | |
195 } | |
196 } | 194 } |
| 195 } else if (panner.distanceModel === 'linear') { |
| 196 for (let k = 0; k < distanceGain.length; ++k) { |
| 197 distanceGain[k] = |
| 198 linearDistance(panner, xcoord[k], ycoord[k], zcoord[k]) |
| 199 } |
| 200 } else if (panner.distanceModel === 'exponential') { |
| 201 for (let k = 0; k < distanceGain.length; ++k) { |
| 202 distanceGain[k] = |
| 203 exponentialDistance(panner, xcoord[k], ycoord[k], zcoord[k]) |
| 204 } |
| 205 } |
197 | 206 |
198 // Compute the expected result. Since we're on the z-axis, the left
and right channels | 207 // Compute the expected result. Since we're on the z-axis, the left |
199 // pass through the equalpower panner unchanged. Only need to apply
the distance gain. | 208 // and right channels pass through the equalpower panner unchanged. |
200 var buffer0 = source.buffer.getChannelData(0); | 209 // Only need to apply the distance gain. |
201 var buffer1 = channelCount == 2 ? source.buffer.getChannelData(1) :
buffer0; | 210 let buffer0 = source.buffer.getChannelData(0); |
| 211 let buffer1 = |
| 212 channelCount == 2 ? source.buffer.getChannelData(1) : buffer0; |
202 | 213 |
203 var azimuth = new Float32Array(buffer0.length); | 214 let azimuth = new Float32Array(buffer0.length); |
204 | |
205 for (var k = 0; k < data0.length; ++k) { | |
206 azimuth[k] = calculateAzimuth([ | |
207 xcoord[k], | |
208 ycoord[k], | |
209 zcoord[k] | |
210 ], [ | |
211 context.listener.positionX.value, | |
212 context.listener.positionY.value, | |
213 context.listener.positionZ.value | |
214 ], [ | |
215 context.listener.forwardX.value, | |
216 context.listener.forwardY.value, | |
217 context.listener.forwardZ.value | |
218 ], [ | |
219 context.listener.upX.value, | |
220 context.listener.upY.value, | |
221 context.listener.upZ.value | |
222 ]); | |
223 } | |
224 | 215 |
225 var expected = applyPanner(azimuth, buffer0, buffer1, channelCount); | 216 for (let k = 0; k < data0.length; ++k) { |
226 var expected0 = expected.left; | 217 azimuth[k] = calculateAzimuth( |
227 var expected1 = expected.right; | 218 [xcoord[k], ycoord[k], zcoord[k]], |
228 | 219 [ |
229 for (var k = 0; k < expected0.length; ++k) { | 220 context.listener.positionX.value, |
230 expected0[k] *= distanceGain[k]; | 221 context.listener.positionY.value, |
231 expected1[k] *= distanceGain[k]; | 222 context.listener.positionZ.value |
232 } | 223 ], |
| 224 [ |
| 225 context.listener.forwardX.value, |
| 226 context.listener.forwardY.value, |
| 227 context.listener.forwardZ.value |
| 228 ], |
| 229 [ |
| 230 context.listener.upX.value, context.listener.upY.value, |
| 231 context.listener.upZ.value |
| 232 ]); |
| 233 } |
233 | 234 |
234 var info = options.distanceModel.model + ", rolloff: " + options.dis
tanceModel.rolloff; | 235 let expected = applyPanner(azimuth, buffer0, buffer1, channelCount); |
235 var prefix = channelCount + "-channel " | 236 let expected0 = expected.left; |
236 + "[" + options.startPosition[0] + ", " | 237 let expected1 = expected.right; |
237 + options.startPosition[1] + ", " | |
238 + options.startPosition[2] + "] -> [" | |
239 + options.endPosition[0] + ", " | |
240 + options.endPosition[1] + ", " | |
241 + options.endPosition[2] + "]: "; | |
242 | 238 |
243 var errorThreshold = 0; | 239 for (let k = 0; k < expected0.length; ++k) { |
| 240 expected0[k] *= distanceGain[k]; |
| 241 expected1[k] *= distanceGain[k]; |
| 242 } |
244 | 243 |
245 if (options.errorThreshold) | 244 let info = options.distanceModel.model + |
246 errorThreshold = options.errorThreshold[channelCount - 1] | 245 ', rolloff: ' + options.distanceModel.rolloff; |
| 246 let prefix = channelCount + '-channel ' + |
| 247 '[' + options.startPosition[0] + ', ' + options.startPosition[1] + |
| 248 ', ' + options.startPosition[2] + '] -> [' + |
| 249 options.endPosition[0] + ', ' + options.endPosition[1] + ', ' + |
| 250 options.endPosition[2] + ']: '; |
247 | 251 |
248 should(data0, prefix + "distanceModel: " + info + ", left channel") | 252 let errorThreshold = 0; |
249 .beCloseToArray(expected0, { | 253 |
250 absoluteThreshold: errorThreshold | 254 if (options.errorThreshold) |
251 }); | 255 errorThreshold = options.errorThreshold[channelCount - 1] |
252 should(data1, prefix + "distanceModel: " + info + ", right channel") | 256 |
253 .beCloseToArray(expected1, { | 257 should(data0, prefix + 'distanceModel: ' + info + ', left channel') |
254 absoluteThreshold: errorThreshold | 258 .beCloseToArray(expected0, {absoluteThreshold: errorThreshold}); |
255 }); | 259 should(data1, prefix + 'distanceModel: ' + info + ', right channel') |
256 }); | 260 .beCloseToArray(expected1, {absoluteThreshold: errorThreshold}); |
| 261 }); |
257 } | 262 } |
258 </script> | 263 </script> |
259 </body> | 264 </body> |
260 </html> | 265 </html> |
OLD | NEW |