sub-sample-scheduling.html (17393B)
1 <!doctype html> 2 <html> 3 <head> 4 <title> 5 Test Sub-Sample Accurate Scheduling for ABSN 6 </title> 7 <script src="/resources/testharness.js"></script> 8 <script src="/resources/testharnessreport.js"></script> 9 <script src="/webaudio/resources/audit-util.js"></script> 10 <script src="/webaudio/resources/audit.js"></script> 11 </head> 12 <body> 13 <script> 14 // Power of two so there's no roundoff converting from integer frames to 15 // time. 16 let sampleRate = 32768; 17 18 let audit = Audit.createTaskRunner(); 19 20 audit.define('sub-sample accurate start', (task, should) => { 21 // There are two channels, one for each source. Only need to render 22 // quanta for this test. 23 let context = new OfflineAudioContext( 24 {numberOfChannels: 2, length: 8192, sampleRate: sampleRate}); 25 let merger = new ChannelMergerNode( 26 context, {numberOfInputs: context.destination.channelCount}); 27 28 merger.connect(context.destination); 29 30 // Use a simple linear ramp for the sources with integer steps starting 31 // at 1 to make it easy to verify and test that have sub-sample accurate 32 // start. Ramp starts at 1 so we can easily tell when the source 33 // starts. 34 let rampBuffer = new AudioBuffer( 35 {length: context.length, sampleRate: context.sampleRate}); 36 let r = rampBuffer.getChannelData(0); 37 for (let k = 0; k < r.length; ++k) { 38 r[k] = k + 1; 39 } 40 41 const src0 = new AudioBufferSourceNode(context, {buffer: rampBuffer}); 42 const src1 = new AudioBufferSourceNode(context, {buffer: rampBuffer}); 43 44 // Frame where sources should start. This is pretty arbitrary, but one 45 // should be close to an integer and the other should be close to the 46 // next integer. We do this to catch the case where rounding of the 47 // start frame is being done. Rounding is incorrect. 48 const startFrame = 33; 49 const startFrame0 = startFrame + 0.1; 50 const startFrame1 = startFrame + 0.9; 51 52 src0.connect(merger, 0, 0); 53 src1.connect(merger, 0, 1); 54 55 src0.start(startFrame0 / context.sampleRate); 56 src1.start(startFrame1 / context.sampleRate); 57 58 context.startRendering() 59 .then(audioBuffer => { 60 const output0 = audioBuffer.getChannelData(0); 61 const output1 = audioBuffer.getChannelData(1); 62 63 // Compute the expected output by interpolating the ramp buffer of 64 // the sources if they started at the given frame. 65 const ramp = rampBuffer.getChannelData(0); 66 const expected0 = interpolateRamp(ramp, startFrame0); 67 const expected1 = interpolateRamp(ramp, startFrame1); 68 69 // Verify output0 has the correct values 70 71 // For information only 72 should(startFrame0, 'src0 start frame').beEqualTo(startFrame0); 73 74 // Output must be zero before the source start frame, and it must 75 // be interpolated correctly after the start frame. The 76 // absoluteThreshold below is currently set for Chrome which does 77 // linear interpolation. This needs to be updated eventually if 78 // other browsers do not user interpolation. 79 should( 80 output0.slice(0, startFrame + 1), `output0[0:${startFrame}]`) 81 .beConstantValueOf(0); 82 should( 83 output0.slice(startFrame + 1, expected0.length), 84 `output0[${startFrame + 1}:${expected0.length - 1}]`) 85 .beCloseToArray( 86 expected0.slice(startFrame + 1), {absoluteThreshold: 0}); 87 88 // Verify output1 has the correct values. Same approach as for 89 // output0. 90 should(startFrame1, 'src1 start frame').beEqualTo(startFrame1); 91 92 should( 93 output1.slice(0, startFrame + 1), `output1[0:${startFrame}]`) 94 .beConstantValueOf(0); 95 should( 96 output1.slice(startFrame + 1, expected1.length), 97 `output1[${startFrame + 1}:${expected1.length - 1}]`) 98 .beCloseToArray( 99 expected1.slice(startFrame + 1), {absoluteThreshold: 0}); 100 }) 101 .then(() => task.done()); 102 }); 103 104 audit.define('sub-sample accurate stop', (task, should) => { 105 // There are threes channesl, one for each source. Only need to render 106 // quanta for this test. 107 let context = new OfflineAudioContext( 108 {numberOfChannels: 3, length: 128, sampleRate: sampleRate}); 109 let merger = new ChannelMergerNode( 110 context, {numberOfInputs: context.destination.channelCount}); 111 112 merger.connect(context.destination); 113 114 // The source can be as simple constant for this test. 115 let buffer = new AudioBuffer( 116 {length: context.length, sampleRate: context.sampleRate}); 117 buffer.getChannelData(0).fill(1); 118 119 const src0 = new AudioBufferSourceNode(context, {buffer: buffer}); 120 const src1 = new AudioBufferSourceNode(context, {buffer: buffer}); 121 const src2 = new AudioBufferSourceNode(context, {buffer: buffer}); 122 123 // Frame where sources should start. This is pretty arbitrary, but one 124 // should be an integer, one should be close to an integer and the other 125 // should be close to the next integer. This is to catch the case where 126 // rounding is used for the end frame. Rounding is incorrect. 127 const endFrame = 33; 128 const endFrame1 = endFrame + 0.1; 129 const endFrame2 = endFrame + 0.9; 130 131 src0.connect(merger, 0, 0); 132 src1.connect(merger, 0, 1); 133 src2.connect(merger, 0, 2); 134 135 src0.start(0); 136 src1.start(0); 137 src2.start(0); 138 src0.stop(endFrame / context.sampleRate); 139 src1.stop(endFrame1 / context.sampleRate); 140 src2.stop(endFrame2 / context.sampleRate); 141 142 context.startRendering() 143 .then(audioBuffer => { 144 let actual0 = audioBuffer.getChannelData(0); 145 let actual1 = audioBuffer.getChannelData(1); 146 let actual2 = audioBuffer.getChannelData(2); 147 148 // Just verify that we stopped at the right time. 149 150 // This is case where the end frame is an integer. Since the first 151 // output ends on an exact frame, the output must be zero at that 152 // frame number. We print the end frame for information only; it 153 // makes interpretation of the rest easier. 154 should(endFrame - 1, 'src0 end frame') 155 .beEqualTo(endFrame - 1); 156 should(actual0[endFrame - 1], `output0[${endFrame - 1}]`) 157 .notBeEqualTo(0); 158 should(actual0.slice(endFrame), 159 `output0[${endFrame}:]`) 160 .beConstantValueOf(0); 161 162 // The case where the end frame is just a little above an integer. 163 // The output must not be zero just before the end and must be zero 164 // after. 165 should(endFrame1, 'src1 end frame') 166 .beEqualTo(endFrame1); 167 should(actual1[endFrame], `output1[${endFrame}]`) 168 .notBeEqualTo(0); 169 should(actual1.slice(endFrame + 1), 170 `output1[${endFrame + 1}:]`) 171 .beConstantValueOf(0); 172 173 // The case where the end frame is just a little below an integer. 174 // The output must not be zero just before the end and must be zero 175 // after. 176 should(endFrame2, 'src2 end frame') 177 .beEqualTo(endFrame2); 178 should(actual2[endFrame], `output2[${endFrame}]`) 179 .notBeEqualTo(0); 180 should(actual2.slice(endFrame + 1), 181 `output2[${endFrame + 1}:]`) 182 .beConstantValueOf(0); 183 }) 184 .then(() => task.done()); 185 }); 186 187 audit.define('sub-sample-grain', (task, should) => { 188 let context = new OfflineAudioContext( 189 {numberOfChannels: 2, length: 128, sampleRate: sampleRate}); 190 191 let merger = new ChannelMergerNode( 192 context, {numberOfInputs: context.destination.channelCount}); 193 194 merger.connect(context.destination); 195 196 // The source can be as simple constant for this test. 197 let buffer = new AudioBuffer( 198 {length: context.length, sampleRate: context.sampleRate}); 199 buffer.getChannelData(0).fill(1); 200 201 let src0 = new AudioBufferSourceNode(context, {buffer: buffer}); 202 let src1 = new AudioBufferSourceNode(context, {buffer: buffer}); 203 204 src0.connect(merger, 0, 0); 205 src1.connect(merger, 0, 1); 206 207 // Start a short grain. 208 const src0StartGrain = 3.1; 209 const src0EndGrain = 37.2; 210 src0.start( 211 src0StartGrain / context.sampleRate, 0, 212 (src0EndGrain - src0StartGrain) / context.sampleRate); 213 214 const src1StartGrain = 5.8; 215 const src1EndGrain = 43.9; 216 src1.start( 217 src1StartGrain / context.sampleRate, 0, 218 (src1EndGrain - src1StartGrain) / context.sampleRate); 219 220 context.startRendering() 221 .then(audioBuffer => { 222 let output0 = audioBuffer.getChannelData(0); 223 let output1 = audioBuffer.getChannelData(1); 224 225 let expected = new Float32Array(context.length); 226 227 // Compute the expected output for output0 and verify the actual 228 // output matches. 229 expected.fill(1); 230 for (let k = 0; k <= Math.floor(src0StartGrain); ++k) { 231 expected[k] = 0; 232 } 233 for (let k = Math.ceil(src0EndGrain); k < expected.length; ++k) { 234 expected[k] = 0; 235 } 236 237 verifyGrain(should, output0, { 238 startGrain: src0StartGrain, 239 endGrain: src0EndGrain, 240 sourceName: 'src0', 241 outputName: 'output0' 242 }); 243 244 verifyGrain(should, output1, { 245 startGrain: src1StartGrain, 246 endGrain: src1EndGrain, 247 sourceName: 'src1', 248 outputName: 'output1' 249 }); 250 }) 251 .then(() => task.done()); 252 }); 253 254 audit.define( 255 'sub-sample accurate start with playbackRate', (task, should) => { 256 // There are two channels, one for each source. Only need to render 257 // quanta for this test. 258 let context = new OfflineAudioContext( 259 {numberOfChannels: 2, length: 8192, sampleRate: sampleRate}); 260 let merger = new ChannelMergerNode( 261 context, {numberOfInputs: context.destination.channelCount}); 262 263 merger.connect(context.destination); 264 265 // Use a simple linear ramp for the sources with integer steps 266 // starting at 1 to make it easy to verify and test that have 267 // sub-sample accurate start. Ramp starts at 1 so we can easily 268 // tell when the source starts. 269 let buffer = new AudioBuffer( 270 {length: context.length, sampleRate: context.sampleRate}); 271 let r = buffer.getChannelData(0); 272 for (let k = 0; k < r.length; ++k) { 273 r[k] = k + 1; 274 } 275 276 // Two sources with different playback rates 277 const src0 = new AudioBufferSourceNode( 278 context, {buffer: buffer, playbackRate: .25}); 279 const src1 = new AudioBufferSourceNode( 280 context, {buffer: buffer, playbackRate: 4}); 281 282 // Frame where sources start. Pretty arbitrary but should not be an 283 // integer. 284 const startFrame = 17.8; 285 286 src0.connect(merger, 0, 0); 287 src1.connect(merger, 0, 1); 288 289 src0.start(startFrame / context.sampleRate); 290 src1.start(startFrame / context.sampleRate); 291 292 context.startRendering() 293 .then(audioBuffer => { 294 const output0 = audioBuffer.getChannelData(0); 295 const output1 = audioBuffer.getChannelData(1); 296 297 const frameBefore = Math.floor(startFrame); 298 const frameAfter = frameBefore + 1; 299 300 // Informative message so we know what the following output 301 // indices really mean. 302 should(startFrame, 'Source start frame') 303 .beEqualTo(startFrame); 304 305 // Verify the output 306 307 // With a startFrame of 17.8, the first output is at frame 18, 308 // but the actual start is at 17.8. So we would interpolate 309 // the output 0.2 fraction of the way between 17.8 and 18, for 310 // an output of 1.2 for our ramp. But the playback rate is 311 // 0.25, so we're really only 1/4 as far along as we think so 312 // the output is .2*0.25 of the way between 1 and 2 or 1.05. 313 314 const ramp0 = buffer.getChannelData(0)[0]; 315 const ramp1 = buffer.getChannelData(0)[1]; 316 317 const src0Output = ramp0 + 318 (ramp1 - ramp0) * (frameAfter - startFrame) * 319 src0.playbackRate.value; 320 321 let playbackMessage = 322 `With playbackRate ${src0.playbackRate.value}:`; 323 324 should( 325 output0[frameBefore], 326 `${playbackMessage} output0[${frameBefore}]`) 327 .beEqualTo(0); 328 should( 329 output0[frameAfter], 330 `${playbackMessage} output0[${frameAfter}]`) 331 .beCloseTo(src0Output, {threshold: 4.542e-8}); 332 333 const src1Output = ramp0 + 334 (ramp1 - ramp0) * (frameAfter - startFrame) * 335 src1.playbackRate.value; 336 337 playbackMessage = 338 `With playbackRate ${src1.playbackRate.value}:`; 339 340 should( 341 output1[frameBefore], 342 `${playbackMessage} output1[${frameBefore}]`) 343 .beEqualTo(0); 344 should( 345 output1[frameAfter], 346 `${playbackMessage} output1[${frameAfter}]`) 347 .beCloseTo(src1Output, {threshold: 4.542e-8}); 348 }) 349 .then(() => task.done()); 350 }); 351 352 audit.run(); 353 354 // Given an input ramp in |rampBuffer|, interpolate the signal assuming 355 // this ramp is used for an ABSN that starts at frame |startFrame|, which 356 // is not necessarily an integer. For simplicity we just use linear 357 // interpolation here. The interpolation is not part of the spec but 358 // this should be pretty close to whatever interpolation is being done. 359 function interpolateRamp(rampBuffer, startFrame) { 360 // |start| is the last zero sample before the ABSN actually starts. 361 const start = Math.floor(startFrame); 362 // One less than the rampBuffer because we can't linearly interpolate 363 // the last frame. 364 let result = new Float32Array(rampBuffer.length - 1); 365 366 for (let k = 0; k <= start; ++k) { 367 result[k] = 0; 368 } 369 370 // Now start linear interpolation. 371 let frame = startFrame; 372 let index = 1; 373 for (let k = start + 1; k < result.length; ++k) { 374 let s0 = rampBuffer[index]; 375 let s1 = rampBuffer[index - 1]; 376 let delta = frame - k; 377 let s = s1 - delta * (s0 - s1); 378 result[k] = s; 379 ++frame; 380 ++index; 381 } 382 383 return result; 384 } 385 386 function verifyGrain(should, output, options) { 387 let {startGrain, endGrain, sourceName, outputName} = options; 388 let expected = new Float32Array(output.length); 389 // Compute the expected output for output and verify the actual 390 // output matches. 391 expected.fill(1); 392 for (let k = 0; k <= Math.floor(startGrain); ++k) { 393 expected[k] = 0; 394 } 395 for (let k = Math.ceil(endGrain); k < expected.length; ++k) { 396 expected[k] = 0; 397 } 398 399 should(startGrain, `${sourceName} grain start`).beEqualTo(startGrain); 400 should(endGrain - startGrain, `${sourceName} grain duration`) 401 .beEqualTo(endGrain - startGrain); 402 should(endGrain, `${sourceName} grain end`).beEqualTo(endGrain); 403 should(output, outputName).beEqualToArray(expected); 404 should( 405 output[Math.floor(startGrain)], 406 `${outputName}[${Math.floor(startGrain)}]`) 407 .beEqualTo(0); 408 should( 409 output[1 + Math.floor(startGrain)], 410 `${outputName}[${1 + Math.floor(startGrain)}]`) 411 .notBeEqualTo(0); 412 should( 413 output[Math.floor(endGrain)], 414 `${outputName}[${Math.floor(endGrain)}]`) 415 .notBeEqualTo(0); 416 should( 417 output[1 + Math.floor(endGrain)], 418 `${outputName}[${1 + Math.floor(endGrain)}]`) 419 .beEqualTo(0); 420 } 421 </script> 422 </body> 423 </html>