OLD | NEW |
(Empty) | |
| 1 <!doctype html> |
| 2 <html> |
| 3 <head> |
| 4 <script src="../../resources/testharness.js"></script> |
| 5 <script src="../../resources/testharnessreport.js"></script> |
| 6 <script src="../resources/audit-util.js"></script> |
| 7 <script src="../resources/audit.js"></script> |
| 8 <title>Test CancelValuesAndHoldAtTime</title> |
| 9 </head> |
| 10 |
| 11 <body> |
| 12 <script> |
| 13 let sampleRate = 48000; |
| 14 let renderDuration = 0.5; |
| 15 |
| 16 let audit = Audit.createTaskRunner(); |
| 17 |
| 18 // The first few tasks test the cancellation of each relevant automation |
| 19 // function. For the test, a simple linear ramp from 0 to 1 is used to |
| 20 // start things off. Then the automation to be tested is scheduled and |
| 21 // cancelled. |
| 22 |
| 23 audit.define("linear", function (task, should) { |
| 24 task.describe("Cancel linearRampToValueAtTime"); |
| 25 cancelTest(should, linearRampTest("linearRampToValueAtTime"), { |
| 26 valueThreshold: 8.3998e-5, |
| 27 curveThreshold: 0 |
| 28 }).then(task.done.bind(task)); |
| 29 }); |
| 30 |
| 31 audit.define("exponential", function (task, should) { |
| 32 task.describe("Cancel exponentialRampAtTime"); |
| 33 // Cancel an exponential ramp. The thresholds are experimentally |
| 34 // determined. |
| 35 cancelTest(should, function (g, v0, t0, cancelTime) { |
| 36 // Initialize values to 0. |
| 37 g[0].gain.setValueAtTime(0, 0); |
| 38 g[1].gain.setValueAtTime(0, 0); |
| 39 // Schedule a short linear ramp to start things off. |
| 40 g[0].gain.linearRampToValueAtTime(v0, t0); |
| 41 g[1].gain.linearRampToValueAtTime(v0, t0); |
| 42 |
| 43 // After the linear ramp, schedule an exponential ramp to the end. |
| 44 // (This is the event that will be be cancelled.) |
| 45 let v1 = 0.001; |
| 46 let t1 = renderDuration; |
| 47 |
| 48 g[0].gain.exponentialRampToValueAtTime(v1, t1); |
| 49 g[1].gain.exponentialRampToValueAtTime(v1, t1); |
| 50 |
| 51 expectedConstant = Math.fround(v0 * Math.pow(v1 / v0, ( |
| 52 cancelTime - |
| 53 t0) / (t1 - t0))); |
| 54 return { |
| 55 expectedConstant: expectedConstant, |
| 56 autoMessage: "exponentialRampToValue(" + v1 + ", " + t1 + ")", |
| 57 summary: "exponentialRampToValueAtTime", |
| 58 }; |
| 59 }, { |
| 60 valueThreshold: 1.8664e-6, |
| 61 curveThreshold: 5.9605e-8 |
| 62 }).then(task.done.bind(task)); |
| 63 }); |
| 64 |
| 65 audit.define("setTarget", function (task, should) { |
| 66 task.describe("Cancel setTargetAtTime"); |
| 67 // Cancel a setTarget event. |
| 68 cancelTest(should, function (g, v0, t0, cancelTime) { |
| 69 // Initialize values to 0. |
| 70 g[0].gain.setValueAtTime(0, 0); |
| 71 g[1].gain.setValueAtTime(0, 0); |
| 72 // Schedule a short linear ramp to start things off. |
| 73 g[0].gain.linearRampToValueAtTime(v0, t0); |
| 74 g[1].gain.linearRampToValueAtTime(v0, t0); |
| 75 |
| 76 // At the end of the linear ramp, schedule a setTarget. (This is the |
| 77 // event that will be cancelled.) |
| 78 let v1 = 0; |
| 79 let t1 = t0; |
| 80 let timeConstant = 0.05; |
| 81 |
| 82 g[0].gain.setTargetAtTime(v1, t1, timeConstant); |
| 83 g[1].gain.setTargetAtTime(v1, t1, timeConstant); |
| 84 |
| 85 expectedConstant = Math.fround(v1 + (v0 - v1) * Math.exp(-( |
| 86 cancelTime - t0) / timeConstant)); |
| 87 return { |
| 88 expectedConstant: expectedConstant, |
| 89 autoMessage: "setTargetAtTime(" + v1 + ", " + t1 + ", " + |
| 90 timeConstant + ")", |
| 91 summary: "setTargetAtTime", |
| 92 }; |
| 93 }, { |
| 94 valueThreshold: 4.5267e-7, //1.1317e-7, |
| 95 curveThreshold: 0 |
| 96 }).then(task.done.bind(task)); |
| 97 }); |
| 98 |
| 99 audit.define("setValueCurve", function (task, should) { |
| 100 task.describe("Cancel setValueCurveAtTime"); |
| 101 // Cancel a setValueCurve event. |
| 102 cancelTest(should, function (g, v0, t0, cancelTime) { |
| 103 // Initialize values to 0. |
| 104 g[0].gain.setValueAtTime(0, 0); |
| 105 g[1].gain.setValueAtTime(0, 0); |
| 106 // Schedule a short linear ramp to start things off. |
| 107 g[0].gain.linearRampToValueAtTime(v0, t0); |
| 108 g[1].gain.linearRampToValueAtTime(v0, t0); |
| 109 |
| 110 // After the linear ramp, schedule a setValuesCurve. (This is the |
| 111 // event that will be cancelled.) |
| 112 let v1 = 0; |
| 113 let duration = renderDuration - t0; |
| 114 |
| 115 // For simplicity, a 2-point curve so we get a linear interpolated res
ult. |
| 116 let curve = Float32Array.from([v0, 0]); |
| 117 |
| 118 g[0].gain.setValueCurveAtTime(curve, t0, duration); |
| 119 g[1].gain.setValueCurveAtTime(curve, t0, duration); |
| 120 |
| 121 let index = Math.floor((curve.length - 1) / duration * ( |
| 122 cancelTime - t0)); |
| 123 |
| 124 let curvePointsPerFrame = (curve.length - 1) / duration / |
| 125 sampleRate; |
| 126 let virtualIndex = (cancelTime - t0) * sampleRate * |
| 127 curvePointsPerFrame; |
| 128 |
| 129 let delta = virtualIndex - index; |
| 130 expectedConstant = curve[0] + (curve[1] - curve[0]) * delta; |
| 131 return { |
| 132 expectedConstant: expectedConstant, |
| 133 autoMessage: "setValueCurveAtTime([" + curve + "], " + t0 + |
| 134 ", " + duration + |
| 135 ")", |
| 136 summary: "setValueCurveAtTime", |
| 137 }; |
| 138 }, { |
| 139 valueThreshold: 9.5368e-9, |
| 140 curveThreshold: 0 |
| 141 }).then(task.done.bind(task)); |
| 142 }); |
| 143 |
| 144 audit.define("setValueCurve after end", function (task, should) { |
| 145 task.describe("Cancel setValueCurveAtTime after the end"); |
| 146 cancelTest(should, function (g, v0, t0, cancelTime) { |
| 147 // Initialize values to 0. |
| 148 g[0].gain.setValueAtTime(0, 0); |
| 149 g[1].gain.setValueAtTime(0, 0); |
| 150 // Schedule a short linear ramp to start things off. |
| 151 g[0].gain.linearRampToValueAtTime(v0, t0); |
| 152 g[1].gain.linearRampToValueAtTime(v0, t0); |
| 153 |
| 154 // After the linear ramp, schedule a setValuesCurve. (This is the |
| 155 // event that will be cancelled.) Make sure the curve ends before the |
| 156 // cancellation time. |
| 157 let v1 = 0; |
| 158 let duration = cancelTime - t0 - 0.125; |
| 159 |
| 160 // For simplicity, a 2-point curve so we get a linear interpolated |
| 161 // result. |
| 162 let curve = Float32Array.from([v0, 0]); |
| 163 |
| 164 g[0].gain.setValueCurveAtTime(curve, t0, duration); |
| 165 g[1].gain.setValueCurveAtTime(curve, t0, duration); |
| 166 |
| 167 expectedConstant = curve[1]; |
| 168 return { |
| 169 expectedConstant: expectedConstant, |
| 170 autoMessage: "setValueCurveAtTime([" + curve + "], " + t0 + |
| 171 ", " + duration + |
| 172 ")", |
| 173 summary: "setValueCurveAtTime", |
| 174 }; |
| 175 }, { |
| 176 valueThreshold: 0, |
| 177 curveThreshold: 0 |
| 178 }).then(task.done.bind(task)); |
| 179 }); |
| 180 |
| 181 // Special case where we schedule a setTarget and there is no earlier |
| 182 // automation event. This tests that we pick up the starting point |
| 183 // correctly from the last setting of the AudioParam value attribute. |
| 184 |
| 185 |
| 186 audit.define("initial setTarget", function (task, should) { |
| 187 task.describe("Cancel with initial setTargetAtTime"); |
| 188 cancelTest(should, function (g, v0, t0, cancelTime) { |
| 189 let v1 = 0; |
| 190 let timeConstant = 0.1; |
| 191 g[0].gain.value = 1; |
| 192 g[0].gain.setTargetAtTime(v1, t0, timeConstant); |
| 193 g[1].gain.value = 1; |
| 194 g[1].gain.setTargetAtTime(v1, t0, timeConstant); |
| 195 |
| 196 let expectedConstant = Math.fround(v1 + (v0 - v1) * Math.exp(- |
| 197 (cancelTime - t0) / |
| 198 timeConstant)); |
| 199 |
| 200 return { |
| 201 expectedConstant: expectedConstant, |
| 202 autoMessage: "setTargetAtTime(" + v1 + ", " + t0 + ", " + |
| 203 timeConstant + ")", |
| 204 summary: "Initial setTargetAtTime", |
| 205 }; |
| 206 }, { |
| 207 valueThreshold: 1.2320e-6, |
| 208 curveThreshold: 0 |
| 209 }).then(task.done.bind(task)); |
| 210 }); |
| 211 |
| 212 // Test automations scheduled after the call to cancelValuesAndHoldAtTime. |
| 213 // Very similar to the above tests, but we also schedule an event after |
| 214 // cancelValuesAndHoldAtTime and verify that curve after cancellation has |
| 215 // the correct values. |
| 216 |
| 217 audit.define("post cancel: Linear", function (task, should) { |
| 218 // Run the cancel test using a linearRamp as the event to be cancelled. |
| 219 // Then schedule another linear ramp after the cancellation. |
| 220 task.describe("LinearRamp after cancelling"); |
| 221 cancelTest(should, linearRampTest( |
| 222 "Post cancellation linearRampToValueAtTime"), { |
| 223 valueThreshold: 8.3998e-5, |
| 224 curveThreshold: 0 |
| 225 }, function (g, cancelTime, expectedConstant) { |
| 226 // Schedule the linear ramp on g[0], and do the same for g[2], using t
he starting point |
| 227 // given by expectedConstant. |
| 228 let v2 = 2; |
| 229 let t2 = cancelTime + 0.125; |
| 230 g[0].gain.linearRampToValueAtTime(v2, t2); |
| 231 g[2].gain.setValueAtTime(expectedConstant, cancelTime); |
| 232 g[2].gain.linearRampToValueAtTime(v2, t2); |
| 233 return { |
| 234 constantEndTime: cancelTime, |
| 235 message: "Post linearRamp(" + v2 + ", " + t2 + ")" |
| 236 }; |
| 237 }).then(task.done.bind(task)); |
| 238 }); |
| 239 |
| 240 audit.define("post cancel: Exponential", function (task, should) { |
| 241 task.describe("ExponentialRamp after cancelling"); |
| 242 // Run the cancel test using a linearRamp as the event to be cancelled. |
| 243 // Then schedule an exponential ramp after the cancellation. |
| 244 cancelTest(should, linearRampTest( |
| 245 "Post cancel exponentialRampToValueAtTime"), { |
| 246 valueThreshold: 8.3998e-5, |
| 247 curveThreshold: 0 |
| 248 }, function (g, cancelTime, expectedConstant) { |
| 249 // Schedule the exponential ramp on g[0], and do the same for g[2], |
| 250 // using the starting point given by expectedConstant. |
| 251 let v2 = 2; |
| 252 let t2 = cancelTime + 0.125; |
| 253 g[0].gain.exponentialRampToValueAtTime(v2, t2); |
| 254 g[2].gain.setValueAtTime(expectedConstant, cancelTime); |
| 255 g[2].gain.exponentialRampToValueAtTime(v2, t2); |
| 256 return { |
| 257 constantEndTime: cancelTime, |
| 258 message: "Post exponentialRamp(" + v2 + ", " + t2 + ")" |
| 259 }; |
| 260 }).then(task.done.bind(task)); |
| 261 }); |
| 262 |
| 263 audit.define("post cancel: ValueCurve", function (task, should) { |
| 264 // Run the cancel test using a linearRamp as the event to be cancelled. |
| 265 // Then schedule a setValueCurve after the cancellation. |
| 266 cancelTest(should, linearRampTest("Post cancel setValueCurveAtTime"), { |
| 267 valueThreshold: 8.3998e-5, |
| 268 curveThreshold: 0 |
| 269 }, function (g, cancelTime, expectedConstant) { |
| 270 // Schedule the exponential ramp on g[0], and do the same for g[2], |
| 271 // using the starting point given by expectedConstant. |
| 272 let t2 = cancelTime + 0.125; |
| 273 let duration = 0.125; |
| 274 let curve = Float32Array.from([.125, 2]); |
| 275 g[0].gain.setValueCurveAtTime(curve, t2, duration); |
| 276 g[2].gain.setValueAtTime(expectedConstant, cancelTime); |
| 277 g[2].gain.setValueCurveAtTime(curve, t2, duration); |
| 278 return { |
| 279 constantEndTime: cancelTime, |
| 280 message: "Post setValueCurve([" + curve + "], " + t2 + ", " + |
| 281 duration + ")", |
| 282 errorThreshold: 8.3998e-5 |
| 283 }; |
| 284 }).then(task.done.bind(task)); |
| 285 }); |
| 286 |
| 287 audit.define("post cancel: setTarget", function (task, should) { |
| 288 // Run the cancel test using a linearRamp as the event to be cancelled. |
| 289 // Then schedule a setTarget after the cancellation. |
| 290 cancelTest(should, linearRampTest("Post cancel setTargetAtTime"), { |
| 291 valueThreshold: 8.3998e-5, |
| 292 curveThreshold: 0 |
| 293 }, function (g, cancelTime, expectedConstant) { |
| 294 // Schedule the exponential ramp on g[0], and do the same for g[2], |
| 295 // using the starting point given by expectedConstant. |
| 296 let v2 = 0.125; |
| 297 let t2 = cancelTime + 0.125; |
| 298 let timeConstant = 0.1; |
| 299 g[0].gain.setTargetAtTime(v2, t2, timeConstant); |
| 300 g[2].gain.setValueAtTime(expectedConstant, cancelTime); |
| 301 g[2].gain.setTargetAtTime(v2, t2, timeConstant); |
| 302 return { |
| 303 constantEndTime: cancelTime + 0.125, |
| 304 message: "Post setTargetAtTime(" + v2 + ", " + t2 + ", " + |
| 305 timeConstant + ")", |
| 306 errorThreshold: 8.4037e-5 |
| 307 }; |
| 308 }).then(task.done.bind(task)); |
| 309 }); |
| 310 |
| 311 audit.define("post cancel: setValue", function (task, should) { |
| 312 // Run the cancel test using a linearRamp as the event to be cancelled. |
| 313 // Then schedule a setTarget after the cancellation. |
| 314 cancelTest(should, linearRampTest("Post cancel setValueAtTime"), { |
| 315 valueThreshold: 8.3998e-5, |
| 316 curveThreshold: 0 |
| 317 }, function (g, cancelTime, expectedConstant) { |
| 318 // Schedule the exponential ramp on g[0], and do the same for g[2], |
| 319 // using the starting point given by expectedConstant. |
| 320 let v2 = 0.125; |
| 321 let t2 = cancelTime + 0.125; |
| 322 g[0].gain.setValueAtTime(v2, t2); |
| 323 g[2].gain.setValueAtTime(expectedConstant, cancelTime); |
| 324 g[2].gain.setValueAtTime(v2, t2); |
| 325 return { |
| 326 constantEndTime: cancelTime + 0.125, |
| 327 message: "Post setValueAtTime(" + v2 + ", " + t2 + ")" |
| 328 }; |
| 329 }).then(task.done.bind(task)); |
| 330 }); |
| 331 |
| 332 audit.run(); |
| 333 |
| 334 // Common function for doing a linearRamp test. This just does a linear |
| 335 // ramp from 0 to v0 at from time 0 to t0. Then another linear ramp is |
| 336 // scheduled from v0 to 0 from time t0 to t1. This is the ramp that is to |
| 337 // be cancelled. |
| 338 function linearRampTest(message) { |
| 339 return function (g, v0, t0, cancelTime) { |
| 340 g[0].gain.setValueAtTime(0, 0); |
| 341 g[1].gain.setValueAtTime(0, 0); |
| 342 g[0].gain.linearRampToValueAtTime(v0, t0); |
| 343 g[1].gain.linearRampToValueAtTime(v0, t0); |
| 344 |
| 345 let v1 = 0; |
| 346 let t1 = renderDuration; |
| 347 g[0].gain.linearRampToValueAtTime(v1, t1); |
| 348 g[1].gain.linearRampToValueAtTime(v1, t1); |
| 349 |
| 350 expectedConstant = Math.fround(v0 + (v1 - v0) * (cancelTime - t0) / |
| 351 (t1 - t0)); |
| 352 |
| 353 return { |
| 354 expectedConstant: expectedConstant, |
| 355 autoMessage: "linearRampToValue(" + v1 + ", " + t1 + ")", |
| 356 summary: message, |
| 357 }; |
| 358 } |
| 359 } |
| 360 |
| 361 // Run the cancellation test. A set of automations is created and |
| 362 // canceled. |
| 363 // |
| 364 // |testFunction| is a function that generates the automation to be |
| 365 // tested. It is given an array of 3 gain nodes, the value and time of an |
| 366 // initial linear ramp, and the time where the cancellation should occur. |
| 367 // The function must do the automations for the first two gain nodes. It |
| 368 // must return a dictionary with |expectedConstant| being the value at the |
| 369 // cancellation time, |autoMessage| for message to describe the test, and |
| 370 // |summary| for general summary message to be printed at the end of the |
| 371 // test. |
| 372 // |
| 373 // |thresholdOptions| is a property bag that specifies the error threshold |
| 374 // to use. |thresholdOptions.valueThreshold| is the error threshold for |
| 375 // comparing the actual constant output after cancelling to the expected |
| 376 // value. |thresholdOptions.curveThreshold| is the error threshold for |
| 377 // comparing the actual and expected automation curves before the |
| 378 // cancelation point. |
| 379 // |
| 380 // For cancellation tests, |postCancelTest| is a function that schedules |
| 381 // some automation after the cancellation. It takes 3 arguments: an array |
| 382 // of the gain nodes, the cancellation time, and the expected value at the |
| 383 // cancellation time. This function must return a dictionary consisting |
| 384 // of |constantEndtime| indicating when the held constant from |
| 385 // cancellation stops being constant, |message| giving a summary of what |
| 386 // automation is being used, and |errorThreshold| that is the error |
| 387 // threshold between the expected curve and the actual curve. |
| 388 // |
| 389 function cancelTest(should, testerFunction, thresholdOptions, |
| 390 postCancelTest) { |
| 391 // Create a context with three channels. Channel 0 is the test channel |
| 392 // containing the actual output that includes the cancellation of |
| 393 // events. Channel 1 is the expected data upto the cancellation so we |
| 394 // can verify the cancellation produced the correct result. Channel 2 |
| 395 // is for verifying events inserted after the cancellation so we can |
| 396 // verify that automations are correctly generated after the |
| 397 // cancellation point. |
| 398 let context = new OfflineAudioContext(3, renderDuration * sampleRate, |
| 399 sampleRate); |
| 400 |
| 401 // Test source is a constant signal |
| 402 let src = context.createBufferSource(); |
| 403 src.buffer = createConstantBuffer(context, 1, 1); |
| 404 src.loop = true; |
| 405 |
| 406 // We'll do the automation tests with three gain nodes. One (g0) will |
| 407 // have cancelValuesAndHoldAtTime and the other (g1) will not. g1 is |
| 408 // used as the expected result for that automation up to the |
| 409 // cancellation point. They should be the same. The third node (g2) is |
| 410 // used for testing automations inserted after the cancellation point, |
| 411 // if any. g2 is the expected result from the cancellation point to the |
| 412 // end of the test. |
| 413 |
| 414 let g0 = context.createGain(); |
| 415 let g1 = context.createGain(); |
| 416 let g2 = context.createGain(); |
| 417 let v0 = 1; |
| 418 let t0 = 0.01; |
| 419 |
| 420 let cancelTime = renderDuration / 2; |
| 421 |
| 422 // Test automation here. The tester function is responsible for setting |
| 423 // up the gain nodes with the desired automation for testing. |
| 424 autoResult = testerFunction([g0, g1, g2], v0, t0, cancelTime); |
| 425 let expectedConstant = autoResult.expectedConstant; |
| 426 let autoMessage = autoResult.autoMessage; |
| 427 let summaryMessage = autoResult.summary; |
| 428 |
| 429 // Cancel scheduled events somewhere in the middle of the test |
| 430 // automation. |
| 431 g0.gain.cancelValuesAndHoldAtTime(cancelTime); |
| 432 |
| 433 let constantEndTime; |
| 434 if (postCancelTest) { |
| 435 postResult = postCancelTest([g0, g1, g2], cancelTime, |
| 436 expectedConstant); |
| 437 constantEndTime = postResult.constantEndTime; |
| 438 } |
| 439 |
| 440 // Connect everything together (with a merger to make a two-channel |
| 441 // result). Channel 0 is the test (with cancelValuesAndHoldAtTime) and |
| 442 // channel 1 is the reference (without cancelValuesAndHoldAtTime). |
| 443 // Channel 1 is used to verify that everything up to the cancellation |
| 444 // has the correct values. |
| 445 src.connect(g0); |
| 446 src.connect(g1); |
| 447 src.connect(g2); |
| 448 let merger = context.createChannelMerger(3); |
| 449 g0.connect(merger, 0, 0); |
| 450 g1.connect(merger, 0, 1); |
| 451 g2.connect(merger, 0, 2); |
| 452 merger.connect(context.destination); |
| 453 |
| 454 // Go! |
| 455 src.start(); |
| 456 |
| 457 return context.startRendering().then(function (buffer) { |
| 458 let actual = buffer.getChannelData(0); |
| 459 let expected = buffer.getChannelData(1); |
| 460 |
| 461 // The actual output should be a constant from the cancel time to the |
| 462 // end. We use the last value of the actual output as the constant, |
| 463 // but we also want to compare that with what we thought it should |
| 464 // really be. |
| 465 |
| 466 let cancelFrame = Math.ceil(cancelTime * sampleRate); |
| 467 |
| 468 // Verify that the curves up to the cancel time are "identical". The |
| 469 // should be but round-off may make them differ slightly due to the |
| 470 // way cancelling is done. |
| 471 let endFrame = Math.floor(cancelTime * sampleRate); |
| 472 should(actual.slice(0, endFrame), |
| 473 autoMessage + " up to time " + cancelTime) |
| 474 .beCloseToArray(expected.slice(0, endFrame), { |
| 475 absoluteThreshold: thresholdOptions.curveThreshold |
| 476 }); |
| 477 |
| 478 // Verify the output after the cancellation is a constant. |
| 479 let actualTail; |
| 480 let constantEndFrame; |
| 481 |
| 482 if (postCancelTest) { |
| 483 constantEndFrame = Math.ceil(constantEndTime * sampleRate); |
| 484 actualTail = actual.slice(cancelFrame, constantEndFrame); |
| 485 } else { |
| 486 actualTail = actual.slice(cancelFrame); |
| 487 } |
| 488 |
| 489 let actualConstant = actual[cancelFrame]; |
| 490 |
| 491 should(actualTail, "Cancelling " + autoMessage + " at time " + |
| 492 cancelTime) |
| 493 .beConstantValueOf(actualConstant); |
| 494 |
| 495 // Verify that the constant is the value we expect. |
| 496 should(actualConstant, "Expected value for cancelling " + |
| 497 autoMessage + " at time " + |
| 498 cancelTime) |
| 499 .beCloseTo(expectedConstant, { |
| 500 threshold: thresholdOptions.valueThreshold |
| 501 }); |
| 502 |
| 503 // Verify the curve after the constantEndTime matches our |
| 504 // expectations. |
| 505 if (postCancelTest) { |
| 506 let c2 = buffer.getChannelData(2); |
| 507 should(actual.slice(constantEndFrame), postResult.message) |
| 508 .beCloseToArray(c2.slice(constantEndFrame), { |
| 509 absoluteThreshold: postResult.errorThreshold || 0 |
| 510 }); |
| 511 } |
| 512 }); |
| 513 } |
| 514 </script> |
| 515 </body> |
| 516 </html> |
OLD | NEW |