| <!DOCTYPE html> |
| <html> |
| <head> |
| <title> |
| Test Interpolation for AudioParam.setValueCurveAtTime |
| </title> |
| <script src="../../imported/w3c/web-platform-tests/resources/testharness.js"></script> |
| <script src="../../resources/testharnessreport.js"></script> |
| <script src="../resources/audit-util.js"></script> |
| <script src="../resources/audit.js"></script> |
| <title> |
| Test Interpolation for AudioParam.setValueCurveAtTime |
| </title> |
| </head> |
| <body> |
| <script id="layout-test-code"> |
| // Play a constant signal through a gain node that is automated using |
| // setValueCurveAtTime with a 2-element curve. The output should be a |
| // linear change. |
| |
| // Choose a sample rate that is a multiple of 128, the rendering quantum |
| // size. This makes the math work out to be nice numbers. |
| let sampleRate = 25600; |
| let testDurationSec = 1; |
| let testDurationFrames = testDurationSec * sampleRate; |
| |
| // Where the curve starts and its duration. This MUST be less than the |
| // total rendering time. |
| let curveStartTime = 256 / sampleRate; |
| let curveDuration = 300 / sampleRate; |
| ; |
| let curveValue = 0.75; |
| |
| // At this time, the gain node goes to gain 1. This is used to make sure |
| // the value curve is propagated correctly until the next event. |
| let fullGainTime = 0.75; |
| |
| // Thresholds use to determine if the test passes; these are |
| // experimentally determined. The SNR between the actual and expected |
| // result should be at least |snrThreshold|. The maximum difference |
| // betwen them should not exceed |maxErrorThreshold|. |
| let snrThreshold = 10000; |
| let maxErrorThreshold = 0; |
| |
| let context; |
| let actualResult; |
| let expectedResult; |
| |
| let audit = Audit.createTaskRunner(); |
| |
| // Array of test configs. Each config must specify curveStartTime, |
| // curveDuration, curveLength, fullGainTime, maxErrorThreshold, and |
| // snrThreshold. |
| let testConfigs = [ |
| { |
| // The main test |
| curveStartTime: 256 / sampleRate, |
| curveDuration: 300 / sampleRate, |
| curveLength: 2, |
| fullGainTime: 0.75, |
| maxErrorThreshold: 5.9605e-8, |
| snrThreshold: 171.206 |
| }, |
| { |
| // Increase the curve length |
| curveStartTime: 256 / sampleRate, |
| curveDuration: 300 / sampleRate, |
| curveLength: 3, |
| fullGainTime: 0.75, |
| maxErrorThreshold: 5.9605e-8, |
| snrThreshold: 171.206 |
| }, |
| { |
| // Increase the curve length |
| curveStartTime: 256 / sampleRate, |
| curveDuration: 300 / sampleRate, |
| curveLength: 16, |
| fullGainTime: 0.75, |
| maxErrorThreshold: 5.9605e-8, |
| snrThreshold: 170.892 |
| }, |
| { |
| // Increase the curve length |
| curveStartTime: 256 / sampleRate, |
| curveDuration: 300 / sampleRate, |
| curveLength: 100, |
| fullGainTime: 0.75, |
| maxErrorThreshold: 1.1921e-7, |
| snrThreshold: 168.712 |
| }, |
| { |
| // Corner case with duration less than a frame! |
| curveStartTime: 256 / sampleRate, |
| curveDuration: 0.25 / sampleRate, |
| curveLength: 2, |
| fullGainTime: 0.75, |
| maxErrorThreshold: 0, |
| snrThreshold: 10000 |
| }, |
| { |
| // Short duration test |
| curveStartTime: 256 / sampleRate, |
| curveDuration: 2 / sampleRate, |
| curveLength: 2, |
| fullGainTime: 0.75, |
| maxErrorThreshold: 0, |
| snrThreshold: 10000 |
| }, |
| { |
| // Short duration test with many points. |
| curveStartTime: 256 / sampleRate, |
| curveDuration: 2 / sampleRate, |
| curveLength: 8, |
| fullGainTime: 0.75, |
| maxErrorThreshold: 0, |
| snrThreshold: 10000 |
| }, |
| { |
| // Long duration, big curve |
| curveStartTime: 256 / sampleRate, |
| curveDuration: .5, |
| curveLength: 1000, |
| fullGainTime: 0.75, |
| maxErrorThreshold: 5.9605e-8, |
| snrThreshold: 152.784 |
| } |
| ]; |
| |
| // Creates a function based on the test config that is suitable for use by |
| // defineTask(). |
| function createTaskFunction(config) { |
| return function(task, should) { |
| runTest(should, config).then(() => task.done()); |
| }; |
| } |
| |
| // Define a task for each config, in the order listed in testConfigs. |
| for (let k = 0; k < testConfigs.length; ++k) { |
| let config = testConfigs[k]; |
| let name = k + ':curve=' + config.curveLength + |
| ',duration=' + (config.curveDuration * sampleRate); |
| audit.define(name, createTaskFunction(config)); |
| } |
| |
| // Simple test from crbug.com/441471. Makes sure the end points and the |
| // middle point are interpolated correctly. |
| audit.define('crbug-441471', (task, should) => { |
| // Any sample rate should work; we pick something small such that the |
| // time end points are on a sampling point. |
| let context = new OfflineAudioContext(1, 5000, 5000) |
| |
| // A constant source |
| let source = context.createBufferSource(); |
| source.buffer = createConstantBuffer(context, 1, 1); |
| source.loop = true; |
| |
| let gain = context.createGain(); |
| |
| let startTime = 0.7; |
| let duration = 0.2; |
| |
| // Create the curve. The interpolated result should be just a straight |
| // line from -1 to 1 from time startTime to startTime + duration. |
| |
| let c = new Float32Array(3); |
| c[0] = -1; |
| c[1] = 0; |
| c[2] = 1; |
| gain.gain.setValueCurveAtTime(c, startTime, duration); |
| source.connect(gain); |
| gain.connect(context.destination); |
| source.start(); |
| |
| context.startRendering() |
| .then(function(renderedBuffer) { |
| let data = renderedBuffer.getChannelData(0); |
| let endTime = startTime + duration; |
| let midPoint = (startTime + endTime) / 2; |
| |
| should( |
| data[timeToSampleFrame(startTime, context.sampleRate)], |
| 'Curve value at time ' + startTime) |
| .beEqualTo(c[0]); |
| // Due to round-off, the value at the midpoint is not exactly zero |
| // on arm64. See crbug.com/558563. The current value is |
| // experimentally determined. |
| should( |
| data[timeToSampleFrame(midPoint, context.sampleRate)], |
| 'Curve value at time ' + midPoint) |
| .beCloseTo(0, {threshold: Math.pow(2, -51)}); |
| should( |
| data[timeToSampleFrame(endTime, context.sampleRate)], |
| 'Curve value at time ' + endTime) |
| .beEqualTo(c[2]); |
| }) |
| .then(() => task.done()); |
| }); |
| |
| function runTest(should, config) { |
| context = new OfflineAudioContext(1, testDurationFrames, sampleRate); |
| |
| // A constant audio source of value 1. |
| let source = context.createBufferSource(); |
| source.buffer = createConstantBuffer(context, 1, 1); |
| source.loop = true; |
| |
| // The value curve for testing. Just to make things easy for testing, |
| // make the curve a simple ramp up to curveValue. |
| // TODO(rtoy): Maybe allow more complicated curves? |
| let curve = new Float32Array(config.curveLength); |
| for (let k = 0; k < config.curveLength; ++k) { |
| curve[k] = curveValue / (config.curveLength - 1) * k; |
| } |
| |
| // A gain node that is to be automated using setValueCurveAtTime. |
| let gain = context.createGain(); |
| gain.gain.value = 0; |
| gain.gain.setValueCurveAtTime( |
| curve, config.curveStartTime, config.curveDuration); |
| // This is to verify that setValueCurveAtTime ends appropriately. |
| gain.gain.setValueAtTime(1, config.fullGainTime); |
| |
| source.connect(gain); |
| gain.connect(context.destination); |
| source.start(); |
| |
| // Some consistency checks on the test parameters |
| let prefix = 'Length ' + config.curveLength + ', duration ' + |
| config.curveDuration; |
| should( |
| config.curveStartTime + config.curveDuration, |
| prefix + ': Check: Curve end time') |
| .beLessThanOrEqualTo(testDurationSec); |
| should(config.fullGainTime, prefix + ': Check: Full gain start time') |
| .beLessThanOrEqualTo(testDurationSec); |
| should(config.fullGainTime, prefix + ': Check: Full gain start time') |
| .beGreaterThanOrEqualTo( |
| config.curveStartTime + config.curveDuration); |
| |
| // Rock and roll! |
| return context.startRendering().then(checkResult(should, config)); |
| } |
| |
| // Return a function to check that the rendered result matches the |
| // expected result. |
| function checkResult(should, config) { |
| return function(renderedBuffer) { |
| let success = true; |
| |
| actualResult = renderedBuffer.getChannelData(0); |
| expectedResult = computeExpectedResult(config); |
| |
| // Compute the SNR and max absolute difference between the actual and |
| // expected result. |
| let SNR = 10 * Math.log10(computeSNR(actualResult, expectedResult)); |
| let maxDiff = -1; |
| let posn = -1; |
| |
| for (let k = 0; k < actualResult.length; ++k) { |
| let diff = Math.abs(actualResult[k] - expectedResult[k]); |
| if (maxDiff < diff) { |
| maxDiff = diff; |
| posn = k; |
| } |
| } |
| |
| let prefix = 'Curve length ' + config.curveLength + ', duration ' + |
| config.curveDuration; |
| should(SNR, prefix + ': SNR') |
| .beGreaterThanOrEqualTo(config.snrThreshold); |
| |
| should(maxDiff, prefix + ': Max difference') |
| .beLessThanOrEqualTo(config.maxErrorThreshold); |
| } |
| } |
| |
| // Compute the expected result based on the config settings. |
| function computeExpectedResult(config) { |
| // The automation curve starts at |curveStartTime| and has duration |
| // |curveDuration|. So, the output should be zero until curveStartTime, |
| // linearly ramp up from there to |curveValue|, and then be constant 1 |
| // from then to the end of the buffer. |
| |
| let expected = new Float32Array(testDurationFrames); |
| |
| let curveStartFrame = config.curveStartTime * sampleRate; |
| let curveEndFrame = |
| (config.curveStartTime + config.curveDuration) * sampleRate; |
| let fullGainFrame = config.fullGainTime * sampleRate; |
| |
| let k; |
| |
| // Zero out the start. |
| for (k = 0; k < curveStartFrame; ++k) |
| expected[k] = 0; |
| |
| // Linearly ramp now. This assumes that the actual curve used is a |
| // linear ramp, even if there are many curve points. |
| let stepSize = curveValue / (config.curveDuration * sampleRate); |
| for (; k < curveEndFrame; ++k) |
| expected[k] = stepSize * (k - curveStartFrame); |
| |
| // Hold it constant until the next event |
| for (; k < fullGainFrame; ++k) |
| expected[k] = curveValue; |
| |
| // Amplitude is one for the rest of the test. |
| for (; k < testDurationFrames; ++k) |
| expected[k] = 1; |
| |
| return expected; |
| } |
| |
| audit.run(); |
| </script> |
| </body> |
| </html> |