<!DOCTYPE html>
<html>
<head>
<title>
Test OscillatorNode Has No Dezippering
</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">
// 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.8016e-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>