| 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 |