diff options
Diffstat (limited to 'testing/web-platform/tests/webaudio/the-audio-api/the-oscillatornode-interface')
4 files changed, 536 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-oscillatornode-interface/ctor-oscillator.html b/testing/web-platform/tests/webaudio/the-audio-api/the-oscillatornode-interface/ctor-oscillator.html new file mode 100644 index 0000000000..bf50195a5b --- /dev/null +++ b/testing/web-platform/tests/webaudio/the-audio-api/the-oscillatornode-interface/ctor-oscillator.html @@ -0,0 +1,112 @@ +<!DOCTYPE html> +<html> + <head> + <title> + Test Constructor: Oscillator + </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/audionodeoptions.js"></script> + </head> + <body> + <script id="layout-test-code"> + let context; + + let audit = Audit.createTaskRunner(); + + audit.define('initialize', (task, should) => { + context = initializeContext(should); + task.done(); + }); + + audit.define('invalid constructor', (task, should) => { + testInvalidConstructor(should, 'OscillatorNode', context); + task.done(); + }); + + audit.define('default constructor', (task, should) => { + let prefix = 'node0'; + let node = testDefaultConstructor(should, 'OscillatorNode', context, { + prefix: prefix, + numberOfInputs: 0, + numberOfOutputs: 1, + channelCount: 2, + channelCountMode: 'max', + channelInterpretation: 'speakers' + }); + + testDefaultAttributes( + should, node, prefix, + [{name: 'type', value: 'sine'}, {name: 'frequency', value: 440}]); + + task.done(); + }); + + audit.define('test AudioNodeOptions', (task, should) => { + testAudioNodeOptions(should, context, 'OscillatorNode'); + task.done(); + }); + + audit.define('constructor options', (task, should) => { + let node; + let options = {type: 'sawtooth', detune: 7, frequency: 918}; + + should( + () => { + node = new OscillatorNode(context, options); + }, + 'node1 = new OscillatorNode(c, ' + JSON.stringify(options) + ')') + .notThrow(); + + should(node.type, 'node1.type').beEqualTo(options.type); + should(node.detune.value, 'node1.detune.value') + .beEqualTo(options.detune); + should(node.frequency.value, 'node1.frequency.value') + .beEqualTo(options.frequency); + + should(node.channelCount, 'node1.channelCount').beEqualTo(2); + should(node.channelCountMode, 'node1.channelCountMode') + .beEqualTo('max'); + should(node.channelInterpretation, 'node1.channelInterpretation') + .beEqualTo('speakers'); + + // Test that type and periodicWave options work as described. + options = { + type: 'sine', + periodicWave: new PeriodicWave(context, {real: [1, 1]}) + }; + should(() => { + node = new OscillatorNode(context, options); + }, 'new OscillatorNode(c, ' + JSON.stringify(options) + ')').notThrow(); + + options = {type: 'custom'}; + should( + () => { + node = new OscillatorNode(context, options); + }, + 'new OscillatorNode(c, ' + JSON.stringify(options) + ')') + .throw(DOMException, 'InvalidStateError'); + + options = { + type: 'custom', + periodicWave: new PeriodicWave(context, {real: [1, 1]}) + }; + should(() => { + node = new OscillatorNode(context, options); + }, 'new OscillatorNode(c, ' + JSON.stringify(options) + ')').notThrow(); + + should( + () => { + node = new OscillatorNode(context, {periodicWave: null}); + }, + 'new OscillatorNode(c, {periodicWave: null}') + .throw(DOMException, 'TypeError'); + task.done(); + }); + + audit.run(); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-oscillatornode-interface/detune-limiting.html b/testing/web-platform/tests/webaudio/the-audio-api/the-oscillatornode-interface/detune-limiting.html new file mode 100644 index 0000000000..81a1293d03 --- /dev/null +++ b/testing/web-platform/tests/webaudio/the-audio-api/the-oscillatornode-interface/detune-limiting.html @@ -0,0 +1,154 @@ +<!doctype html> +<html> + <head> + <title> + Oscillator Detune Limits + </title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/webaudio/resources/audit.js"></script> + </head> + + <body> + <script> + const sampleRate = 44100; + const renderLengthSeconds = 0.125; + + let audit = Audit.createTaskRunner(); + + audit.define( + { + label: 'detune limits', + description: + 'Oscillator with detune and frequency at Nyquist or above' + }, + (task, should) => { + let context = new OfflineAudioContext( + 2, renderLengthSeconds * sampleRate, sampleRate); + + let merger = new ChannelMergerNode( + context, {numberOfInputs: context.destination.channelCount}); + merger.connect(context.destination); + + // For test oscillator, set the oscillator frequency to -Nyquist and + // set detune to be a large number that would cause the detuned + // frequency to be way above Nyquist. + const oscFrequency = 1; + const detunedFrequency = sampleRate; + const detuneValue = Math.fround(1200 * Math.log2(detunedFrequency)); + + let testOsc = new OscillatorNode( + context, {frequency: oscFrequency, detune: detuneValue}); + testOsc.connect(merger, 0, 1); + + // For the reference oscillator, determine the computed oscillator + // frequency using the values above and set that as the oscillator + // frequency. + let computedFreq = oscFrequency * Math.pow(2, detuneValue / 1200); + + let refOsc = new OscillatorNode(context, {frequency: computedFreq}); + refOsc.connect(merger, 0, 0); + + // Start 'em up and render + testOsc.start(); + refOsc.start(); + + context.startRendering() + .then(renderedBuffer => { + let expected = renderedBuffer.getChannelData(0); + let actual = renderedBuffer.getChannelData(1); + + // Let user know about the smaple rate so following messages + // make more sense. + should(context.sampleRate, 'Context sample rate') + .beEqualTo(context.sampleRate); + + // Since the frequency is at Nyquist, the reference oscillator + // output should be zero. + should( + refOsc.frequency.value, 'Reference oscillator frequency') + .beGreaterThanOrEqualTo(context.sampleRate / 2); + should( + expected, `Osc(freq: ${refOsc.frequency.value}) output`) + .beConstantValueOf(0); + // The output from each oscillator should be the same. + should( + actual, + 'Osc(freq: ' + oscFrequency + ', detune: ' + detuneValue + + ') output') + .beCloseToArray(expected, {absoluteThreshold: 0}); + + }) + .then(() => task.done()); + }); + + audit.define( + { + label: 'detune automation', + description: + 'Oscillator output with detune automation should be zero ' + + 'above Nyquist' + }, + (task, should) => { + let context = new OfflineAudioContext( + 1, renderLengthSeconds * sampleRate, sampleRate); + + const baseFrequency = 1; + const rampEnd = renderLengthSeconds / 2; + const detuneEnd = 1e7; + + let src = new OscillatorNode(context, {frequency: baseFrequency}); + src.detune.linearRampToValueAtTime(detuneEnd, rampEnd); + + src.connect(context.destination); + + src.start(); + + context.startRendering() + .then(renderedBuffer => { + let audio = renderedBuffer.getChannelData(0); + + // At some point, the computed oscillator frequency will go + // above Nyquist. Determine at what time this occurrs. The + // computed frequency is f * 2^(d/1200) where |f| is the + // oscillator frequency and |d| is the detune value. Thus, + // find |d| such that Nyquist = f*2^(d/1200). That is, d = + // 1200*log2(Nyquist/f) + let criticalDetune = + 1200 * Math.log2(context.sampleRate / 2 / baseFrequency); + + // Now figure out at what point on the linear ramp does the + // detune value reach this critical value. For a linear ramp: + // + // v(t) = V0+(V1-V0)*(t-T0)/(T1-T0) + // + // Thus, + // + // t = ((T1-T0)*v(t) + T0*V1 - T1*V0)/(V1-V0) + // + // In this test, T0 = 0, V0 = 0, T1 = rampEnd, V1 = + // detuneEnd, and v(t) = criticalDetune + let criticalTime = (rampEnd * criticalDetune) / detuneEnd; + let criticalFrame = + Math.ceil(criticalTime * context.sampleRate); + + should( + criticalFrame, + `Frame where detuned oscillator reaches Nyquist`) + .beEqualTo(criticalFrame); + + should( + audio.slice(0, criticalFrame), + `osc[0:${criticalFrame - 1}]`) + .notBeConstantValueOf(0); + + should(audio.slice(criticalFrame), `osc[${criticalFrame}:]`) + .beConstantValueOf(0); + }) + .then(() => task.done()); + }); + + audit.run(); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-oscillatornode-interface/detune-overflow.html b/testing/web-platform/tests/webaudio/the-audio-api/the-oscillatornode-interface/detune-overflow.html new file mode 100644 index 0000000000..28c28bc1db --- /dev/null +++ b/testing/web-platform/tests/webaudio/the-audio-api/the-oscillatornode-interface/detune-overflow.html @@ -0,0 +1,41 @@ +<!doctype html> +<html> + <head> + <title>Test Osc.detune Overflow</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/webaudio/resources/audit.js"></script> + <script src="/webaudio/resources/audit-util.js"></script> + </head> + + <body> + <script> + const sampleRate = 44100; + const renderLengthFrames = RENDER_QUANTUM_FRAMES; + + let audit = Audit.createTaskRunner(); + + audit.define('detune overflow', async (task, should) => { + let context = + new OfflineAudioContext(1, renderLengthFrames, sampleRate); + + // This value of frequency and detune results in a computed frequency of + // 440*2^(153600/1200) = 1.497e41. The frequency needs to be clamped to + // Nyquist. But a sine wave at Nyquist frequency is all zeroes. Verify + // the output is 0. + let osc = new OscillatorNode(context, {frequency: 440, detune: 153600}); + + osc.connect(context.destination); + + let buffer = await context.startRendering(); + let output = buffer.getChannelData(0); + should(output, 'Osc freq and detune outside nominal range') + .beConstantValueOf(0); + + task.done(); + }); + + audit.run(); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-oscillatornode-interface/osc-basic-waveform.html b/testing/web-platform/tests/webaudio/the-audio-api/the-oscillatornode-interface/osc-basic-waveform.html new file mode 100644 index 0000000000..b34c96855f --- /dev/null +++ b/testing/web-platform/tests/webaudio/the-audio-api/the-oscillatornode-interface/osc-basic-waveform.html @@ -0,0 +1,229 @@ +<!doctype html> +<html> + <head> + <title> + Test Basic Oscillator Sine Wave Test + </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> + </head> + + <body> + <script> + // Don't change the sample rate. The tests below depend on this sample + // rate to cover all the cases in Chrome's implementation. But the tests + // are general and apply to any browser. + const sampleRate = 44100; + + // Only need a few samples for testing, so just use two renders. + const durationFrames = 2 * RENDER_QUANTUM_FRAMES; + + let audit = Audit.createTaskRunner(); + + // The following tests verify that the oscillator produces the same + // results as the mathematical oscillators. We choose sine wave and a + // custom wave because we know they're bandlimited and won't change with + // the frequency. + // + // The tests for 1 and 2 Hz are intended to test Chrome's interpolation + // algorithm, but are still generally applicable to any browser. + + audit.define( + {label: 'Test 0', description: 'Sine wave: 100 Hz'}, + async (task, should) => { + let context = new OfflineAudioContext( + {length: durationFrames, sampleRate: sampleRate}); + + const freqHz = 100; + + let src = + new OscillatorNode(context, {type: 'sine', frequency: freqHz}); + src.connect(context.destination); + + src.start(); + + let renderedBuffer = await context.startRendering(); + checkResult(should, renderedBuffer, context, { + freqHz: freqHz, + a1: 0, + b1: 1, + prefix: 'Sine', + threshold: 1.8045e-6, + snrThreshold: 118.91 + }); + task.done(); + }); + + audit.define( + {label: 'Test 1', description: 'Sine wave: -100 Hz'}, + async (task, should) => { + let context = new OfflineAudioContext( + {length: durationFrames, sampleRate: sampleRate}); + + const freqHz = -100; + + let src = + new OscillatorNode(context, {type: 'sine', frequency: freqHz}); + src.connect(context.destination); + + src.start(); + + let renderedBuffer = await context.startRendering(); + checkResult(should, renderedBuffer, context, { + freqHz: freqHz, + a1: 0, + b1: 1, + prefix: 'Sine', + threshold: 4.7684e-7, + snrThreshold: 130.95 + }); + task.done(); + }); + + audit.define( + {label: 'Test 2', description: 'Sine wave: 2 Hz'}, + async (task, should) => { + let context = new OfflineAudioContext( + {length: durationFrames, sampleRate: sampleRate}); + + const freqHz = 2; + + let src = + new OscillatorNode(context, {type: 'sine', frequency: freqHz}); + src.connect(context.destination); + + src.start(); + + let renderedBuffer = await context.startRendering(); + checkResult(should, renderedBuffer, context, { + freqHz: freqHz, + a1: 0, + b1: 1, + prefix: 'Sine', + threshold: 1.4516e-7, + snrThreshold: 119.93 + }); + task.done(); + }); + + audit.define( + {label: 'Test 3', description: 'Sine wave: 1 Hz'}, + async (task, should) => { + let context = new OfflineAudioContext( + {length: durationFrames, sampleRate: sampleRate}); + + const freqHz = 1; + + let src = + new OscillatorNode(context, {type: 'sine', frequency: freqHz}); + src.connect(context.destination); + + src.start(); + + let renderedBuffer = await context.startRendering(); + checkResult(should, renderedBuffer, context, { + freqHz: freqHz, + a1: 0, + b1: 1, + prefix: 'Sine', + threshold: 1.4157e-7, + snrThreshold: 112.22 + }); + task.done(); + }); + + audit.define( + {label: 'Test 4', description: 'Custom wave: 100 Hz'}, + async (task, should) => { + let context = new OfflineAudioContext( + {length: durationFrames, sampleRate: sampleRate}); + + const freqHz = 100; + + let wave = new PeriodicWave( + context, + {real: [0, 1], imag: [0, 1], disableNormalization: true}); + let src = new OscillatorNode( + context, + {type: 'custom', frequency: freqHz, periodicWave: wave}); + src.connect(context.destination); + + src.start(); + + let renderedBuffer = await context.startRendering(); + checkResult(should, renderedBuffer, context, { + freqHz: freqHz, + a1: 1, + b1: 1, + prefix: 'Custom', + threshold: 1.8478e-6, + snrThreshold: 122.43 + }); + task.done(); + }); + + audit.define( + {label: 'Test 5', description: 'Custom wave: 1 Hz'}, + async (task, should) => { + let context = new OfflineAudioContext( + {length: durationFrames, sampleRate: sampleRate}); + + const freqHz = 1; + + let wave = new PeriodicWave( + context, + {real: [0, 1], imag: [0, 1], disableNormalization: true}); + let src = new OscillatorNode( + context, + {type: 'custom', frequency: freqHz, periodicWave: wave}); + src.connect(context.destination); + + src.start(); + + let renderedBuffer = await context.startRendering(); + checkResult(should, renderedBuffer, context, { + freqHz: freqHz, + a1: 1, + b1: 1, + prefix: 'Custom', + threshold: 4.7684e-7, + snrThreshold: 138.76 + }); + task.done(); + }); + + audit.run(); + + function waveForm(context, freqHz, a1, b1, nsamples) { + let buffer = + new AudioBuffer({length: nsamples, sampleRate: context.sampleRate}); + let signal = buffer.getChannelData(0); + const omega = 2 * Math.PI * freqHz / context.sampleRate; + for (let k = 0; k < nsamples; ++k) { + signal[k] = a1 * Math.cos(omega * k) + b1 * Math.sin(omega * k); + } + + return buffer; + } + + function checkResult(should, renderedBuffer, context, options) { + let {freqHz, a1, b1, prefix, threshold, snrThreshold} = options; + + let actual = renderedBuffer.getChannelData(0); + + let expected = + waveForm(context, freqHz, a1, b1, actual.length).getChannelData(0); + + should(actual, `${prefix}: ${freqHz} Hz`).beCloseToArray(expected, { + absoluteThreshold: threshold + }); + + let snr = 10 * Math.log10(computeSNR(actual, expected)); + + should(snr, `${prefix}: SNR (db)`).beGreaterThanOrEqualTo(snrThreshold); + } + </script> + </body> +</html> |