| <!DOCTYPE html> |
| <html> |
| <head> |
| <title> |
| Test Sampling of Oscillator Start Times |
| </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"> |
| // Experimentation indicates that this sample rate with a 440 Hz |
| // oscillator makes for a large difference in the difference signal if the |
| // oscillator start isn't sampled correctly. |
| let defaultSampleRate = 24000; |
| let renderDuration = 1; |
| let renderFrames = renderDuration * defaultSampleRate; |
| |
| let audit = Audit.createTaskRunner(); |
| |
| audit.define( |
| { |
| label: 'basic test small', |
| description: 'Start oscillator slightly past a sample frame' |
| }, |
| function(task, should) { |
| testStartSampling(should, 1.25, { |
| error: 1.0880e-4, |
| snrThreshold: 84.054 |
| }).then(task.done.bind(task)); |
| }); |
| |
| audit.define( |
| { |
| label: 'basic test big', |
| description: 'Start oscillator slightly before a sample frame' |
| }, |
| function(task, should) { |
| testStartSampling(should, 1.75, { |
| error: 1.0844e-4, |
| snrThreshold: 84.056 |
| }).then(task.done.bind(task)); |
| }); |
| |
| audit.define( |
| { |
| label: 'diff big offset', |
| description: |
| 'Test sampling with start offset greater than 1/2 sampling frame' |
| }, |
| function(task, should) { |
| // With a sample rate of 24000 Hz, and an oscillator frequency of |
| // 440 Hz (the default), a quarter wave delay is 13.636363... |
| // frames. This tests the case where the starting time is more than |
| // 1/2 frame from the preceding sampling frame. This tests one path |
| // of the internal implementation. |
| testStartWithGain(should, defaultSampleRate, { |
| error: 1.9521e-6, |
| snrThreshold: 128.12 |
| }).then(task.done.bind(task)); |
| }); |
| |
| audit.define( |
| { |
| label: 'diff small offset', |
| description: |
| 'Test sampling with start offset less than 1/2 sampling frame' |
| }, |
| function(task, should) { |
| // With a sample rate of 48000 Hz, and an oscillator frequency of |
| // 440 Hz (the default), a quarter wave delay is 27.2727... frames. |
| // This tests the case where the starting time is less than 1/2 |
| // frame from the preceding sampling frame. This tests one path of |
| // the internal implementation. |
| testStartWithGain(should, 48000, { |
| error: 1.9521e-6, |
| snrThreshold: 122.92 |
| }).then(task.done.bind(task)); |
| }); |
| |
| function testStartSampling(should, startFrame, thresholds) { |
| // Start the oscillator in the middle of a sample frame and compare |
| // against the theoretical result. |
| let context = |
| new OfflineAudioContext(1, renderFrames, defaultSampleRate); |
| let osc = context.createOscillator(); |
| osc.connect(context.destination); |
| osc.start(startFrame / context.sampleRate); |
| |
| return context.startRendering().then(function(result) { |
| let actual = result.getChannelData(0); |
| let expected = new Array(actual.length); |
| expected.fill(0); |
| |
| // The expected curve is |
| // |
| // sin(2*pi*f*(t-t0)) |
| // |
| // where f is the oscillator frequency and t0 is the start time. |
| let actualStart = Math.ceil(startFrame); |
| let omega = 2 * Math.PI * osc.frequency.value / context.sampleRate; |
| for (let k = actualStart; k < actual.length; ++k) { |
| expected[k] = Math.sin(omega * (k - startFrame)); |
| } |
| |
| let prefix = 'Oscillator.start(' + startFrame + ' frames)'; |
| should(actual, prefix).beCloseToArray(expected, { |
| absoluteThreshold: thresholds.error |
| }); |
| let snr = 10 * Math.log10(computeSNR(actual, expected)); |
| should(snr, prefix + ': SNR (dB)') |
| .beGreaterThanOrEqualTo(thresholds.snrThreshold); |
| }) |
| } |
| |
| function testStartWithGain(should, sampleRate, thresholds) { |
| // Test consists of starting a cosine wave with a quarter wavelength |
| // delay and comparing that with a sine wave that has the initial |
| // quarter wavelength zeroed out. These should be equal. |
| |
| let context = new OfflineAudioContext(3, renderFrames, sampleRate); |
| let osc = context.createOscillator(); |
| |
| let merger = context.createChannelMerger(3); |
| merger.connect(context.destination); |
| |
| // Start the cosine oscillator at this time. This means the wave starts |
| // at frame 13.636363.... |
| let quarterWaveTime = (1 / 4) / osc.frequency.value; |
| |
| // Sine wave oscillator with gain term to zero out the initial quarter |
| // wave length of the output. |
| let g = context.createGain(); |
| g.gain.setValueAtTime(0, 0); |
| g.gain.setValueAtTime(1, quarterWaveTime); |
| osc.connect(g); |
| g.connect(merger, 0, 2); |
| g.connect(merger, 0, 0); |
| |
| // Cosine wave oscillator with starting after a quarter wave length. |
| let osc2 = context.createOscillator(); |
| // Creates a cosine wave. |
| let wave = context.createPeriodicWave( |
| Float32Array.from([0, 1]), Float32Array.from([0, 0])); |
| osc2.setPeriodicWave(wave); |
| |
| osc2.connect(merger, 0, 1); |
| |
| // A gain inverter so subtract the two waveforms. |
| let inverter = context.createGain(); |
| inverter.gain.value = -1; |
| osc2.connect(inverter); |
| inverter.connect(merger, 0, 0); |
| |
| osc.start(); |
| osc2.start(quarterWaveTime); |
| |
| return context.startRendering().then(function(result) { |
| // Channel 0 = diff |
| // Channel 1 = osc with start |
| // Channel 2 = osc with gain |
| |
| // Channel 0 should be very close to 0. |
| // Channel 1 should match channel 2 very closely. |
| let diff = result.getChannelData(0); |
| let oscStart = result.getChannelData(1); |
| let oscGain = result.getChannelData(2); |
| let snr = 10 * Math.log10(computeSNR(oscStart, oscGain)); |
| |
| let prefix = |
| 'Sample rate ' + sampleRate + ': Delayed cosine oscillator'; |
| should(oscStart, prefix).beCloseToArray(oscGain, { |
| absoluteThreshold: thresholds.error |
| }); |
| should(snr, prefix + ': SNR (dB)') |
| .beGreaterThanOrEqualTo(thresholds.snrThreshold); |
| }); |
| } |
| |
| audit.run(); |
| </script> |
| </body> |
| </html> |