| <!doctype html> |
| <html> |
| <head> |
| <title> |
| Test Handling of Event Insertion |
| </title> |
| <script src="/resources/testharness.js"></script> |
| <script src="/resources/testharnessreport.js"></script> |
| <script src="/webaudio/resources/audit-util.js"></script> |
| <script src="/webaudio/resources/audit.js"></script> |
| <script src="/webaudio/resources/audio-param.js"></script> |
| </head> |
| <body> |
| <script id="layout-test-code"> |
| let audit = Audit.createTaskRunner(); |
| |
| // Use a power of two for the sample rate so there's no round-off in |
| // computing time from frame. |
| let sampleRate = 16384; |
| |
| audit.define( |
| {label: 'Insert same event at same time'}, (task, should) => { |
| // Context for testing. |
| let context = new OfflineAudioContext( |
| {length: 16384, sampleRate: sampleRate}); |
| |
| // The source node to use. Automations will be scheduled here. |
| let src = new ConstantSourceNode(context, {offset: 0}); |
| src.connect(context.destination); |
| |
| // An array of tests to be done. Each entry specifies the event |
| // type and the event time. The events are inserted in the order |
| // given (in |values|), and the second event should be inserted |
| // after the first one, as required by the spec. |
| let testCases = [ |
| { |
| event: 'setValueAtTime', |
| frame: RENDER_QUANTUM_FRAMES, |
| values: [99, 1], |
| outputTestFrame: RENDER_QUANTUM_FRAMES, |
| expectedOutputValue: 1 |
| }, |
| { |
| event: 'linearRampToValueAtTime', |
| frame: 2 * RENDER_QUANTUM_FRAMES, |
| values: [99, 2], |
| outputTestFrame: 2 * RENDER_QUANTUM_FRAMES, |
| expectedOutputValue: 2 |
| }, |
| { |
| event: 'exponentialRampToValueAtTime', |
| frame: 3 * RENDER_QUANTUM_FRAMES, |
| values: [99, 3], |
| outputTestFrame: 3 * RENDER_QUANTUM_FRAMES, |
| expectedOutputValue: 3 |
| }, |
| { |
| event: 'setValueCurveAtTime', |
| frame: 3 * RENDER_QUANTUM_FRAMES, |
| values: [[3, 4]], |
| extraArgs: RENDER_QUANTUM_FRAMES / context.sampleRate, |
| outputTestFrame: 4 * RENDER_QUANTUM_FRAMES, |
| expectedOutputValue: 4 |
| }, |
| { |
| event: 'setValueAtTime', |
| frame: 5 * RENDER_QUANTUM_FRAMES - 1, |
| values: [99, 1, 5], |
| outputTestFrame: 5 * RENDER_QUANTUM_FRAMES, |
| expectedOutputValue: 5 |
| } |
| ]; |
| |
| testCases.forEach(entry => { |
| entry.values.forEach(value => { |
| let eventTime = entry.frame / context.sampleRate; |
| let message = eventToString( |
| entry.event, value, eventTime, entry.extraArgs); |
| // This is mostly to print out the event that is getting |
| // inserted. It should never ever throw. |
| should(() => { |
| src.offset[entry.event](value, eventTime, entry.extraArgs); |
| }, message).notThrow(); |
| }); |
| }); |
| |
| src.start(); |
| |
| context.startRendering() |
| .then(audioBuffer => { |
| let audio = audioBuffer.getChannelData(0); |
| |
| // Look through the test cases to figure out what the correct |
| // output values should be. |
| testCases.forEach(entry => { |
| let expected = entry.expectedOutputValue; |
| let frame = entry.outputTestFrame; |
| let time = frame / context.sampleRate; |
| should( |
| audio[frame], `Output at frame ${frame} (time ${time})`) |
| .beEqualTo(expected); |
| }); |
| }) |
| .then(() => task.done()); |
| }); |
| |
| audit.define( |
| { |
| label: 'Linear + Expo', |
| description: 'Different events at same time' |
| }, |
| (task, should) => { |
| // Should be a linear ramp up to the event time, and after a |
| // constant value because the exponential ramp has ended. |
| let testCase = [ |
| {event: 'linearRampToValueAtTime', value: 2, relError: 0}, |
| {event: 'setValueAtTime', value: 99}, |
| {event: 'exponentialRampToValueAtTime', value: 3}, |
| ]; |
| let eventFrame = 2 * RENDER_QUANTUM_FRAMES; |
| let prefix = 'Linear+Expo: '; |
| |
| testEventInsertion(prefix, should, eventFrame, testCase) |
| .then(expectConstant(prefix, should, eventFrame, testCase)) |
| .then(() => task.done()); |
| }); |
| |
| audit.define( |
| { |
| label: 'Expo + Linear', |
| description: 'Different events at same time', |
| }, |
| (task, should) => { |
| // Should be an exponential ramp up to the event time, and after a |
| // constant value because the linear ramp has ended. |
| let testCase = [ |
| { |
| event: 'exponentialRampToValueAtTime', |
| value: 3, |
| relError: 4.2533e-6 |
| }, |
| {event: 'setValueAtTime', value: 99}, |
| {event: 'linearRampToValueAtTime', value: 2}, |
| ]; |
| let eventFrame = 2 * RENDER_QUANTUM_FRAMES; |
| let prefix = 'Expo+Linear: '; |
| |
| testEventInsertion(prefix, should, eventFrame, testCase) |
| .then(expectConstant(prefix, should, eventFrame, testCase)) |
| .then(() => task.done()); |
| }); |
| |
| audit.define( |
| { |
| label: 'Linear + SetTarget', |
| description: 'Different events at same time', |
| }, |
| (task, should) => { |
| // Should be a linear ramp up to the event time, and then a |
| // decaying value. |
| let testCase = [ |
| {event: 'linearRampToValueAtTime', value: 3, relError: 0}, |
| {event: 'setValueAtTime', value: 100}, |
| {event: 'setTargetAtTime', value: 0, extraArgs: 0.1}, |
| ]; |
| let eventFrame = 2 * RENDER_QUANTUM_FRAMES; |
| let prefix = 'Linear+SetTarget: '; |
| |
| testEventInsertion(prefix, should, eventFrame, testCase) |
| .then(audioBuffer => { |
| let audio = audioBuffer.getChannelData(0); |
| let prefix = 'Linear+SetTarget: '; |
| let eventTime = eventFrame / sampleRate; |
| let expectedValue = methodMap[testCase[0].event]( |
| (eventFrame - 1) / sampleRate, 1, 0, testCase[0].value, |
| eventTime); |
| should( |
| audio[eventFrame - 1], |
| prefix + |
| `At time ${ |
| (eventFrame - 1) / sampleRate |
| } (frame ${eventFrame - 1}) output`) |
| .beCloseTo( |
| expectedValue, |
| {threshold: testCase[0].relError || 0}); |
| |
| // The setValue should have taken effect |
| should( |
| audio[eventFrame], |
| prefix + |
| `At time ${eventTime} (frame ${eventFrame}) output`) |
| .beEqualTo(testCase[1].value); |
| |
| // The final event is setTarget. Compute the expected output. |
| let actual = audio.slice(eventFrame); |
| let expected = new Float32Array(actual.length); |
| for (let k = 0; k < expected.length; ++k) { |
| let t = (eventFrame + k) / sampleRate; |
| expected[k] = audioParamSetTarget( |
| t, testCase[1].value, eventTime, testCase[2].value, |
| testCase[2].extraArgs); |
| } |
| should( |
| actual, |
| prefix + |
| `At time ${eventTime} (frame ${ |
| eventFrame |
| }) and later`) |
| .beCloseToArray(expected, {relativeThreshold: 2.6694e-7}); |
| }) |
| .then(() => task.done()); |
| }); |
| |
| audit.define( |
| { |
| label: 'Multiple linear ramps at the same time', |
| description: 'Verify output' |
| }, |
| (task, should) => { |
| testMultipleSameEvents(should, { |
| method: 'linearRampToValueAtTime', |
| prefix: 'Multiple linear ramps: ', |
| threshold: 0 |
| }).then(() => task.done()); |
| }); |
| |
| audit.define( |
| { |
| label: 'Multiple exponential ramps at the same time', |
| description: 'Verify output' |
| }, |
| (task, should) => { |
| testMultipleSameEvents(should, { |
| method: 'exponentialRampToValueAtTime', |
| prefix: 'Multiple exponential ramps: ', |
| threshold: 5.3924e-7 |
| }).then(() => task.done()); |
| }); |
| |
| audit.run(); |
| |
| // Takes a list of |testCases| consisting of automation methods and |
| // schedules them to occur at |eventFrame|. |prefix| is a prefix for |
| // messages produced by |should|. |
| // |
| // Each item in |testCases| is a dictionary with members: |
| // event - the name of automation method to be inserted, |
| // value - the value for the event, |
| // extraArgs - extra arguments if the event needs more than the value |
| // and time (such as setTargetAtTime). |
| function testEventInsertion(prefix, should, eventFrame, testCases) { |
| let context = new OfflineAudioContext( |
| {length: 4 * RENDER_QUANTUM_FRAMES, sampleRate: sampleRate}); |
| |
| // The source node to use. Automations will be scheduled here. |
| let src = new ConstantSourceNode(context, {offset: 0}); |
| src.connect(context.destination); |
| |
| // Initialize value to 1 at the beginning. |
| src.offset.setValueAtTime(1, 0); |
| |
| // Test automations have this event time. |
| let eventTime = eventFrame / context.sampleRate; |
| |
| // Sanity check that context is long enough for the test |
| should( |
| eventFrame < context.length, |
| prefix + 'Context length is long enough for the test') |
| .beTrue(); |
| |
| // Automations to be tested. The first event should be the actual |
| // output up to the event time. The last event should be the final |
| // output from the event time and onwards. |
| testCases.forEach(entry => { |
| should( |
| () => { |
| src.offset[entry.event]( |
| entry.value, eventTime, entry.extraArgs); |
| }, |
| prefix + |
| eventToString( |
| entry.event, entry.value, eventTime, entry.extraArgs)) |
| .notThrow(); |
| }); |
| |
| src.start(); |
| |
| return context.startRendering(); |
| } |
| |
| // Verify output of test where the final value of the automation is |
| // expected to be constant. |
| function expectConstant(prefix, should, eventFrame, testCases) { |
| return audioBuffer => { |
| let audio = audioBuffer.getChannelData(0); |
| |
| let eventTime = eventFrame / sampleRate; |
| |
| // Compute the expected value of the first automation one frame before |
| // the event time. This is a quick check that the correct automation |
| // was done. |
| let expectedValue = methodMap[testCases[0].event]( |
| (eventFrame - 1) / sampleRate, 1, 0, testCases[0].value, |
| eventTime); |
| should( |
| audio[eventFrame - 1], |
| prefix + |
| `At time ${ |
| (eventFrame - 1) / sampleRate |
| } (frame ${eventFrame - 1}) output`) |
| .beCloseTo(expectedValue, {threshold: testCases[0].relError}); |
| |
| // The last event scheduled is expected to set the value for all |
| // future times. Verify that the output has the expected value. |
| should( |
| audio.slice(eventFrame), |
| prefix + |
| `At time ${eventTime} (frame ${ |
| eventFrame |
| }) and later, output`) |
| .beConstantValueOf(testCases[testCases.length - 1].value); |
| }; |
| } |
| |
| // Test output when two events of the same time are scheduled at the same |
| // time. |
| function testMultipleSameEvents(should, options) { |
| let {method, prefix, threshold} = options; |
| |
| // Context for testing. |
| let context = |
| new OfflineAudioContext({length: 16384, sampleRate: sampleRate}); |
| |
| let src = new ConstantSourceNode(context); |
| src.connect(context.destination); |
| |
| let initialValue = 1; |
| |
| // Informative print |
| should(() => { |
| src.offset.setValueAtTime(initialValue, 0); |
| }, prefix + `setValueAtTime(${initialValue}, 0)`).notThrow(); |
| |
| let frame = 64; |
| let time = frame / context.sampleRate; |
| let values = [2, 7, 10]; |
| |
| // Schedule two events of the same type at the same time, but with |
| // different values. |
| |
| values.forEach(value => { |
| // Informative prints to show what we're doing in this test. |
| should( |
| () => { |
| src.offset[method](value, time); |
| }, |
| prefix + |
| eventToString( |
| method, |
| value, |
| time, |
| )) |
| .notThrow(); |
| }) |
| |
| src.start(); |
| |
| return context.startRendering().then(audioBuffer => { |
| let actual = audioBuffer.getChannelData(0); |
| |
| // The output should be a ramp from time 0 to the event time. But we |
| // only verify the value just before the event time, which should be |
| // fairly close to values[0]. (But compute the actual expected value |
| // to be sure.) |
| let expected = methodMap[method]( |
| (frame - 1) / context.sampleRate, initialValue, 0, values[0], |
| time); |
| should(actual[frame - 1], prefix + `Output at frame ${frame - 1}`) |
| .beCloseTo(expected, {threshold: threshold, precision: 3}); |
| |
| // Any other values shouldn't show up in the output. Only the value |
| // from last event should appear. We only check the value at the |
| // event time. |
| should( |
| actual[frame], prefix + `Output at frame ${frame} (${time} sec)`) |
| .beEqualTo(values[values.length - 1]); |
| }); |
| } |
| |
| // Convert an automation method to a string for printing. |
| function eventToString(method, value, time, extras) { |
| let string = method + '('; |
| string += (value instanceof Array) ? `[${value}]` : value; |
| string += ', ' + time; |
| if (extras) { |
| string += ', ' + extras; |
| } |
| string += ')'; |
| return string; |
| } |
| |
| // Map between the automation method name and a function that computes the |
| // output value of the automation method. |
| const methodMap = { |
| linearRampToValueAtTime: audioParamLinearRamp, |
| exponentialRampToValueAtTime: audioParamExponentialRamp, |
| setValueAtTime: (t, v) => v |
| }; |
| </script> |
| </body> |
| </html> |