Commit eb40846e authored by Raymond Toy's avatar Raymond Toy Committed by Commit Bot

Connected k-rate AudioParams must get input values (1 of N)

When an AudioParam was selected to be k-rate, any inputs to the
AudioParam were ignored, so the AudioParam only contained the effect
of automation events.  This is incorrect.  Any inputs should be
reflected in the AudioParam values.

The case of connected AudioParams with a-rate automation was working fine.

Handles ConstantSource, Gain, and StereoPanner since these have very
similar AudioParam processing with only one simple AudioParam.

Bug: 1015760
Change-Id: I29ec3d7b58248a96da911e03890b74f6fb1bf098
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1873392Reviewed-by: default avatarHongchan Choi <hongchan@chromium.org>
Commit-Queue: Raymond Toy <rtoy@chromium.org>
Cr-Commit-Position: refs/heads/master@{#745658}
parent 40498960
......@@ -190,6 +190,17 @@ class AudioParamHandler final : public ThreadSafeRefCounted<AudioParamHandler>,
return has_values || NumberOfRenderingConnections();
}
// TODO(crbug.com/1015760) This is like HasSAmpleAccurateValues, but
// we don't check for the rate. When the bug is fixed,
// HasSampleAccurateValues can be removed and this methed renamed.
bool HasSampleAccurateValuesTimeline() {
bool has_values =
timeline_.HasValues(destination_handler_->CurrentSampleFrame(),
destination_handler_->SampleRate());
return has_values || NumberOfRenderingConnections();
}
bool IsAudioRate() const { return automation_rate_ == kAudio; }
// Calculates numberOfValues parameter values starting at the context's
......
......@@ -70,7 +70,9 @@ void ConstantSourceHandler::Process(uint32_t frames_to_process) {
return;
}
if (offset_->HasSampleAccurateValues()) {
bool is_sample_accurate = offset_->HasSampleAccurateValuesTimeline();
if (is_sample_accurate && offset_->IsAudioRate()) {
DCHECK_LE(frames_to_process, sample_accurate_values_.size());
float* offsets = sample_accurate_values_.Data();
offset_->CalculateSampleAccurateValues(offsets, frames_to_process);
......@@ -82,19 +84,20 @@ void ConstantSourceHandler::Process(uint32_t frames_to_process) {
} else {
output_bus->Zero();
}
} else {
float value = offset_->Value();
if (value == 0) {
output_bus->Zero();
} else {
float* dest = output_bus->Channel(0)->MutableData();
dest += quantum_frame_offset;
for (unsigned k = 0; k < non_silent_frames_to_process; ++k) {
dest[k] = value;
}
output_bus->ClearSilentFlag();
return;
}
float value = is_sample_accurate ? offset_->FinalValue() : offset_->Value();
if (value == 0) {
output_bus->Zero();
} else {
float* dest = output_bus->Channel(0)->MutableData();
dest += quantum_frame_offset;
for (unsigned k = 0; k < non_silent_frames_to_process; ++k) {
dest[k] = value;
}
output_bus->ClearSilentFlag();
}
}
......
......@@ -67,7 +67,9 @@ void GainHandler::Process(uint32_t frames_to_process) {
} else {
scoped_refptr<AudioBus> input_bus = Input(0).Bus();
if (gain_->HasSampleAccurateValues()) {
bool is_sample_accurate = gain_->HasSampleAccurateValuesTimeline();
if (is_sample_accurate && gain_->IsAudioRate()) {
// Apply sample-accurate gain scaling for precise envelopes, grain
// windows, etc.
DCHECK_LE(frames_to_process, sample_accurate_gain_values_.size());
......@@ -75,14 +77,19 @@ void GainHandler::Process(uint32_t frames_to_process) {
gain_->CalculateSampleAccurateValues(gain_values, frames_to_process);
output_bus->CopyWithSampleAccurateGainValuesFrom(*input_bus, gain_values,
frames_to_process);
return;
}
// The gain is not sample-accurate or not a-rate. In this case, we have a
// fixed gain for the render and just need to incorporate any inputs to the
// gain, if any.
float gain = is_sample_accurate ? gain_->FinalValue() : gain_->Value();
if (gain == 0) {
output_bus->Zero();
} else {
// Apply the gain.
if (gain_->Value() == 0) {
// If the gain is 0, just zero the bus and set the silence hint.
output_bus->Zero();
} else {
output_bus->CopyWithGainFrom(*input_bus, gain_->Value());
}
output_bus->CopyWithGainFrom(*input_bus, gain);
}
}
}
......
......@@ -59,17 +59,25 @@ void StereoPannerHandler::Process(uint32_t frames_to_process) {
return;
}
if (pan_->HasSampleAccurateValues()) {
bool is_sample_accurate = pan_->HasSampleAccurateValuesTimeline();
if (is_sample_accurate && pan_->IsAudioRate()) {
// Apply sample-accurate panning specified by AudioParam automation.
DCHECK_LE(frames_to_process, sample_accurate_pan_values_.size());
float* pan_values = sample_accurate_pan_values_.Data();
pan_->CalculateSampleAccurateValues(pan_values, frames_to_process);
stereo_panner_->PanWithSampleAccurateValues(input_bus.get(), output_bus,
pan_values, frames_to_process);
} else {
stereo_panner_->PanToTargetValue(input_bus.get(), output_bus, pan_->Value(),
frames_to_process);
return;
}
// The pan value is not sample-accurate or not a-rate. In this case, we have
// a fixed pan value for the render and just need to incorporate any inputs to
// the value, if any.
float pan_value = is_sample_accurate ? pan_->FinalValue() : pan_->Value();
stereo_panner_->PanToTargetValue(input_bus.get(), output_bus, pan_value,
frames_to_process);
}
void StereoPannerHandler::ProcessOnlyAudioParams(uint32_t frames_to_process) {
......
<!doctype html>
<html>
<head>
<title>k-rate AudioParams with Inputs</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>
let audit = Audit.createTaskRunner();
// Must be power of two to eliminate round-off
const sampleRate = 8192;
// Arbitrary duration that doesn't need to be too long to verify k-rate
// automations. Probably should be at least a few render quanta.
const testDuration = 8 * RENDER_QUANTUM_FRAMES / sampleRate;
// Test k-rate GainNode.gain is k-rate
audit.define(
{label: 'Gain', description: 'k-rate GainNode.gain'},
(task, should) => {
let context = new OfflineAudioContext({
numberOfChannels: 2,
sampleRate: sampleRate,
length: testDuration * sampleRate
});
let merger = new ChannelMergerNode(
context, {numberOfInputs: context.destination.channelCount});
merger.connect(context.destination);
let src = new ConstantSourceNode(context);
createTestSubGraph(context, src, merger, 'GainNode', 'gain');
src.start();
context.startRendering()
.then(buffer => {
let actual = buffer.getChannelData(0);
let expected = buffer.getChannelData(1);
for (let k = 0; k < actual.length;
k += RENDER_QUANTUM_FRAMES) {
should(
actual.slice(k, k + RENDER_QUANTUM_FRAMES),
`gain[${k}:${k + RENDER_QUANTUM_FRAMES}]`)
.beConstantValueOf(expected[k]);
}
})
.then(() => task.done());
});
// Test k-rate StereoPannerNode.pan is k-rate
audit.define(
{label: 'StereoPanner', description: 'k-rate StereoPannerNode.pan'},
(task, should) => {
let context = new OfflineAudioContext({
numberOfChannels: 2,
sampleRate: sampleRate,
length: testDuration * sampleRate
});
let merger = new ChannelMergerNode(
context, {numberOfInputs: context.destination.channelCount});
merger.connect(context.destination);
let src = new ConstantSourceNode(context);
createTestSubGraph(
context, src, merger, 'StereoPannerNode', 'pan', {
testModSetup: node => {
node.offset.setValueAtTime(-1, 0);
node.offset.linearRampToValueAtTime(1, testDuration);
}
});
src.start();
context.startRendering()
.then(buffer => {
let actual = buffer.getChannelData(0);
let expected = buffer.getChannelData(1);
for (let k = 0; k < actual.length; k += 128) {
should(actual.slice(k, k + 128), `pan[${k}:${k + 128}]`)
.beConstantValueOf(expected[k]);
}
})
.then(() => task.done());
});
audit.run();
function createTestSubGraph(
context, src, merger, nodeName, paramName, options) {
// The test node which has its AudioParam set up for k-rate autmoations.
let tstNode = new window[nodeName](context);
if (options && options.setups) {
options.setups(tstNode);
}
tstNode[paramName].automationRate = 'k-rate';
// Modulating signal for the test node. Just a linear ramp. This is
// connected to the AudioParam of the tstNode.
let tstMod = new ConstantSourceNode(context);
if (options && options.testModSetup) {
options.testModSetup(tstMod);
} else {
tstMod.offset.linearRampToValueAtTime(context.length, testDuration);
}
tstMod.connect(tstNode[paramName]);
src.connect(tstNode).connect(merger, 0, 0);
// The ref node is the same type of node as the test node, but uses
// a-rate automation. However, the modulating signal is k-rate. This
// causes the input to the audio param to be constant over a render,
// which is basically the same as making the audio param be k-rate.
let refNode = new window[nodeName](context);
let refMod = new ConstantSourceNode(context);
refMod.offset.automationRate = 'k-rate';
if (options && options.testModSetup) {
options.testModSetup(refMod);
} else {
refMod.offset.linearRampToValueAtTime(context.length, testDuration);
}
refMod.connect(refNode[paramName]);
src.connect(refNode).connect(merger, 0, 1);
tstMod.start();
refMod.start();
}
</script>
</body>
</html>
......@@ -44,6 +44,132 @@
}).then(() => task.done());
});
// Parameters for the For the following tests.
// Must be power of two to eliminate round-off
const sampleRate8k = 8192;
// Arbitrary duration that doesn't need to be too long to verify k-rate
// automations. Probably should be at least a few render quanta.
const testDuration = 8 * RENDER_QUANTUM_FRAMES / sampleRate8k;
// Basic test that k-rate ConstantSourceNode.offset is k-rate. This is
// the basis for all of the following tests, so make sure it's right.
audit.define(
{
label: 'ConstantSourceNode.offset k-rate automation',
description:
'Explicitly test ConstantSourceNode.offset k-rate automation is k-rate'
},
(task, should) => {
let context = new OfflineAudioContext({
numberOfChannels: 2,
sampleRate: sampleRate8k,
length: testDuration * sampleRate8k
});
let merger = new ChannelMergerNode(
context, {numberOfInputs: context.destination.channelCount});
merger.connect(context.destination);
// k-rate ConstantSource.offset using a linear ramp starting at 0
// and incrementing by 1 for each frame.
let src = new ConstantSourceNode(context, {offset: 0});
src.offset.automationRate = 'k-rate';
src.offset.setValueAtTime(0, 0);
src.offset.linearRampToValueAtTime(context.length, testDuration);
src.connect(merger, 0, 0);
src.start();
// a-rate ConstantSource using the same ramp as above.
let refSrc = new ConstantSourceNode(context, {offset: 0});
refSrc.offset.setValueAtTime(0, 0);
refSrc.offset.linearRampToValueAtTime(context.length, testDuration);
refSrc.connect(merger, 0, 1);
refSrc.start();
context.startRendering()
.then(buffer => {
let actual = buffer.getChannelData(0);
let expected = buffer.getChannelData(1);
for (let k = 0; k < actual.length;
k += RENDER_QUANTUM_FRAMES) {
// Verify that the k-rate output is constant over the render
// and that it matches the value of the a-rate value at the
// beginning of the render.
should(
actual.slice(k, k + RENDER_QUANTUM_FRAMES),
`k-rate ConstantSource.offset: output[${k}:${
k + RENDER_QUANTUM_FRAMES}]`)
.beConstantValueOf(expected[k]);
}
})
.then(() => task.done());
});
// This test verifies that a k-rate input to the ConstantSourceNode.offset
// works just as if we set the AudioParam to be k-rate. This is the basis
// of the following tests, so make sure it works.
audit.define(
{
label: 'ConstantSource.offset',
description: 'Verify k-rate automation matches k-rate input'
},
(task, should) => {
let context = new OfflineAudioContext({
numberOfChannels: 2,
sampleRate: sampleRate8k,
length: testDuration * sampleRate8k
});
let merger = new ChannelMergerNode(
context, {numberOfInputs: context.destination.channelCount});
merger.connect(context.destination);
let tstSrc = new ConstantSourceNode(context);
let tstMod = new ConstantSourceNode(context);
tstSrc.offset.automationRate = 'k-rate';
tstMod.offset.linearRampToValueAtTime(context.length, testDuration);
tstMod.connect(tstSrc.offset)
tstSrc.connect(merger, 0, 0);
let refSrc = new ConstantSourceNode(context);
let refMod = new ConstantSourceNode(context);
refMod.offset.linearRampToValueAtTime(context.length, testDuration);
refMod.offset.automationRate = 'k-rate';
refMod.connect(refSrc.offset);
refSrc.connect(merger, 0, 1);
tstSrc.start();
tstMod.start();
refSrc.start();
refMod.start();
context.startRendering()
.then(buffer => {
let actual = buffer.getChannelData(0);
let expected = buffer.getChannelData(1);
for (let k = 0; k < context.length;
k += RENDER_QUANTUM_FRAMES) {
should(
actual.slice(k, k + RENDER_QUANTUM_FRAMES),
`ConstantSource.offset k-rate input: output[${k}:${
k + RENDER_QUANTUM_FRAMES}]`)
.beConstantValueOf(expected[k]);
}
})
.then(() => task.done());
});
audit.run();
</script>
</body>
......
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