Commit 96bea082 authored by Raymond Toy's avatar Raymond Toy Committed by Commit Bot

Remove Dezippering from OscillatorNode

Remove dezippering from the frequency and detune attributes. The values
are changed immediately instead of gradually changing from the old
value to the new.

Chromium Feature: https://www.chromestatus.com/features/5287995770929152
Intent to ship: https://groups.google.com/a/chromium.org/d/msg/blink-dev/YKYRrh0nWMo/aGzd3049AgAJ

Bug: 752987
Test: Oscillator/dezipper.html
Change-Id: I58df869ca99a24f6429c9cc50c1af2101dbdf03d
Reviewed-on: https://chromium-review.googlesource.com/609095
Commit-Queue: Raymond Toy <rtoy@chromium.org>
Reviewed-by: default avatarHongchan Choi <hongchan@chromium.org>
Cr-Commit-Position: refs/heads/master@{#532663}
parent 848e1096
<!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.1921e-7, 1.3114e-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>
......@@ -184,10 +184,8 @@ bool OscillatorHandler::CalculateSampleAccuratePhaseIncrements(
frequency_->CalculateSampleAccurateValues(phase_increments,
frames_to_process);
} else {
// Handle ordinary parameter smoothing/de-zippering if there are no
// scheduled changes.
frequency_->Smooth();
float frequency = frequency_->SmoothedValue();
// Handle ordinary parameter changes if there are no scheduled changes.
float frequency = frequency_->Value();
final_scale *= frequency;
}
......@@ -212,10 +210,9 @@ bool OscillatorHandler::CalculateSampleAccuratePhaseIncrements(
frames_to_process);
}
} else {
// Handle ordinary parameter smoothing/de-zippering if there are no
// scheduled changes.
detune_->Smooth();
float detune = detune_->SmoothedValue();
// Handle ordinary parameter changes if there are no scheduled
// changes.
float detune = detune_->Value();
float detune_scale = powf(2, detune / 1200);
final_scale *= detune_scale;
}
......@@ -393,8 +390,8 @@ void OscillatorHandler::Process(size_t frames_to_process) {
float table_interpolation_factor = 0;
if (!has_sample_accurate_values) {
frequency = frequency_->SmoothedValue();
float detune = detune_->SmoothedValue();
frequency = frequency_->Value();
float detune = detune_->Value();
float detune_scale = powf(2, detune / 1200);
frequency *= detune_scale;
periodic_wave_->WaveDataForFundamentalFrequency(frequency, lower_wave_data,
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment