| <!DOCTYPE html> |
| <html> |
| <head> |
| <title> |
| Test OscillatorNode Has No Dezippering |
| </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> |
| </head> |
| <body> |
| <script id="layout-test-code"> |
| // The sample rate must be a power of two to avoid any round-off errors in |
| // computing when to suspend a context on a rendering quantum boundary. |
| // Otherwise this is pretty arbitrary. |
| let sampleRate = 16384; |
| |
| let audit = Audit.createTaskRunner(); |
| |
| // Compare value setter with Javascript-generated reference for frequency. |
| audit.define( |
| { |
| label: 'frequency', |
| description: 'Test Oscillator frequency has no dezippering' |
| }, |
| (task, should) => { |
| let frequency0 = 128; |
| let frequency1 = 440; |
| let newValue = frequency1; |
| |
| testWithSine(should, { |
| frequency0: frequency0, |
| frequency1: frequency1, |
| paramName: 'frequency', |
| newValue: newValue, |
| thresholds: [1.1921e-7, 1.7882e-7] |
| }).then(() => task.done()); |
| }); |
| |
| // Compare value setter with Javascript-generated reference for detune. |
| audit.define( |
| { |
| label: 'detune', |
| description: 'Test Oscillator detune has no dezippering' |
| }, |
| (task, should) => { |
| let frequency0 = 64; |
| let detune = 600; |
| // Compute new frequency this way to make the JS value match the |
| // internal frequency better. |
| let frequency1 = frequency0 * |
| Math.fround(Math.pow(2, Math.fround(detune / 1200))); |
| |
| testWithSine(should, { |
| frequency0: frequency0, |
| frequency1: frequency1, |
| paramName: 'detune', |
| newValue: detune, |
| thresholds: [1.1921e-7, 3.6657e-6], |
| }).then(() => task.done()); |
| }); |
| |
| audit.define( |
| { |
| label: 'setValueAtTime', |
| description: 'Test Oscillator value setter against setValueAtTime' |
| }, |
| (task, should) => { |
| testWithSetValue(should, { |
| initialValues: {frequency: 100}, |
| modulation: false, |
| changeList: [{ |
| suspendQuantum: 2, |
| changes: [ |
| {paramName: 'frequency', paramValue: 440}, |
| {paramName: 'detune', paramValue: 600} |
| ] |
| }] |
| }).then(() => task.done()); |
| }); |
| |
| audit.define( |
| { |
| label: 'modulation', |
| description: |
| 'Test Oscillator value setter against setValueAtTime ' + |
| 'with modulation' |
| }, |
| (task, should) => { |
| testWithSetValue(should, { |
| prefix: 'With modulation: ', |
| initialValues: {frequency: 1000}, |
| changeList: [{ |
| suspendQuantum: 2, |
| changes: [ |
| {paramName: 'frequency', paramValue: 440}, |
| {paramName: 'detune', paramValue: 600} |
| ] |
| }], |
| modParams: [ |
| { |
| paramName: 'frequency', |
| initialValue: {frequency: 1000}, |
| modGain: 100, |
| }, |
| { |
| paramName: 'detune', |
| initialValue: {frequency: 1000}, |
| modGain: 1000, |
| } |
| ] |
| }).then(() => task.done()); |
| }); |
| |
| audit.run(); |
| |
| // Compute a sample sine wave of frequency |f| assuming a sample rate of |
| // |sampleRate|. The number of samples computed is |length|. |
| function sineWave(f, sampleRate, length) { |
| let omega = 2 * Math.PI * f / sampleRate; |
| let data = new Float32Array(length); |
| for (let k = 0; k < length; ++k) { |
| data[k] = Math.sin(omega * k); |
| } |
| return data; |
| } |
| |
| // Test oscillator against a Javascript reference. |optioos| is a |
| // dictionary with the following items: |
| // frequency0 - initial frequency of the oscillator |
| // paramName - name of oscillator attribute to modified |
| // newValue - the new value of the attribute |
| // frequency1 - new value of oscillator, used for computing the reference |
| // value |
| // threshold - array of thresholds use to compare against the JS |
| // reference |
| // |
| // The oscillator starts at |frequency0|. After some time, the oscillator |
| // attribute |paramName| is set to |newValue|. The output from the |
| // oscillator is compared against a Javascript reference. |
| function testWithSine(should, options) { |
| let context = new OfflineAudioContext(1, sampleRate, sampleRate); |
| |
| // Frequency of oscillator must be such that the period is a whole |
| // number of render quanta. |
| let frequency0 = options.frequency0; |
| let frequency1 = options.frequency1; |
| |
| let periodFrames = sampleRate / frequency0; |
| let period = periodFrames / sampleRate; |
| |
| // Sanity check that periodFrames is an integer and that it is a |
| // multiple of 128 so that we suspend on a rendering boundary. We do |
| // this to make the Javascript reference easier to compute so that when |
| // the frequency changes, we start from the beginning of the sine wave, |
| // not somewhere in between. |
| should( |
| periodFrames === Math.floor(periodFrames), |
| `Oscillator period in frames (${periodFrames}) is an integer`) |
| .beTrue(); |
| should( |
| periodFrames / RENDER_QUANTUM_FRAMES === |
| Math.floor(periodFrames / RENDER_QUANTUM_FRAMES), |
| 'Oscillator period in frames (' + periodFrames + |
| `) is a multiple of ${RENDER_QUANTUM_FRAMES}`) |
| .beTrue(); |
| |
| osc = |
| new OscillatorNode(context, {type: 'sine', frequency: frequency0}); |
| |
| osc.connect(context.destination); |
| |
| // After 1 oscillator period, change the frequency. This will happen |
| // on a rendering boundary. |
| context.suspend(period) |
| .then(() => osc[options.paramName].value = options.newValue) |
| .then(() => context.resume()); |
| |
| osc.start(); |
| return context.startRendering().then(renderedBuffer => { |
| let renderedData = renderedBuffer.getChannelData(0); |
| |
| // Compute expected results. The first part should one period |
| // of a sine wave with frequency |frequency0|. The second |
| // part should be a sine wave with frequency |frequency1|. |
| let part0 = sineWave(frequency0, sampleRate, periodFrames); |
| let part1 = sineWave( |
| frequency1, sampleRate, renderedData.length - periodFrames); |
| |
| // Verify the two parts match. Thresholds here are |
| // experimentally determined. |
| should( |
| renderedData.slice(0, periodFrames), |
| `Part 0 (sine wave at ${frequency0} Hz)`) |
| .beCloseToArray( |
| part0, {absoluteThreshold: options.thresholds[0]}); |
| should( |
| renderedData.slice(periodFrames), |
| `Part 1 (sine wave at ${frequency1} Hz)`) |
| .beCloseToArray( |
| part1, {absoluteThreshold: options.thresholds[1]}); |
| }); |
| } |
| |
| // Test oscillator using automation as a reference. |options| is a |
| // dictionary with the following items: |
| // |
| // prefix - optional prefix for messages (to make messages |
| // unique) |
| // initialValues - initial values for the oscillator |
| // changeList - an array specifying when and what should be changed. |
| // modParams - an array specifying the modulation parameters. The |
| // modulation is an oscillator that is connected to one |
| // of the AudioParams of the oscillator. |
| // |
| // The |changeList| entry is a dictionary with the following items: |
| // suspendQuantum - render quantum at which the value is changed. |
| // changes - an array of dictionaries specifying what oscillator |
| // attribute should be changed and the correspond |
| // value. This is a dictionary with items |paramName| |
| // and |paramValue| |
| // |
| // The |modParams| entry is an array of dictionaries, and each dictionary |
| // has the following items: |
| // paramName - name of the oscillator attribute to change |
| // initialValue - initial value for the modulation oscillator |
| // modGain - gain applied to the oscillator output before |
| // connecting to the AudioParam of the test |
| // oscillator. |
| function testWithSetValue(should, options) { |
| let context = new OfflineAudioContext(2, sampleRate, sampleRate); |
| let merger = new ChannelMergerNode(context, {numberOfChannels: 2}); |
| merger.connect(context.destination); |
| |
| // |srcTest| is the oscillator to be tested using the value setter. |
| // |srcRef| is an identical oscillator except that |setValueAtTime| will |
| // be used to change the oscillator. |
| let srcTest = new OscillatorNode(context, options.initialValues); |
| let srcRef = new OscillatorNode(context, options.initialValues); |
| |
| srcTest.connect(merger, 0, 0); |
| srcRef.connect(merger, 0, 1); |
| |
| // Apply each change given by |changeList|. |
| options.changeList.forEach(change => { |
| let changeTime = change.suspendQuantum * RENDER_QUANTUM_FRAMES / |
| context.sampleRate; |
| // Use setValue on the reference oscillator and also set the value of |
| // the test oscillator. |
| change.changes.forEach(item => { |
| srcRef[item.paramName].setValueAtTime(item.paramValue, changeTime); |
| }); |
| context.suspend(changeTime) |
| .then(() => { |
| change.changes.forEach(item => { |
| srcTest[item.paramName].value = item.paramValue; |
| }); |
| }) |
| .then(() => context.resume()); |
| }); |
| |
| if (options.modParams) { |
| // If |modParams| is given, create an oscillator with an appropriate |
| // gain for each entry and connect it to the specified AudioParam of |
| // both the reference and test oscillators. |
| options.modParams.forEach(item => { |
| let mod = new OscillatorNode(context, item.initialValue); |
| let modGain = new GainNode(context, {gain: item.modGain}); |
| mod.connect(modGain); |
| modGain.connect(srcRef[item.paramName]); |
| modGain.connect(srcTest[item.paramName]); |
| mod.start(); |
| }); |
| } |
| |
| srcRef.start(); |
| srcTest.start(); |
| |
| return context.startRendering().then(renderedBuffer => { |
| let actual = renderedBuffer.getChannelData(0); |
| let expected = renderedBuffer.getChannelData(1); |
| |
| let prefix = options.prefix || ''; |
| |
| // The output using the value setter (|actual|) should be identical to |
| // the output using |setValueAtTime| (|expected|). |
| let match = should(actual, prefix + 'Output from .value setter') |
| .beEqualToArray(expected); |
| should( |
| match, |
| prefix + 'Output from .value setter matches ' + |
| 'setValueAtTime output') |
| .beTrue(); |
| }) |
| } |
| </script> |
| </body> |
| </html> |