event-insertion.html (15963B)
1 <!doctype html> 2 <html> 3 <head> 4 <title> 5 Test Handling of Event Insertion 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 <script src="/webaudio/resources/audio-param.js"></script> 12 </head> 13 <body> 14 <script id="layout-test-code"> 15 let audit = Audit.createTaskRunner(); 16 17 // Use a power of two for the sample rate so there's no round-off in 18 // computing time from frame. 19 let sampleRate = 16384; 20 21 audit.define( 22 {label: 'Insert same event at same time'}, (task, should) => { 23 // Context for testing. 24 let context = new OfflineAudioContext( 25 {length: 16384, sampleRate: sampleRate}); 26 27 // The source node to use. Automations will be scheduled here. 28 let src = new ConstantSourceNode(context, {offset: 0}); 29 src.connect(context.destination); 30 31 // An array of tests to be done. Each entry specifies the event 32 // type and the event time. The events are inserted in the order 33 // given (in |values|), and the second event should be inserted 34 // after the first one, as required by the spec. 35 let testCases = [ 36 { 37 event: 'setValueAtTime', 38 frame: RENDER_QUANTUM_FRAMES, 39 values: [99, 1], 40 outputTestFrame: RENDER_QUANTUM_FRAMES, 41 expectedOutputValue: 1 42 }, 43 { 44 event: 'linearRampToValueAtTime', 45 frame: 2 * RENDER_QUANTUM_FRAMES, 46 values: [99, 2], 47 outputTestFrame: 2 * RENDER_QUANTUM_FRAMES, 48 expectedOutputValue: 2 49 }, 50 { 51 event: 'exponentialRampToValueAtTime', 52 frame: 3 * RENDER_QUANTUM_FRAMES, 53 values: [99, 3], 54 outputTestFrame: 3 * RENDER_QUANTUM_FRAMES, 55 expectedOutputValue: 3 56 }, 57 { 58 event: 'setValueCurveAtTime', 59 frame: 3 * RENDER_QUANTUM_FRAMES, 60 values: [[3, 4]], 61 extraArgs: RENDER_QUANTUM_FRAMES / context.sampleRate, 62 outputTestFrame: 4 * RENDER_QUANTUM_FRAMES, 63 expectedOutputValue: 4 64 }, 65 { 66 event: 'setValueAtTime', 67 frame: 5 * RENDER_QUANTUM_FRAMES - 1, 68 values: [99, 1, 5], 69 outputTestFrame: 5 * RENDER_QUANTUM_FRAMES, 70 expectedOutputValue: 5 71 } 72 ]; 73 74 testCases.forEach(entry => { 75 entry.values.forEach(value => { 76 let eventTime = entry.frame / context.sampleRate; 77 let message = eventToString( 78 entry.event, value, eventTime, entry.extraArgs); 79 // This is mostly to print out the event that is getting 80 // inserted. It should never ever throw. 81 should(() => { 82 src.offset[entry.event](value, eventTime, entry.extraArgs); 83 }, message).notThrow(); 84 }); 85 }); 86 87 src.start(); 88 89 context.startRendering() 90 .then(audioBuffer => { 91 let audio = audioBuffer.getChannelData(0); 92 93 // Look through the test cases to figure out what the correct 94 // output values should be. 95 testCases.forEach(entry => { 96 let expected = entry.expectedOutputValue; 97 let frame = entry.outputTestFrame; 98 let time = frame / context.sampleRate; 99 should( 100 audio[frame], `Output at frame ${frame} (time ${time})`) 101 .beEqualTo(expected); 102 }); 103 }) 104 .then(() => task.done()); 105 }); 106 107 audit.define( 108 { 109 label: 'Linear + Expo', 110 description: 'Different events at same time' 111 }, 112 (task, should) => { 113 // Should be a linear ramp up to the event time, and after a 114 // constant value because the exponential ramp has ended. 115 let testCase = [ 116 {event: 'linearRampToValueAtTime', value: 2, relError: 0}, 117 {event: 'setValueAtTime', value: 99}, 118 {event: 'exponentialRampToValueAtTime', value: 3}, 119 ]; 120 let eventFrame = 2 * RENDER_QUANTUM_FRAMES; 121 let prefix = 'Linear+Expo: '; 122 123 testEventInsertion(prefix, should, eventFrame, testCase) 124 .then(expectConstant(prefix, should, eventFrame, testCase)) 125 .then(() => task.done()); 126 }); 127 128 audit.define( 129 { 130 label: 'Expo + Linear', 131 description: 'Different events at same time', 132 }, 133 (task, should) => { 134 // Should be an exponential ramp up to the event time, and after a 135 // constant value because the linear ramp has ended. 136 let testCase = [ 137 { 138 event: 'exponentialRampToValueAtTime', 139 value: 3, 140 relError: 4.2533e-6 141 }, 142 {event: 'setValueAtTime', value: 99}, 143 {event: 'linearRampToValueAtTime', value: 2}, 144 ]; 145 let eventFrame = 2 * RENDER_QUANTUM_FRAMES; 146 let prefix = 'Expo+Linear: '; 147 148 testEventInsertion(prefix, should, eventFrame, testCase) 149 .then(expectConstant(prefix, should, eventFrame, testCase)) 150 .then(() => task.done()); 151 }); 152 153 audit.define( 154 { 155 label: 'Linear + SetTarget', 156 description: 'Different events at same time', 157 }, 158 (task, should) => { 159 // Should be a linear ramp up to the event time, and then a 160 // decaying value. 161 let testCase = [ 162 {event: 'linearRampToValueAtTime', value: 3, relError: 0}, 163 {event: 'setValueAtTime', value: 100}, 164 {event: 'setTargetAtTime', value: 0, extraArgs: 0.1}, 165 ]; 166 let eventFrame = 2 * RENDER_QUANTUM_FRAMES; 167 let prefix = 'Linear+SetTarget: '; 168 169 testEventInsertion(prefix, should, eventFrame, testCase) 170 .then(audioBuffer => { 171 let audio = audioBuffer.getChannelData(0); 172 let prefix = 'Linear+SetTarget: '; 173 let eventTime = eventFrame / sampleRate; 174 let expectedValue = methodMap[testCase[0].event]( 175 (eventFrame - 1) / sampleRate, 1, 0, testCase[0].value, 176 eventTime); 177 should( 178 audio[eventFrame - 1], 179 prefix + 180 `At time ${ 181 (eventFrame - 1) / sampleRate 182 } (frame ${eventFrame - 1}) output`) 183 .beCloseTo( 184 expectedValue, 185 {threshold: testCase[0].relError || 0}); 186 187 // The setValue should have taken effect 188 should( 189 audio[eventFrame], 190 prefix + 191 `At time ${eventTime} (frame ${eventFrame}) output`) 192 .beEqualTo(testCase[1].value); 193 194 // The final event is setTarget. Compute the expected output. 195 let actual = audio.slice(eventFrame); 196 let expected = new Float32Array(actual.length); 197 for (let k = 0; k < expected.length; ++k) { 198 let t = (eventFrame + k) / sampleRate; 199 expected[k] = audioParamSetTarget( 200 t, testCase[1].value, eventTime, testCase[2].value, 201 testCase[2].extraArgs); 202 } 203 should( 204 actual, 205 prefix + 206 `At time ${eventTime} (frame ${ 207 eventFrame 208 }) and later`) 209 .beCloseToArray(expected, {relativeThreshold: 2.6694e-7}); 210 }) 211 .then(() => task.done()); 212 }); 213 214 audit.define( 215 { 216 label: 'Multiple linear ramps at the same time', 217 description: 'Verify output' 218 }, 219 (task, should) => { 220 testMultipleSameEvents(should, { 221 method: 'linearRampToValueAtTime', 222 prefix: 'Multiple linear ramps: ', 223 threshold: 0 224 }).then(() => task.done()); 225 }); 226 227 audit.define( 228 { 229 label: 'Multiple exponential ramps at the same time', 230 description: 'Verify output' 231 }, 232 (task, should) => { 233 testMultipleSameEvents(should, { 234 method: 'exponentialRampToValueAtTime', 235 prefix: 'Multiple exponential ramps: ', 236 threshold: 5.3924e-7 237 }).then(() => task.done()); 238 }); 239 240 audit.run(); 241 242 // Takes a list of |testCases| consisting of automation methods and 243 // schedules them to occur at |eventFrame|. |prefix| is a prefix for 244 // messages produced by |should|. 245 // 246 // Each item in |testCases| is a dictionary with members: 247 // event - the name of automation method to be inserted, 248 // value - the value for the event, 249 // extraArgs - extra arguments if the event needs more than the value 250 // and time (such as setTargetAtTime). 251 function testEventInsertion(prefix, should, eventFrame, testCases) { 252 let context = new OfflineAudioContext( 253 {length: 4 * RENDER_QUANTUM_FRAMES, sampleRate: sampleRate}); 254 255 // The source node to use. Automations will be scheduled here. 256 let src = new ConstantSourceNode(context, {offset: 0}); 257 src.connect(context.destination); 258 259 // Initialize value to 1 at the beginning. 260 src.offset.setValueAtTime(1, 0); 261 262 // Test automations have this event time. 263 let eventTime = eventFrame / context.sampleRate; 264 265 // Sanity check that context is long enough for the test 266 should( 267 eventFrame < context.length, 268 prefix + 'Context length is long enough for the test') 269 .beTrue(); 270 271 // Automations to be tested. The first event should be the actual 272 // output up to the event time. The last event should be the final 273 // output from the event time and onwards. 274 testCases.forEach(entry => { 275 should( 276 () => { 277 src.offset[entry.event]( 278 entry.value, eventTime, entry.extraArgs); 279 }, 280 prefix + 281 eventToString( 282 entry.event, entry.value, eventTime, entry.extraArgs)) 283 .notThrow(); 284 }); 285 286 src.start(); 287 288 return context.startRendering(); 289 } 290 291 // Verify output of test where the final value of the automation is 292 // expected to be constant. 293 function expectConstant(prefix, should, eventFrame, testCases) { 294 return audioBuffer => { 295 let audio = audioBuffer.getChannelData(0); 296 297 let eventTime = eventFrame / sampleRate; 298 299 // Compute the expected value of the first automation one frame before 300 // the event time. This is a quick check that the correct automation 301 // was done. 302 let expectedValue = methodMap[testCases[0].event]( 303 (eventFrame - 1) / sampleRate, 1, 0, testCases[0].value, 304 eventTime); 305 should( 306 audio[eventFrame - 1], 307 prefix + 308 `At time ${ 309 (eventFrame - 1) / sampleRate 310 } (frame ${eventFrame - 1}) output`) 311 .beCloseTo(expectedValue, {threshold: testCases[0].relError}); 312 313 // The last event scheduled is expected to set the value for all 314 // future times. Verify that the output has the expected value. 315 should( 316 audio.slice(eventFrame), 317 prefix + 318 `At time ${eventTime} (frame ${ 319 eventFrame 320 }) and later, output`) 321 .beConstantValueOf(testCases[testCases.length - 1].value); 322 }; 323 } 324 325 // Test output when two events of the same time are scheduled at the same 326 // time. 327 function testMultipleSameEvents(should, options) { 328 let {method, prefix, threshold} = options; 329 330 // Context for testing. 331 let context = 332 new OfflineAudioContext({length: 16384, sampleRate: sampleRate}); 333 334 let src = new ConstantSourceNode(context); 335 src.connect(context.destination); 336 337 let initialValue = 1; 338 339 // Informative print 340 should(() => { 341 src.offset.setValueAtTime(initialValue, 0); 342 }, prefix + `setValueAtTime(${initialValue}, 0)`).notThrow(); 343 344 let frame = 64; 345 let time = frame / context.sampleRate; 346 let values = [2, 7, 10]; 347 348 // Schedule two events of the same type at the same time, but with 349 // different values. 350 351 values.forEach(value => { 352 // Informative prints to show what we're doing in this test. 353 should( 354 () => { 355 src.offset[method](value, time); 356 }, 357 prefix + 358 eventToString( 359 method, 360 value, 361 time, 362 )) 363 .notThrow(); 364 }) 365 366 src.start(); 367 368 return context.startRendering().then(audioBuffer => { 369 let actual = audioBuffer.getChannelData(0); 370 371 // The output should be a ramp from time 0 to the event time. But we 372 // only verify the value just before the event time, which should be 373 // fairly close to values[0]. (But compute the actual expected value 374 // to be sure.) 375 let expected = methodMap[method]( 376 (frame - 1) / context.sampleRate, initialValue, 0, values[0], 377 time); 378 should(actual[frame - 1], prefix + `Output at frame ${frame - 1}`) 379 .beCloseTo(expected, {threshold: threshold, precision: 3}); 380 381 // Any other values shouldn't show up in the output. Only the value 382 // from last event should appear. We only check the value at the 383 // event time. 384 should( 385 actual[frame], prefix + `Output at frame ${frame} (${time} sec)`) 386 .beEqualTo(values[values.length - 1]); 387 }); 388 } 389 390 // Convert an automation method to a string for printing. 391 function eventToString(method, value, time, extras) { 392 let string = method + '('; 393 string += (value instanceof Array) ? `[${value}]` : value; 394 string += ', ' + time; 395 if (extras) { 396 string += ', ' + extras; 397 } 398 string += ')'; 399 return string; 400 } 401 402 // Map between the automation method name and a function that computes the 403 // output value of the automation method. 404 const methodMap = { 405 linearRampToValueAtTime: audioParamLinearRamp, 406 exponentialRampToValueAtTime: audioParamExponentialRamp, 407 setValueAtTime: (t, v) => v 408 }; 409 </script> 410 </body> 411 </html>