Commit 7e979350 authored by Raymond Toy's avatar Raymond Toy Committed by Commit Bot

Handle k-rate AudioParam inputs for PannerNode

As with other fixes, use HasSampleAccurateValuesTimeline() to determine
if there are sample-accurate values which is either caused by timeline
events or connected inputs to the AudioParam.  Added IsAudioRate() if
any AudioParam is k-rate and use that to determine how to handle the
AudioParam.

This applies to the 6 AudioParams of a PannerNode, and the 9 AudioParams
for an AudioListener since they're closely coupled.

Performance impact is negligible, based on Spotify's benchmark.

Without this CL (macbook pro):
TEST	                μs	MIN	Q1	MEDIAN	Q3	MAX
Baseline	        495	495	530	550	556	1765
Panner-equalpower	359	359	377	395	399	1186
Panner-HRTF	        948	948	1023	1119	1135	7163

With this CL:
TEST	                μs	MIN	Q1	MEDIAN	Q3	MAX
Baseline	        496	496	545	558	578	8654
Panner-equalpower	359	359	377	395	399	611
Panner-HRTF	        941	941	1021	1119	1134	8270

Bug: 1015760
Change-Id: Iea0ef37ae07c7113c8ab9cc3e1cb47e6179a151e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2096786
Commit-Queue: Raymond Toy <rtoy@chromium.org>
Reviewed-by: default avatarHongchan Choi <hongchan@chromium.org>
Cr-Commit-Position: refs/heads/master@{#756259}
parent 04f8669a
......@@ -150,15 +150,26 @@ void AudioListener::RemovePanner(PannerHandler& panner) {
}
bool AudioListener::HasSampleAccurateValues() const {
return positionX()->Handler().HasSampleAccurateValues() ||
positionY()->Handler().HasSampleAccurateValues() ||
positionZ()->Handler().HasSampleAccurateValues() ||
forwardX()->Handler().HasSampleAccurateValues() ||
forwardY()->Handler().HasSampleAccurateValues() ||
forwardZ()->Handler().HasSampleAccurateValues() ||
upX()->Handler().HasSampleAccurateValues() ||
upY()->Handler().HasSampleAccurateValues() ||
upZ()->Handler().HasSampleAccurateValues();
return positionX()->Handler().HasSampleAccurateValuesTimeline() ||
positionY()->Handler().HasSampleAccurateValuesTimeline() ||
positionZ()->Handler().HasSampleAccurateValuesTimeline() ||
forwardX()->Handler().HasSampleAccurateValuesTimeline() ||
forwardY()->Handler().HasSampleAccurateValuesTimeline() ||
forwardZ()->Handler().HasSampleAccurateValuesTimeline() ||
upX()->Handler().HasSampleAccurateValuesTimeline() ||
upY()->Handler().HasSampleAccurateValuesTimeline() ||
upZ()->Handler().HasSampleAccurateValuesTimeline();
}
bool AudioListener::IsAudioRate() const {
return positionX()->Handler().IsAudioRate() ||
positionY()->Handler().IsAudioRate() ||
positionZ()->Handler().IsAudioRate() ||
forwardX()->Handler().IsAudioRate() ||
forwardY()->Handler().IsAudioRate() ||
forwardZ()->Handler().IsAudioRate() ||
upX()->Handler().IsAudioRate() || upY()->Handler().IsAudioRate() ||
upZ()->Handler().IsAudioRate();
}
void AudioListener::UpdateValuesIfNeeded(uint32_t frames_to_process) {
......
......@@ -69,6 +69,10 @@ class AudioListener : public ScriptWrappable, public InspectorHelperMixin {
// True if any of AudioParams have automations.
bool HasSampleAccurateValues() const;
// True if any of the AudioParams are set for a-rate automations
// (the default).
bool IsAudioRate() const;
// Update the internal state of the listener, including updating the dirty
// state of all PannerNodes if necessary.
void UpdateState();
......
......@@ -271,7 +271,11 @@ void AudioParamHandler::CalculateFinalValues(float* values,
if (NumberOfRenderingConnections() > 0) {
DCHECK_LE(number_of_values, audio_utilities::kRenderQuantumFrames);
summing_bus_->SetChannelMemory(0, values, number_of_values);
// If we're not sample accurate, we only need one value, so make the summing
// bus have length 1. When the connections are added in, only the first
// value will be added. Which is exactly what we want.
summing_bus_->SetChannelMemory(0, values,
sample_accurate ? number_of_values : 1);
for (unsigned i = 0; i < NumberOfRenderingConnections(); ++i) {
AudioNodeOutput* output = RenderingOutput(i);
......@@ -285,6 +289,14 @@ void AudioParamHandler::CalculateFinalValues(float* values,
summing_bus_->SumFrom(*connection_bus);
}
// If we're not sample accurate, duplicate the first element of |values| to
// all of the elements.
if (!sample_accurate) {
for (unsigned k = 0; k < number_of_values; ++k) {
values[k] = values[0];
}
}
// Clamp the values now to the nominal range
float min_value = MinValue();
float max_value = MaxValue();
......
......@@ -192,7 +192,8 @@ void PannerHandler::Process(uint32_t frames_to_process) {
Listener()->WaitForHRTFDatabaseLoaderThreadCompletion();
}
if (HasSampleAccurateValues() || Listener()->HasSampleAccurateValues()) {
if ((HasSampleAccurateValues() || Listener()->HasSampleAccurateValues()) &&
(IsAudioRate() || Listener()->IsAudioRate())) {
// It's tempting to skip sample-accurate processing if
// isAzimuthElevationDirty() and isDistanceConeGain() both return false.
// But in general we can't because something may scheduled to start in the
......@@ -330,10 +331,9 @@ void PannerHandler::Initialize() {
Listener()->HrtfDatabaseLoader());
Listener()->AddPanner(*this);
// Set the cached values to the current values to start things off. The
// panner is already marked as dirty, so this won't matter.
last_position_ = GetPosition();
last_orientation_ = Orientation();
// The panner is already marked as dirty, so |last_position_| and
// |last_orientation_| will bet updated on first use. Don't need to
// set them here.
AudioHandler::Initialize();
}
......@@ -710,12 +710,18 @@ void PannerHandler::SetChannelCountMode(const String& mode,
}
bool PannerHandler::HasSampleAccurateValues() const {
return position_x_->HasSampleAccurateValues() ||
position_y_->HasSampleAccurateValues() ||
position_z_->HasSampleAccurateValues() ||
orientation_x_->HasSampleAccurateValues() ||
orientation_y_->HasSampleAccurateValues() ||
orientation_z_->HasSampleAccurateValues();
return position_x_->HasSampleAccurateValuesTimeline() ||
position_y_->HasSampleAccurateValuesTimeline() ||
position_z_->HasSampleAccurateValuesTimeline() ||
orientation_x_->HasSampleAccurateValuesTimeline() ||
orientation_y_->HasSampleAccurateValuesTimeline() ||
orientation_z_->HasSampleAccurateValuesTimeline();
}
bool PannerHandler::IsAudioRate() const {
return position_x_->IsAudioRate() || position_y_->IsAudioRate() ||
position_z_->IsAudioRate() || orientation_x_->IsAudioRate() ||
orientation_y_->IsAudioRate() || orientation_z_->IsAudioRate();
}
void PannerHandler::UpdateDirtyState() {
......
......@@ -176,18 +176,34 @@ class PannerHandler final : public AudioHandler {
float cached_distance_cone_gain_;
const FloatPoint3D GetPosition() const {
return FloatPoint3D(position_x_->Value(), position_y_->Value(),
position_z_->Value());
auto x = position_x_->IsAudioRate() ? position_x_->FinalValue()
: position_x_->Value();
auto y = position_y_->IsAudioRate() ? position_y_->FinalValue()
: position_y_->Value();
auto z = position_z_->IsAudioRate() ? position_z_->FinalValue()
: position_z_->Value();
return FloatPoint3D(x, y, z);
}
const FloatPoint3D Orientation() const {
return FloatPoint3D(orientation_x_->Value(), orientation_y_->Value(),
orientation_z_->Value());
auto x = orientation_x_->IsAudioRate() ? orientation_x_->FinalValue()
: orientation_x_->Value();
auto y = orientation_y_->IsAudioRate() ? orientation_y_->FinalValue()
: orientation_y_->Value();
auto z = orientation_z_->IsAudioRate() ? orientation_z_->FinalValue()
: orientation_z_->Value();
return FloatPoint3D(x, y, z);
}
// True if any of this panner's AudioParams have automations.
bool HasSampleAccurateValues() const;
// True if any of the panner's AudioParams are set for a-rate
// automations (the default).
bool IsAudioRate() const;
scoped_refptr<AudioParamHandler> position_x_;
scoped_refptr<AudioParamHandler> position_y_;
scoped_refptr<AudioParamHandler> position_z_;
......
<!doctype html>
<html>
<head>
<title>
k-rate AudioParams with inputs for PannerNode
</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/webaudio/resources/audit.js"></script>
<script src="/webaudio/resources/audit-util.js"></script>
</title>
</head>
<body>
<script>
let audit = Audit.createTaskRunner();
audit.define(
{label: 'Panner x', description: 'k-rate input'},
async (task, should) => {
await testPannerParams(should, {param: 'positionX'});
task.done();
});
audit.define(
{label: 'Panner y', description: 'k-rate input'},
async (task, should) => {
await testPannerParams(should, {param: 'positionY'});
task.done();
});
audit.define(
{label: 'Panner z', description: 'k-rate input'},
async (task, should) => {
await testPannerParams(should, {param: 'positionZ'});
task.done();
});
audit.define(
{label: 'Listener x', description: 'k-rate input'},
async (task, should) => {
await testListenerParams(should, {param: 'positionX'});
task.done();
});
audit.define(
{label: 'Listener y', description: 'k-rate input'},
async (task, should) => {
await testListenerParams(should, {param: 'positionY'});
task.done();
});
audit.define(
{label: 'Listener z', description: 'k-rate input'},
async (task, should) => {
await testListenerParams(should, {param: 'positionZ'});
task.done();
});
audit.run();
async function testPannerParams(should, options) {
// Arbitrary sample rate and duration.
const sampleRate = 8000;
const testFrames = 5 * RENDER_QUANTUM_FRAMES;
let testDuration = testFrames / sampleRate;
// Four channels needed because the first two are for the output of
// the reference panner, and the next two are for the test panner.
let context = new OfflineAudioContext({
numberOfChannels: 4,
sampleRate: sampleRate,
length: testDuration * sampleRate
});
let merger = new ChannelMergerNode(
context, {numberOfInputs: context.destination.channelCount});
merger.connect(context.destination);
// Create a stereo source out of two mono sources
let src0 = new ConstantSourceNode(context, {offset: 1});
let src1 = new ConstantSourceNode(context, {offset: 2});
let src = new ChannelMergerNode(context, {numberOfInputs: 2});
src0.connect(src, 0, 0);
src1.connect(src, 0, 1);
let finalPosition = 100;
// Reference panner node with k-rate AudioParam automations. The
// output of this panner is the reference output.
let refNode = new PannerNode(context);
// Initialize the panner location to somewhat arbitrary values.
refNode.positionX.value = 1;
refNode.positionY.value = 50;
refNode.positionZ.value = -25;
// Set the AudioParam under test with the appropriate automations.
refNode[options.param].automationRate = 'k-rate';
refNode[options.param].setValueAtTime(1, 0);
refNode[options.param].linearRampToValueAtTime(
finalPosition, testDuration);
let refSplit = new ChannelSplitterNode(context, {numberOfOutputs: 2});
// Test panner node with k-rate AudioParam with inputs.
let tstNode = new PannerNode(context);
tstNode.positionX.value = 1;
tstNode.positionY.value = 50;
tstNode.positionZ.value = -25;
tstNode[options.param].value = 0;
tstNode[options.param].automationRate = 'k-rate';
let tstSplit = new ChannelSplitterNode(context, {numberOfOutputs: 2});
// The input to the AudioParam. It must have the same automation
// sequence as used by refNode. And must be a-rate to demonstrate
// the k-rate effect of the AudioParam.
let mod = new ConstantSourceNode(context, {offset: 0});
mod.offset.setValueAtTime(1, 0);
mod.offset.linearRampToValueAtTime(finalPosition, testDuration);
mod.connect(tstNode[options.param]);
src.connect(refNode).connect(refSplit);
src.connect(tstNode).connect(tstSplit);
refSplit.connect(merger, 0, 0);
refSplit.connect(merger, 1, 1);
tstSplit.connect(merger, 0, 2);
tstSplit.connect(merger, 1, 3);
mod.start();
src0.start();
src1.start();
const buffer = await context.startRendering();
let expected0 = buffer.getChannelData(0);
let expected1 = buffer.getChannelData(1);
let actual0 = buffer.getChannelData(2);
let actual1 = buffer.getChannelData(3);
should(expected0, 'Expected output channel 0')
.notBeConstantValueOf(expected0[0]);
should(expected1, 'Expected output channel 1')
.notBeConstantValueOf(expected1[0]);
// Verify output is a stair step because positionX is k-rate,
// and no other AudioParam is changing.
for (let k = 0; k < testFrames; k += RENDER_QUANTUM_FRAMES) {
should(
actual0.slice(k, k + RENDER_QUANTUM_FRAMES),
`Channel 0 output[${k}, ${k + RENDER_QUANTUM_FRAMES - 1}]`)
.beConstantValueOf(actual0[k]);
}
for (let k = 0; k < testFrames; k += RENDER_QUANTUM_FRAMES) {
should(
actual1.slice(k, k + RENDER_QUANTUM_FRAMES),
`Channel 1 output[${k}, ${k + RENDER_QUANTUM_FRAMES - 1}]`)
.beConstantValueOf(actual1[k]);
}
should(actual0, 'Actual output channel 0').beCloseToArray(expected0, {
absoluteThreshold: 0
});
should(actual1, 'Actual output channel 1').beCloseToArray(expected1, {
absoluteThreshold: 0
});
}
async function testListenerParams(should, options) {
// Arbitrary sample rate and duration.
const sampleRate = 8000;
const testFrames = 5 * RENDER_QUANTUM_FRAMES;
let testDuration = testFrames / sampleRate;
// Four channels needed because the first two are for the output of
// the reference panner, and the next two are for the test panner.
let context = new OfflineAudioContext({
numberOfChannels: 2,
sampleRate: sampleRate,
length: testDuration * sampleRate
});
// Create a stereo source out of two mono sources
let src0 = new ConstantSourceNode(context, {offset: 1});
let src1 = new ConstantSourceNode(context, {offset: 2});
let src = new ChannelMergerNode(context, {numberOfInputs: 2});
src0.connect(src, 0, 0);
src1.connect(src, 0, 1);
let finalPosition = 100;
// Reference panner node with k-rate AudioParam automations. The
// output of this panner is the reference output.
let panner = new PannerNode(context);
panner.positionX.value = 10;
panner.positionY.value = 50;
panner.positionZ.value = -25;
src.connect(panner);
let mod = new ConstantSourceNode(context, {offset: 0});
mod.offset.setValueAtTime(1, 0);
mod.offset.linearRampToValueAtTime(finalPosition, testDuration);
context.listener[options.param].automationRate = 'k-rate';
mod.connect(context.listener[options.param]);
panner.connect(context.destination);
src0.start();
src1.start();
mod.start();
const buffer = await context.startRendering();
let c0 = buffer.getChannelData(0);
let c1 = buffer.getChannelData(1);
// Verify output is a stair step because positionX is k-rate,
// and no other AudioParam is changing.
for (let k = 0; k < testFrames; k += RENDER_QUANTUM_FRAMES) {
should(
c0.slice(k, k + RENDER_QUANTUM_FRAMES),
`Channel 0 output[${k}, ${k + RENDER_QUANTUM_FRAMES - 1}]`)
.beConstantValueOf(c0[k]);
}
for (let k = 0; k < testFrames; k += RENDER_QUANTUM_FRAMES) {
should(
c1.slice(k, k + RENDER_QUANTUM_FRAMES),
`Channel 1 output[${k}, ${k + RENDER_QUANTUM_FRAMES - 1}]`)
.beConstantValueOf(c1[k]);
}
}
</script>
</body>
</html>
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