k-rate-panner.html (8539B)
1 <!doctype html> 2 <html> 3 <head> 4 <title>Test k-rate AudioParams of PannerNode</title> 5 <script src="/resources/testharness.js"></script> 6 <script src="/resources/testharnessreport.js"></script> 7 </head> 8 <body> 9 <script> 10 11 function assert_not_constant(arr, description) { 12 const first = arr[0]; 13 for (let i = 1; i < arr.length; ++i) { 14 if (Math.abs(arr[i] - first) > Number.EPSILON) { 15 // If any element differs from the first by more than a negligible 16 // amount, the array is not constant, and the assertion passes. 17 return; 18 } 19 } 20 assert_unreached(`${description}: unexpectedly constant`); 21 } 22 23 function assert_all_close(arr, value, description) { 24 for (const x of arr) { 25 assert_approx_equals(x, value, Number.EPSILON, description); 26 } 27 } 28 29 function assert_all_constant(arr, value, description) { 30 for (const x of arr) { 31 assert_equals(x, value, description); 32 } 33 } 34 35 36 // Represents the 'k-rate' AudioParam automation rate. 37 const K_RATE = 'k-rate'; 38 // Defines the size of one audio processing block (render quantum) 39 // in frames. 40 const BLOCK = 128; 41 // Arbitrary sample rate and duration. 42 const SAMPLE_RATE = 8000; 43 44 // Define a test where we verify that a k-rate audio param produces 45 // different results from an a-rate audio param for each of the audio 46 // params of a biquad. 47 // 48 // Each entry gives the name of the AudioParam, an initial value to be 49 // used with setValueAtTime, and a final value to be used with 50 // linearRampToValueAtTime. (See |doTest| for details as well.) 51 52 const pannerParams = [ 53 {name: 'positionX', initial: 0, final: 1000}, 54 {name: 'positionY', initial: 0, final: 1000}, 55 {name: 'orientationX', initial: 1, final: 10}, 56 {name: 'orientationY', initial: 1, final: 10}, 57 {name: 'orientationZ', initial: 1, final: 10}, 58 ]; 59 60 pannerParams.forEach(param => { 61 promise_test(async t => { 62 const testDuration = (5 * BLOCK) / SAMPLE_RATE; 63 const context = new OfflineAudioContext({ 64 numberOfChannels: 3, 65 sampleRate: SAMPLE_RATE, 66 length: testDuration * SAMPLE_RATE, 67 }); 68 69 const merger = new ChannelMergerNode(context, {numberOfInputs: 3}); 70 merger.connect(context.destination); 71 // Graph: ConstantSource → Panner → destination 72 const source = new ConstantSourceNode(context); 73 const commonOpts = { 74 distanceModel: 'inverse', 75 coneOuterAngle: 360, 76 coneInnerAngle: 0, 77 positionX: 1, 78 positionY: 1, 79 positionZ: 1, 80 orientationX: 0, 81 orientationY: 1, 82 orientationZ: 1, 83 }; 84 85 const kRatePanner = new PannerNode(context, commonOpts); 86 const aRatePanner = new PannerNode(context, commonOpts); 87 88 // Switch only the k‑rate node’s target param to k‑rate 89 // automation 90 const kRateParam = kRatePanner[param.name]; 91 kRateParam.automationRate = K_RATE; 92 assert_equals(kRateParam.automationRate, K_RATE, 93 `${param.name}.automationRate should be k‑rate`); 94 95 // Identical automation on both nodes 96 [kRatePanner, aRatePanner].forEach(panner => { 97 panner[param.name].setValueAtTime(param.initial, 0); 98 panner[param.name].linearRampToValueAtTime(param.final, 99 testDuration); 100 }); 101 102 // Build routing: source → both panners 103 source.connect(kRatePanner); 104 source.connect(aRatePanner); 105 106 // k‑rate result → channel‑0; a‑rate → channel‑1 107 kRatePanner.connect(merger, 0, 0); 108 aRatePanner.connect(merger, 0, 1); 109 110 // Difference channel: k‑rate – a‑rate 111 const inverter = new GainNode(context, {gain: -1}); 112 kRatePanner.connect(merger, 0, 2); 113 aRatePanner.connect(inverter).connect(merger, 0, 2); 114 115 source.start(); 116 117 const buffer = await context.startRendering(); 118 119 const kData = buffer.getChannelData(0); 120 const aData = buffer.getChannelData(1); 121 const diff = buffer.getChannelData(2); 122 123 // The difference signal must NOT be constant zero. 124 assert_not_constant(diff, `Panner ${param.name} – diff`); 125 126 // Verify that the k‑rate output is constant over each render quantum 127 for (let k = 0; k < kData.length; k += BLOCK) { 128 const slice = kData.slice(k, k + BLOCK); 129 assert_all_close(slice, slice[0], 130 `Panner ${param.name} k‑rate frames [` + 131 `${k}, ${k + slice.length - 1}]` 132 ); 133 } 134 135 // (No strict requirement on a‑rate slice variability, so we skip.) 136 }, `Panner k‑rate vs a‑rate – ${param.name}`); 137 }); 138 139 // Test k-rate automation of the listener. The intial and final 140 // automation values are pretty arbitrary, except that they should be such 141 // that the panner and listener produces non-constant output. 142 const listenerParams = [ 143 {name: 'positionX', initial: [1, 0], final: [1000, 1]}, 144 {name: 'positionY', initial: [1, 0], final: [1000, 1]}, 145 {name: 'positionZ', initial: [1, 0], final: [1000, 1]}, 146 {name: 'forwardX', initial: [-1, 0], final: [1, 1]}, 147 {name: 'forwardY', initial: [-1, 0], final: [1, 1]}, 148 {name: 'forwardZ', initial: [-1, 0], final: [1, 1]}, 149 {name: 'upX', initial: [-1, 0], final: [1000, 1]}, 150 {name: 'upY', initial: [-1, 0], final: [1000, 1]}, 151 {name: 'upZ', initial: [-1, 0], final: [1000, 1]}, 152 ]; 153 154 listenerParams.forEach(param => { 155 promise_test(async t => { 156 const testDuration = (5 * BLOCK) / SAMPLE_RATE; 157 const context = new OfflineAudioContext({ 158 numberOfChannels: 1, 159 sampleRate: SAMPLE_RATE, 160 length: testDuration * SAMPLE_RATE, 161 }); 162 163 const source = new ConstantSourceNode(context); 164 const panner = new PannerNode(context, { 165 distanceModel: 'inverse', 166 coneOuterAngle: 360, 167 coneInnerAngle: 10, 168 positionX: 10, 169 positionY: 10, 170 positionZ: 10, 171 orientationX: 1, 172 orientationY: 1, 173 orientationZ: 1, 174 }); 175 source.connect(panner).connect(context.destination); 176 source.start(); 177 178 const listener = context.listener; 179 180 // Set listener properties to "random" values so that motion on one of 181 // the attributes actually changes things relative to the panner 182 // location. And the up and forward directions should have a simple 183 // relationship between them. 184 listener.positionX.value = -1; 185 listener.positionY.value = 1; 186 listener.positionZ.value = -1; 187 listener.forwardX.value = -1; 188 listener.forwardY.value = 1; 189 listener.forwardZ.value = -1; 190 // Make the up vector not parallel or perpendicular to the forward and 191 // position vectors so that automations of the up vector produce 192 // noticeable differences. 193 listener.upX.value = 1; 194 listener.upY.value = 1; 195 listener.upZ.value = 2; 196 197 const audioParam = listener[param.name]; 198 audioParam.automationRate = K_RATE; 199 assert_equals( 200 audioParam.automationRate, 201 K_RATE, 202 `Listener ${param.name}.automationRate` 203 ); 204 205 audioParam.setValueAtTime(...param.initial); 206 audioParam.linearRampToValueAtTime(...param.final); 207 208 const buffer = await context.startRendering(); 209 const data = buffer.getChannelData(0); 210 211 assert_not_constant(data, `Listener ${param.name}`); 212 for (let k = 0; k < data.length; k += BLOCK) { 213 const slice = data.slice( 214 k, 215 Math.min(k + BLOCK, data.length) 216 ); 217 assert_all_constant( 218 slice, 219 slice[0], 220 `Listener ${param.name} frames [${k}, ` + 221 `${k + slice.length - 1}]` 222 ); 223 } 224 }, `Listener k-rate ${param.name}`); 225 }); 226 </script> 227 </body> 228 </html>