Commit 607f2792 authored by rtoy's avatar rtoy Committed by Commit bot

Sub-sample accurate start of OscillatorNode

Previously, an oscillator would start at the nearest sample frame
boundary.  This can produce noticeable effects.

Instead, make the oscillator start at the requested sample time and
sample the curve appropriately.

BUG=631576
TEST=Oscillator/start-sampling.html

Review-Url: https://codereview.chromium.org/2186813003
Cr-Original-Commit-Position: refs/heads/master@{#442669}
Committed: https://chromium.googlesource.com/chromium/src/+/7909df464ca46a1ea121b0c54e5d04cfa521e38c
Review-Url: https://codereview.chromium.org/2186813003
Cr-Commit-Position: refs/heads/master@{#443040}
parent a08094de
<!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>
// 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("basic test small", function (task, should) {
task.describe("Start oscillator slightly past a sample frame")
testStartSampling(should, 1.25, {
error: 1.0842e-4,
snrThreshold: 84.054
})
.then(task.done.bind(task));
});
audit.define("basic test big", function (task, should) {
task.describe("Start oscillator slightly before a sample frame")
testStartSampling(should, 1.75, {
error: 1.0839e-4,
snrThreshold: 84.056
})
.then(task.done.bind(task));
});
audit.define("diff big offset", function (task, should) {
task.describe(
"Test sampling with start offset greater than 1/2 sampling frame"
);
// 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: 4.1724e-7,
snrThreshold: 137.536
})
.then(task.done.bind(task));
});
audit.define("diff small offset", function (task, should) {
task.describe(
"Test sampling with start offset less than 1/2 sampling frame");
// 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: 4.1724e-7,
snrThreshold: 137.536
})
.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));
}
should(actual, "Oscillator.start(" + startFrame + " frames)")
.beCloseToArray(expected, {
absoluteThreshold: thresholds.error
});
let snr = 10 * Math.log10(computeSNR(actual, expected));
should(snr, "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));
should(oscStart, "Delayed cosine oscillator at sample rate " + sampleRate)
.beCloseToArray(oscGain, {
absoluteThreshold: thresholds.error
});
should(snr, "SNR (dB)")
.beGreaterThanOrEqualTo(thresholds.snrThreshold);
});
}
audit.run();
</script>
</body>
</html>
......@@ -170,24 +170,6 @@ var JSTEST = false;
})();
// Compute the (linear) signal-to-noise ratio between |actual| and |expected|. The result is NOT in
// dB! If the |actual| and |expected| have different lengths, the shorter length is used.
function computeSNR(actual, expected)
{
var signalPower = 0;
var noisePower = 0;
var length = Math.min(actual.length, expected.length);
for (var k = 0; k < length; ++k) {
var diff = actual[k] - expected[k];
signalPower += expected[k] * expected[k];
noisePower += diff * diff;
}
return signalPower / noisePower;
}
// |Should| JS layout test utility.
// Dependency: ../resources/js-test.js
var Should = (function () {
......
......@@ -257,3 +257,21 @@ function grainLengthInSampleFrames(grainOffset, duration, sampleRate) {
function isValidNumber(x) {
return !isNaN(x) && (x != Infinity) && (x != -Infinity);
}
// Compute the (linear) signal-to-noise ratio between |actual| and
// |expected|. The result is NOT in dB! If the |actual| and
// |expected| have different lengths, the shorter length is used.
function computeSNR(actual, expected) {
var signalPower = 0;
var noisePower = 0;
var length = Math.min(actual.length, expected.length);
for (var k = 0; k < length; ++k) {
var diff = actual[k] - expected[k];
signalPower += expected[k] * expected[k];
noisePower += diff * diff;
}
return signalPower / noisePower;
}
......@@ -119,9 +119,10 @@ void AudioBufferSourceHandler::process(size_t framesToProcess) {
size_t quantumFrameOffset;
size_t bufferFramesToProcess;
double startTimeOffset;
updateSchedulingInfo(framesToProcess, outputBus, quantumFrameOffset,
bufferFramesToProcess);
bufferFramesToProcess, startTimeOffset);
if (!bufferFramesToProcess) {
outputBus->zero();
......
......@@ -50,7 +50,8 @@ void AudioScheduledSourceHandler::updateSchedulingInfo(
size_t quantumFrameSize,
AudioBus* outputBus,
size_t& quantumFrameOffset,
size_t& nonSilentFramesToProcess) {
size_t& nonSilentFramesToProcess,
double& startFrameOffset) {
DCHECK(outputBus);
if (!outputBus)
return;
......@@ -94,6 +95,10 @@ void AudioScheduledSourceHandler::updateSchedulingInfo(
// Increment the active source count only if we're transitioning from
// SCHEDULED_STATE to PLAYING_STATE.
setPlaybackState(PLAYING_STATE);
// Determine the offset of the true start time from the starting frame.
startFrameOffset = m_startTime * sampleRate - startFrame;
} else {
startFrameOffset = 0;
}
quantumFrameOffset =
......
......@@ -88,10 +88,15 @@ class AudioScheduledSourceHandler : public AudioHandler {
// rendering.
// nonSilentFramesToProcess : Number of frames rendering non-silence (will be
// <= quantumFrameSize).
// startFrameOffset : The fractional frame offset from quantumFrameOffset
// and the actual starting time of the source. This is
// non-zero only when transitioning from the
// SCHEDULED_STATE to the PLAYING_STATE.
void updateSchedulingInfo(size_t quantumFrameSize,
AudioBus* outputBus,
size_t& quantumFrameOffset,
size_t& nonSilentFramesToProcess);
size_t& nonSilentFramesToProcess,
double& startFrameOffset);
// Called when we have no more sound to play or the stop() time has been
// reached. No onEnded event is called.
......
......@@ -58,11 +58,12 @@ void ConstantSourceHandler::process(size_t framesToProcess) {
size_t quantumFrameOffset;
size_t nonSilentFramesToProcess;
double startFrameOffset;
// Figure out where in the current rendering quantum that the source is
// active and for how many frames.
updateSchedulingInfo(framesToProcess, outputBus, quantumFrameOffset,
nonSilentFramesToProcess);
nonSilentFramesToProcess, startFrameOffset);
if (!nonSilentFramesToProcess) {
outputBus->zero();
......
......@@ -239,9 +239,10 @@ void OscillatorHandler::process(size_t framesToProcess) {
size_t quantumFrameOffset;
size_t nonSilentFramesToProcess;
double startFrameOffset;
updateSchedulingInfo(framesToProcess, outputBus, quantumFrameOffset,
nonSilentFramesToProcess);
nonSilentFramesToProcess, startFrameOffset);
if (!nonSilentFramesToProcess) {
outputBus->zero();
......@@ -286,6 +287,19 @@ void OscillatorHandler::process(size_t framesToProcess) {
destP += quantumFrameOffset;
int n = nonSilentFramesToProcess;
// If startFrameOffset is not 0, that means the oscillator doesn't actually
// start at quantumFrameOffset, but just past that time. Adjust destP and n
// to reflect that, and adjust virtualReadIndex to start the value at
// startFrameOffset.
if (startFrameOffset > 0) {
++destP;
--n;
virtualReadIndex += (1 - startFrameOffset) * frequency * rateScale;
DCHECK(virtualReadIndex < periodicWaveSize);
} else if (startFrameOffset < 0) {
virtualReadIndex = -startFrameOffset * frequency * rateScale;
}
while (n--) {
unsigned readIndex = static_cast<unsigned>(virtualReadIndex);
unsigned readIndex2 = readIndex + 1;
......
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