<!DOCTYPE html>
<html>
<head>
<title>
Test Sampling of Oscillator Start Times
</title>
<script src="../../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>