Commit 54d2a2da authored by Guido Urdaneta's avatar Guido Urdaneta Committed by Commit Bot

Revert "The SnooperNode: Audio loopback for a single stream."

This reverts commit f1b4c43b.

Reason for revert: Causes reliable failures on  Win10 Tests x64 (dbg) bot

Sample failed runs:
https://ci.chromium.org/p/chromium/builders/luci.chromium.ci/Win10%20Tests%20x64%20%28dbg%29/413
https://ci.chromium.org/p/chromium/builders/luci.chromium.ci/Win10%20Tests%20x64%20%28dbg%29/414

Sample logs:
[ RUN      ] SnooperNodeTest.ContinuousAudioFlowAdaptsToSkew/1
[ RUN      ] SnooperNodeTest.ContinuousAudioFlowAdaptsToSkew/1
[ RUN      ] SnooperNodeTest.ContinuousAudioFlowAdaptsToSkew/1
[ RUN      ] SnooperNodeTest.ContinuousAudioFlowAdaptsToSkew/1


Original change's description:
> The SnooperNode: Audio loopback for a single stream.
> 
> An audio::GroupMember::Snooper that records the audio from a GroupMember
> on one thread, and re-renders it to the desired output format on another
> thread. Since the data flow rates are known to be driven by different
> clocks (audio hardware clock versus system clock), the SnooperNode also
> uses its resampler to compensate for skew and re-synchronize the audio
> going into and out of it.
> 
> Bug: 824019
> Change-Id: I87d410724fd00f9372232bfffdfbb89ada0b3de8
> Reviewed-on: https://chromium-review.googlesource.com/1041657
> Commit-Queue: Yuri Wiitala <miu@chromium.org>
> Reviewed-by: Xiangjun Zhang <xjz@chromium.org>
> Reviewed-by: Chrome Cunningham <chcunningham@chromium.org>
> Reviewed-by: Olga Sharonova <olka@chromium.org>
> Cr-Commit-Position: refs/heads/master@{#556719}

TBR=miu@chromium.org,chcunningham@chromium.org,olka@chromium.org,xjz@chromium.org

Change-Id: I47750e3abb5e9387733b5fbf275d930a79a6d45e
No-Presubmit: true
No-Tree-Checks: true
No-Try: true
Bug: 824019
Reviewed-on: https://chromium-review.googlesource.com/1049905Reviewed-by: default avatarGuido Urdaneta <guidou@chromium.org>
Commit-Queue: Guido Urdaneta <guidou@chromium.org>
Cr-Commit-Position: refs/heads/master@{#556767}
parent 98b10b51
...@@ -42,20 +42,12 @@ void ChannelMixer::Initialize( ...@@ -42,20 +42,12 @@ void ChannelMixer::Initialize(
ChannelMixer::~ChannelMixer() = default; ChannelMixer::~ChannelMixer() = default;
void ChannelMixer::Transform(const AudioBus* input, AudioBus* output) { void ChannelMixer::Transform(const AudioBus* input, AudioBus* output) {
CHECK_EQ(input->frames(), output->frames());
TransformPartial(input, input->frames(), output);
}
void ChannelMixer::TransformPartial(const AudioBus* input,
int frame_count,
AudioBus* output) {
CHECK_EQ(matrix_.size(), static_cast<size_t>(output->channels())); CHECK_EQ(matrix_.size(), static_cast<size_t>(output->channels()));
CHECK_EQ(matrix_[0].size(), static_cast<size_t>(input->channels())); CHECK_EQ(matrix_[0].size(), static_cast<size_t>(input->channels()));
CHECK_LE(frame_count, input->frames()); CHECK_EQ(input->frames(), output->frames());
CHECK_LE(frame_count, output->frames());
// Zero initialize |output| so we're accumulating from zero. // Zero initialize |output| so we're accumulating from zero.
output->ZeroFrames(frame_count); output->Zero();
// If we're just remapping we can simply copy the correct input to output. // If we're just remapping we can simply copy the correct input to output.
if (remapping_) { if (remapping_) {
...@@ -65,7 +57,7 @@ void ChannelMixer::TransformPartial(const AudioBus* input, ...@@ -65,7 +57,7 @@ void ChannelMixer::TransformPartial(const AudioBus* input,
if (scale > 0) { if (scale > 0) {
DCHECK_EQ(scale, 1.0f); DCHECK_EQ(scale, 1.0f);
memcpy(output->channel(output_ch), input->channel(input_ch), memcpy(output->channel(output_ch), input->channel(input_ch),
sizeof(*output->channel(output_ch)) * frame_count); sizeof(*output->channel(output_ch)) * output->frames());
break; break;
} }
} }
...@@ -79,7 +71,7 @@ void ChannelMixer::TransformPartial(const AudioBus* input, ...@@ -79,7 +71,7 @@ void ChannelMixer::TransformPartial(const AudioBus* input,
// Scale should always be positive. Don't bother scaling by zero. // Scale should always be positive. Don't bother scaling by zero.
DCHECK_GE(scale, 0); DCHECK_GE(scale, 0);
if (scale > 0) { if (scale > 0) {
vector_math::FMAC(input->channel(input_ch), scale, frame_count, vector_math::FMAC(input->channel(input_ch), scale, output->frames(),
output->channel(output_ch)); output->channel(output_ch));
} }
} }
......
...@@ -35,14 +35,6 @@ class MEDIA_EXPORT ChannelMixer { ...@@ -35,14 +35,6 @@ class MEDIA_EXPORT ChannelMixer {
// Transforms all channels from |input| into |output| channels. // Transforms all channels from |input| into |output| channels.
void Transform(const AudioBus* input, AudioBus* output); void Transform(const AudioBus* input, AudioBus* output);
// Transforms all channels from |input| into |output| channels, for just the
// initial part of the input. Callers can use this to avoid reallocating
// AudioBuses, if the length of the data changes frequently for their use
// case.
void TransformPartial(const AudioBus* input,
int frame_count,
AudioBus* output);
private: private:
void Initialize(ChannelLayout input_layout, int input_channels, void Initialize(ChannelLayout input_layout, int input_channels,
ChannelLayout output_layout, int output_channels); ChannelLayout output_layout, int output_channels);
......
...@@ -53,8 +53,6 @@ source_set("lib") { ...@@ -53,8 +53,6 @@ source_set("lib") {
"service.h", "service.h",
"service_factory.cc", "service_factory.cc",
"service_factory.h", "service_factory.h",
"snooper_node.cc",
"snooper_node.h",
"stream_factory.cc", "stream_factory.cc",
"stream_factory.h", "stream_factory.h",
"sync_reader.cc", "sync_reader.cc",
...@@ -84,7 +82,6 @@ source_set("tests") { ...@@ -84,7 +82,6 @@ source_set("tests") {
"local_muter_unittest.cc", "local_muter_unittest.cc",
"output_controller_unittest.cc", "output_controller_unittest.cc",
"output_stream_unittest.cc", "output_stream_unittest.cc",
"snooper_node_unittest.cc",
"stream_factory_unittest.cc", "stream_factory_unittest.cc",
"sync_reader_unittest.cc", "sync_reader_unittest.cc",
"test/audio_system_to_service_adapter_test.cc", "test/audio_system_to_service_adapter_test.cc",
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "services/audio/snooper_node.h"
#include <algorithm>
#include <cmath>
#include "base/bind.h"
#include "base/numerics/checked_math.h"
#include "media/base/audio_bus.h"
#include "media/base/audio_timestamp_helper.h"
using Helper = media::AudioTimestampHelper;
namespace audio {
namespace {
// The delay buffer size is chosen to be a very conservative maximum, just to
// make sure there is an upper bound in-place so that the buffer won't grow
// indefinitely. In most normal cases, reads will cause the delay buffer to
// automatically prune its recording down to well under this maximum (e.g.,
// around 100 milliseconds of audio).
constexpr base::TimeDelta kDelayBufferSize =
base::TimeDelta::FromMilliseconds(1000);
// A frequency at which people cannot discern tones that differ by 1 Hz. This is
// based on research that shows people can discern tones only when they are more
// than 1 Hz away from 500 Hz. Thus, assume people definitely can't discern
// tones 1 Hz away from 1000 Hz.
constexpr int kStepBasisHz = 1000;
// The number of frames the resampler should request at a time. Three kernel's
// worth is an arbitrary choice, but performs well since the lock guarding
// access to the delay buffer is only held a reasonably short time during the
// data extraction.
constexpr int kResamplerRequestSize = 3 * media::SincResampler::kKernelSize;
} // namespace
// static
constexpr SnooperNode::FrameTicks SnooperNode::kNullPosition;
// static
constexpr SnooperNode::FrameTicks SnooperNode::kWriteStartPosition;
SnooperNode::SnooperNode(const media::AudioParameters& input_params,
const media::AudioParameters& output_params)
: input_params_(input_params),
output_params_(output_params),
input_bus_duration_(
Helper::FramesToTime(input_params_.frames_per_buffer(),
input_params_.sample_rate())),
output_bus_duration_(
Helper::FramesToTime(output_params_.frames_per_buffer(),
output_params_.sample_rate())),
perfect_io_ratio_(static_cast<double>(input_params_.sample_rate()) /
output_params_.sample_rate()),
buffer_(
Helper::TimeToFrames(kDelayBufferSize, input_params_.sample_rate())),
write_position_(kNullPosition),
read_position_(kNullPosition),
correction_fps_(0),
resampler_(
// For efficiency, a |channel_mix_strategy_| is chosen so that the
// resampler is always processing the fewest number of channels.
std::min(input_params_.channels(), output_params_.channels()),
perfect_io_ratio_,
kResamplerRequestSize,
base::BindRepeating(&SnooperNode::ReadFromDelayBuffer,
base::Unretained(this))),
channel_mix_strategy_(
(input_params_.channel_layout() == output_params_.channel_layout())
? kNone
: ((output_params_.channels() < input_params_.channels())
? kBefore
: kAfter)),
channel_mixer_(input_params_.channel_layout(),
output_params_.channel_layout()) {
// Prime the resampler with silence to keep the calculations in Render()
// simple.
resampler_.PrimeWithSilence();
// If channel mixing is to be performed after resampling, allocate a buffer to
// hold the resampler's output.
if (channel_mix_strategy_ == kAfter) {
mix_bus_ = media::AudioBus::Create(input_params_.channels(),
output_params_.frames_per_buffer());
}
}
SnooperNode::~SnooperNode() = default;
void SnooperNode::OnData(const media::AudioBus& input_bus,
base::TimeTicks reference_time,
double volume) {
DCHECK_EQ(input_bus.channels(), input_params_.channels());
DCHECK_EQ(input_bus.frames(), input_params_.frames_per_buffer());
base::AutoLock scoped_lock(lock_);
// If this is the first OnData() call, just set the starting read position.
// Otherwise, check whether a gap (i.e., missing piece) in the recording flow
// has occurred, and skip the write position forward if necessary.
if (write_position_ == kNullPosition) {
write_position_ = kWriteStartPosition;
} else {
const base::TimeDelta delta = reference_time - write_reference_time_;
if (delta >= input_bus_duration_) {
write_position_ +=
Helper::TimeToFrames(delta, input_params_.sample_rate());
}
}
buffer_.Write(write_position_, input_bus, volume);
write_position_ += input_bus.frames();
write_reference_time_ = reference_time + input_bus_duration_;
}
void SnooperNode::Render(base::TimeTicks reference_time,
media::AudioBus* output_bus) {
DCHECK_EQ(output_bus->channels(), output_params_.channels());
DCHECK_EQ(output_bus->frames(), output_params_.frames_per_buffer());
// Use the difference in reference times between OnData() and Render() to
// estimate the position of the audio about to come out of the resampler.
lock_.Acquire();
const FrameTicks estimated_output_position =
(write_position_ == kNullPosition)
? kNullPosition
: (write_position_ +
Helper::TimeToFrames(reference_time - write_reference_time_,
input_params_.sample_rate()));
lock_.Release();
// If recording has not started, just output silence.
if (estimated_output_position == kNullPosition) {
output_bus->Zero();
return;
}
// If this is the first Render() call after recording started, just initialize
// the starting read position. For all successive calls, adjust the resampler
// to account for drift, and also handle any significant time gaps between
// Render() calls.
if (read_position_ == kNullPosition) {
// Walk backwards from the estimated output position to initialize the read
// position.
read_position_ =
estimated_output_position + std::lround(resampler_.BufferedFrames());
DCHECK_EQ(correction_fps_, 0);
} else {
const base::TimeDelta delta = reference_time - render_reference_time_;
if (delta < output_bus_duration_) { // Normal case: No gap.
// Compute the drift, which is the number of frames the resampler is
// behind in reading from the delay buffer. This calculation also accounts
// for the frames buffered within the resampler.
const int64_t actual_output_position =
read_position_ - std::lround(resampler_.BufferedFrames());
const int drift = base::checked_cast<int>(estimated_output_position -
actual_output_position);
// The goal is to have zero drift, and the target is to achieve that goal
// in approximately one second.
const int target_correction_fps = drift; // Drift divided by 1 second.
// The minimum amount to step-up or step-down the correction rate. Using
// this prevents excessive "churn" within the resampler, where otherwise
// it would be recomputing its convolution kernel too often.
const int fps_step = input_params_.sample_rate() / kStepBasisHz;
// Adjust the correction rate (and resampling ratio) based on how
// different the target correction FPS is from the current correction
// FPS. If more than two steps away, make an aggressive adjustment. If
// only more than one step away, nudge the current rate by just one
// step. Otherwise, leave the current rate unchanged.
const int diff = target_correction_fps - correction_fps_;
if (std::abs(diff) > 2 * fps_step) {
UpdateCorrectionRate(target_correction_fps);
} else if (diff > fps_step) {
UpdateCorrectionRate(correction_fps_ + fps_step);
} else if (diff < -fps_step) {
UpdateCorrectionRate(correction_fps_ - fps_step);
} else {
// No correction necessary.
}
} else /* if (delta >= threshold) */ { // Gap detected.
// Rather than flush and re-prime the resampler, just skip-ahead its next
// read-from position.
read_position_ +=
Helper::TimeToFrames(delta, input_params_.sample_rate());
// This special event casts doubt on the validity of the current
// correction rates. The system is likely to behave differently going
// forward. Thus, set a zero correction rate.
UpdateCorrectionRate(0);
}
}
// Perform resampling and also channel mixing, if required. The resampler will
// call ReadFromDelayBuffer(), as needed, to supply itself with more input
// data; and this will move the |read_position_| forward.
if (channel_mix_strategy_ == kAfter) {
resampler_.Resample(mix_bus_->frames(), mix_bus_.get());
channel_mixer_.Transform(mix_bus_.get(), output_bus);
} else {
resampler_.Resample(output_bus->frames(), output_bus);
}
render_reference_time_ = reference_time + output_bus_duration_;
}
void SnooperNode::UpdateCorrectionRate(int correction_fps) {
correction_fps_ = correction_fps;
const double ratio_adjustment =
static_cast<double>(correction_fps_) / input_params_.sample_rate();
DCHECK_GT(ratio_adjustment, -perfect_io_ratio_);
resampler_.SetRatio(perfect_io_ratio_ + ratio_adjustment);
}
void SnooperNode::ReadFromDelayBuffer(int ignored,
media::AudioBus* resampler_bus) {
DCHECK_NE(read_position_, kNullPosition);
const int frames_to_read = resampler_bus->frames();
if (channel_mix_strategy_ == kBefore) {
DCHECK_EQ(resampler_bus->channels(), output_params_.channels());
// Reallocate the |mix_bus_| if needed.
if (!mix_bus_ || mix_bus_->frames() < frames_to_read) {
mix_bus_ = nullptr; // Free memory before allocating more.
mix_bus_ =
media::AudioBus::Create(input_params_.channels(), frames_to_read);
}
// Do the read and also channel remix before resampling.
lock_.Acquire();
buffer_.Read(read_position_, frames_to_read, mix_bus_.get());
lock_.Release();
channel_mixer_.TransformPartial(mix_bus_.get(), frames_to_read,
resampler_bus);
} else {
DCHECK_EQ(resampler_bus->channels(), input_params_.channels());
lock_.Acquire();
buffer_.Read(read_position_, frames_to_read, resampler_bus);
lock_.Release();
}
read_position_ += frames_to_read;
}
} // namespace audio
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef SERVICES_AUDIO_SNOOPER_NODE_H_
#define SERVICES_AUDIO_SNOOPER_NODE_H_
#include <limits>
#include <memory>
#include "base/macros.h"
#include "base/synchronization/lock.h"
#include "base/time/time.h"
#include "media/base/audio_parameters.h"
#include "media/base/channel_mixer.h"
#include "media/base/multi_channel_resampler.h"
#include "services/audio/delay_buffer.h"
#include "services/audio/group_member.h"
namespace media {
class AudioBus;
} // namespace media
namespace audio {
// Thread-safe implementation of Snooper that records the audio from a
// GroupMember on one thread, and re-renders it to the desired output format on
// another thread. Since the data flow rates are known to be driven by different
// clocks (audio hardware clock versus system clock), the base::TimeTicks
// reference clock is used to detect drift and automatically correct for it to
// maintain proper synchronization.
//
// Throughout this class, there are sample counters (in terms of the input
// audio's sample rate) that are tracked/computed. They refer to the media
// timestamp of the audio flowing through specific parts of the processing
// pipeline: inbound from OnData() calls → through the delay buffer → through
// the resampler → and outbound via Render() calls:
//
// write position: The position of audio about to be written into the delay
// buffer. This is managed by OnData().
// read position: The position of audio about to be read from the delay
// buffer and pushed into the resampler. This is managed by
// ReadFromDelayBuffer().
// output position: The position of the audio about to come out of the
// resampler. This is computed within Render(). Note that
// this is a "virtual" position since it is in terms of the
// input audio's sample count, but refers to audio about to
// be generated in the output format (with a possibly
// different sample rate).
//
// Note that the media timestamps represented by the "positions," as well as the
// surrounding math operations, might seem backwards; but they are not. This is
// because the inbound audio is from a source that pre-renders audio for playout
// in the near future, while the outbound audio is audio that would have been
// played-out in the recent past.
class SnooperNode : public GroupMember::Snooper {
public:
// Use sample counts as a precise measure of audio signal position and time
// duration.
using FrameTicks = int64_t;
// Contruct a SnooperNode that buffers input of one format and renders output
// in [possibly] another format.
SnooperNode(const media::AudioParameters& input_params,
const media::AudioParameters& output_params);
~SnooperNode() final;
// GroupMember::Snooper implementation. Inserts more data into the delay
// buffer.
void OnData(const media::AudioBus& input_bus,
base::TimeTicks reference_time,
double volume) final;
// Renders more audio that was recorded from the GroupMember until
// |output_bus| is filled, resampling and remixing the channels if necessary.
// |reference_time| is used for detecting skip-ahead (i.e., a significant
// forward jump in the reference time) and also to maintain synchronization
// with the input.
void Render(base::TimeTicks reference_time, media::AudioBus* output_bus);
private:
// Helper to store the new |correction_fps|, recompute the resampling I/O
// ratio, and reconfigure the resampler with the new ratio.
void UpdateCorrectionRate(int correction_fps);
// Called by the MultiChannelResampler to acquire more data from the delay
// buffer. This is invoked in the same call stack (and thread) as Render(),
// zero or more times as data is needed by the resampler.
void ReadFromDelayBuffer(int ignored, media::AudioBus* resampler_bus);
// Input and output audio parameters.
const media::AudioParameters input_params_;
const media::AudioParameters output_params_;
// Input and output AudioBus time durations, pre-computed from the input and
// output AudioParameters.
const base::TimeDelta input_bus_duration_;
const base::TimeDelta output_bus_duration_;
// The ratio between the input sampling rate and the output sampling rate. It
// is "perfect" because it assumes no clock skew. Corrections are applied to
// this to determine the actual resampler I/O ratio.
const double perfect_io_ratio_;
// Protects concurrent access to |buffer_| and the |write_position_| and
// |write_reference_time_|. All other members are either read-only, or are not
// accessed by multiple threads.
base::Lock lock_;
// Allows input data to be recorded and then read-back from any position
// later (by the resampler).
DelayBuffer buffer_; // Guarded by |lock_|.
// The next frame position at which to write into the delay buffer, and the
// TimeTicks representing its corresponding system clock timestamp.
FrameTicks write_position_; // Guarded by |lock_|.
base::TimeTicks write_reference_time_; // Guarded by |lock_|.
// The next frame position from which to read from the delay buffer. This is
// the position of the frames about to be pushed into the resampler, not the
// position of frames about to be Render()'ed.
FrameTicks read_position_;
// The expected |reference_time| to be provided in the next call to Render().
// This is used to detect skip-ahead in the output, and compensate when
// necessary.
base::TimeTicks render_reference_time_;
// The additional number of frames currently being consumed by the resampler
// each second to correct for drift.
int correction_fps_;
// Resamples input audio that is read from the delay buffer. Even if the input
// and output have the same sampling rate, this is used to subtly stretch the
// audio signal to correct for drift.
media::MultiChannelResampler resampler_;
// Specifies whether channel mixing should occur before or after resampling,
// or is not needed. The strategy is chosen such that the minimal number of
// channels are resampled, as resampling is the more-expensive operation.
enum { kBefore, kAfter, kNone } const channel_mix_strategy_;
// Only used when the input channel layout differs from the output.
media::ChannelMixer channel_mixer_;
// Only allocated when using the channel mixer. When using the kAfter
// strategy, it is allocated just once, in the constructor, since its frame
// length is constant. When using the kBefore strategy, it is re-allocated
// whenever a larger one is needed and is reused thereafter.
std::unique_ptr<media::AudioBus> mix_bus_;
// An impossible value re-purposed to represent the "null" or "not set yet"
// condition for |read_position_| and |write_position_|.
static constexpr FrameTicks kNullPosition =
std::numeric_limits<FrameTicks>::min();
// The frame position where recording into the delay buffer always starts.
static constexpr FrameTicks kWriteStartPosition = 0;
DISALLOW_COPY_AND_ASSIGN(SnooperNode);
};
} // namespace audio
#endif // SERVICES_AUDIO_SNOOPER_NODE_H_
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "services/audio/snooper_node.h"
#include <algorithm>
#include <memory>
#include <vector>
#include "base/memory/scoped_refptr.h"
#include "base/optional.h"
#include "base/test/test_mock_time_task_runner.h"
#include "media/base/audio_bus.h"
#include "media/base/audio_parameters.h"
#include "media/base/channel_layout.h"
#include "services/audio/test/fake_consumer.h"
#include "services/audio/test/fake_group_member.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace audio {
namespace {
// Used to test whether the output AudioBuses have had all their values set to
// something finite.
constexpr float kInvalidAudioSample = std::numeric_limits<float>::infinity();
// The tones the source should generate into the left and right channels.
constexpr double kLeftChannelFrequency = 500.0;
constexpr double kRightChannelFrequency = 1200.0;
constexpr double kSourceVolume = 0.5;
// The duration of the audio that flows through the SnooperNode for each test.
constexpr base::TimeDelta kTestDuration = base::TimeDelta::FromSeconds(10);
// The amount of time in the future where the inbound audio is being recorded.
// This simulates an audio output stream that has rendered audio that is
// scheduled to be played out in the near future.
constexpr base::TimeDelta kInputAdvanceTime =
base::TimeDelta::FromMilliseconds(2);
// The amount of time in the past from which outbound audio is being rendered.
// This simulates the loopback stream's "capture from the recent past" mode-of-
// operation.
constexpr base::TimeDelta kOutputDelayTime =
base::TimeDelta::FromMilliseconds(20);
// Test parameters.
struct InputAndOutputParams {
media::AudioParameters input;
media::AudioParameters output;
};
// Helper so that gtest can produce useful logging of the test parameters.
std::ostream& operator<<(std::ostream& out,
const InputAndOutputParams& test_params) {
return out << "{input=" << test_params.input.AsHumanReadableString()
<< ", output=" << test_params.output.AsHumanReadableString()
<< "}";
}
class SnooperNodeTest : public testing::TestWithParam<InputAndOutputParams> {
public:
// Positions is a vector containing positions (in terms of frames elasped
// since the first) where an AudioBus input or output task should not be
// scheduled. This simulates missing input or skipped consumption.
using Positions = std::vector<int>;
SnooperNodeTest() = default;
~SnooperNodeTest() override = default;
const media::AudioParameters& input_params() const {
return GetParam().input;
}
const media::AudioParameters& output_params() const {
return GetParam().output;
}
FakeGroupMember* group_member() { return &*group_member_; }
SnooperNode* node() { return &*node_; }
FakeConsumer* consumer() { return &*consumer_; }
void SetUp() override {
// Initialize a test clock and task runner. The starting TimeTicks value is
// "huge" to ensure time calculations are being tested for overflow cases.
task_runner_ = base::MakeRefCounted<base::TestMockTimeTaskRunner>(
base::Time(), base::TimeTicks() +
base::TimeDelta::FromMicroseconds(INT64_C(1) << 62));
}
void CreateNewPipeline() {
group_member_.emplace(base::UnguessableToken(), input_params());
group_member_->SetChannelTone(0, kLeftChannelFrequency);
if (input_params().channels() > 1) {
// Set the right channel to kRightChannelFrequency unless the test
// parameters call for stereo→mono channel down-mixing. In that case, just
// use kLeftChannelFrequency again, and the test will confirm that the
// amplitude is 2X because there were two source channels mixed into one.
group_member_->SetChannelTone(1, output_params().channels() == 1
? kLeftChannelFrequency
: kRightChannelFrequency);
}
group_member_->SetVolume(kSourceVolume);
node_.emplace(input_params(), output_params());
group_member_->StartSnooping(node());
consumer_.emplace(output_params().channels(),
output_params().sample_rate());
}
void ScheduleInputTasks(double skew, const Positions& drop_positions) {
CHECK(std::is_sorted(drop_positions.begin(), drop_positions.end()));
const base::TimeTicks start_time =
task_runner_->NowTicks() + kInputAdvanceTime;
const base::TimeTicks end_time = start_time + kTestDuration;
const double time_step = skew / input_params().sample_rate();
auto drop_it = drop_positions.begin();
for (int position = 0;; position += input_params().frames_per_buffer()) {
// If a drop point has been reached, do not schedule an input task.
if (drop_it != drop_positions.end() && *drop_it == position) {
++drop_it;
continue;
}
const base::TimeTicks next_time =
start_time + base::TimeDelta::FromSecondsD(position * time_step);
if (next_time >= end_time) {
break;
}
// FakeGroupMember pushes audio into the SnooperNode.
task_runner_->PostDelayedTask(
FROM_HERE,
base::BindOnce(&FakeGroupMember::RenderMoreAudio,
base::Unretained(group_member()), next_time),
next_time - start_time);
}
}
void ScheduleOutputTasks(double skew, const Positions& skip_positions) {
CHECK(std::is_sorted(skip_positions.begin(), skip_positions.end()));
const base::TimeTicks start_time =
task_runner_->NowTicks() - kOutputDelayTime;
const base::TimeTicks end_time = start_time + kTestDuration;
const double time_step = skew / output_params().sample_rate();
auto skip_it = skip_positions.begin();
for (int position = 0;; position += output_params().frames_per_buffer()) {
// If a skip point has been reached, do not schedule an output task.
if (skip_it != skip_positions.end() && *skip_it == position) {
++skip_it;
continue;
}
const base::TimeTicks next_time =
start_time + base::TimeDelta::FromSecondsD(position * time_step);
if (next_time >= end_time) {
break;
}
task_runner_->PostDelayedTask(
FROM_HERE,
base::BindOnce(
[](SnooperNodeTest* test, base::TimeTicks output_time) {
// Have the SnooperNode render more output data. Before that,
// assign invalid sample values to the AudioBus. Then, after the
// Render() call, confirm that every sample was overwritten in
// the output AudioBus.
const auto bus = media::AudioBus::Create(test->output_params());
for (int ch = 0; ch < bus->channels(); ++ch) {
std::fill_n(bus->channel(ch), bus->frames(),
kInvalidAudioSample);
}
test->node_->Render(output_time, bus.get());
for (int ch = 0; ch < bus->channels(); ++ch) {
EXPECT_FALSE(std::any_of(
bus->channel(ch), bus->channel(ch) + bus->frames(),
[](float x) { return x == kInvalidAudioSample; }))
<< " at output_time=" << output_time << ", ch=" << ch;
}
// Pass the output to the consumer to store for later analysis.
test->consumer_->Consume(*bus);
},
this, next_time),
next_time - start_time);
}
}
void RunAllPendingTasks() { task_runner_->FastForwardUntilNoTasksRemain(); }
private:
scoped_refptr<base::TestMockTimeTaskRunner> task_runner_;
base::Optional<FakeGroupMember> group_member_;
base::Optional<SnooperNode> node_;
base::Optional<FakeConsumer> consumer_;
};
TEST_P(SnooperNodeTest, ContinuousAudioFlowAdaptsToSkew) {
// Note: A skew of 0.999 or 1.001 is very extreme. This is like saying the
// clocks drift 1 ms for every second that goes by. If the implementation can
// handle that, it's very likely to do a perfect job in-the-wild.
for (double input_skew = 0.999; input_skew <= 1.001; input_skew += 0.0005) {
for (double output_skew = 0.999; output_skew <= 1.001;
output_skew += 0.0005) {
SCOPED_TRACE(testing::Message() << "input_skew=" << input_skew
<< ", output_skew=" << output_skew);
// Set up the components, schedule all audio generation and consumption
// tasks, and then run them.
CreateNewPipeline();
ScheduleInputTasks(input_skew, Positions());
ScheduleOutputTasks(output_skew, Positions());
RunAllPendingTasks();
// All rendering for points-in-time before the audio from the source was
// first recorded should be silence.
const double expected_end_of_silence_position =
((input_skew * kInputAdvanceTime.InSecondsF()) +
(output_skew * kOutputDelayTime.InSecondsF())) *
output_params().sample_rate();
const double frames_in_one_millisecond =
output_params().sample_rate() / 1000.0;
EXPECT_NEAR(expected_end_of_silence_position,
consumer()->FindEndOfSilence(0, 0),
frames_in_one_millisecond);
if (output_params().channels() > 1) {
EXPECT_NEAR(expected_end_of_silence_position,
consumer()->FindEndOfSilence(1, 0),
frames_in_one_millisecond);
}
// Analyze the recording in several places for the expected tones.
constexpr int kNumToneChecks = 16;
for (int i = 1; i <= kNumToneChecks; ++i) {
const int end_frame =
consumer()->GetRecordedFrameCount() * i / kNumToneChecks;
SCOPED_TRACE(testing::Message() << "end_frame=" << end_frame);
EXPECT_NEAR(
kSourceVolume,
consumer()->ComputeAmplitudeAt(0, kLeftChannelFrequency, end_frame),
0.01);
if (output_params().channels() > 1) {
const double freq = input_params().channels() == 1
? kLeftChannelFrequency
: kRightChannelFrequency;
EXPECT_NEAR(kSourceVolume,
consumer()->ComputeAmplitudeAt(1, freq, end_frame), 0.01);
}
}
if (HasFailure()) {
return;
}
}
}
}
TEST_P(SnooperNodeTest, HandlesMissingInput) {
// Compute drops to occur once per second for 1/4 second duration. Each drop
// position must be aligned to input_params().frames_per_buffer() for the
// heuristics in ScheduleInputTasks() to process these drop positions
// correctly.
Positions drop_positions;
const int input_frames_in_one_second = input_params().sample_rate();
const int input_frames_in_a_quarter_second = input_frames_in_one_second / 4;
int unaligned_drop_position = input_frames_in_one_second;
for (int gap = 0; gap < 5; ++gap) {
const int aligned_drop_position =
(unaligned_drop_position / input_params().frames_per_buffer()) *
input_params().frames_per_buffer();
const int end_position =
aligned_drop_position + input_frames_in_a_quarter_second;
for (int i = 0;; ++i) {
const int next_drop_position =
aligned_drop_position + i * input_params().frames_per_buffer();
if (next_drop_position >= end_position) {
break;
}
drop_positions.push_back(next_drop_position);
}
unaligned_drop_position += input_frames_in_one_second;
}
// Set up the components, schedule all audio generation and consumption tasks,
// and then run them.
CreateNewPipeline();
ScheduleInputTasks(1.0, drop_positions);
ScheduleOutputTasks(1.0, Positions());
RunAllPendingTasks();
// Check that there is silence in the drop positions, and that tones are
// present around the silent sections. The ranges are adjusted to be 20 ms
// away from the exact begin/end positions to account for a reasonable amount
// of variance in due to the input buffer intervals.
const int output_frames_in_one_second = output_params().sample_rate();
const int output_frames_in_a_quarter_second = output_frames_in_one_second / 4;
const int output_frames_in_20_milliseconds =
output_frames_in_one_second * 20 / 1000;
int output_silence_position =
((kInputAdvanceTime + kOutputDelayTime).InSecondsF() + 1.0) *
output_params().sample_rate();
for (int gap = 0; gap < 5; ++gap) {
SCOPED_TRACE(testing::Message() << "gap=" << gap);
// Just before the drop, there should be a tone.
const int position_a_little_before_silence_begins =
output_silence_position - output_frames_in_20_milliseconds;
EXPECT_NEAR(
kSourceVolume,
consumer()->ComputeAmplitudeAt(0, kLeftChannelFrequency,
position_a_little_before_silence_begins),
0.01);
// There should be silence during the drop.
const int position_a_little_after_silence_begins =
output_silence_position + output_frames_in_20_milliseconds;
const int position_a_little_before_silence_ends =
position_a_little_after_silence_begins +
output_frames_in_a_quarter_second -
2 * output_frames_in_20_milliseconds;
EXPECT_TRUE(
consumer()->IsSilentInRange(0, position_a_little_after_silence_begins,
position_a_little_before_silence_ends));
// Finally, the tone should be back after the drop.
const int position_a_little_after_silence_ends =
position_a_little_before_silence_ends +
2 * output_frames_in_20_milliseconds;
EXPECT_NEAR(
kSourceVolume,
consumer()->ComputeAmplitudeAt(0, kLeftChannelFrequency,
position_a_little_after_silence_ends),
0.01);
output_silence_position += output_frames_in_one_second;
}
}
// TODO: TEST_P(SnooperNodeTest, HandlesSkippingOutput) {}
InputAndOutputParams MakeParams(media::ChannelLayout input_channel_layout,
int input_sample_rate,
int input_frames_per_buffer,
media::ChannelLayout output_channel_layout,
int output_sample_rate,
int output_frames_per_buffer) {
return InputAndOutputParams{
media::AudioParameters(media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
input_channel_layout, input_sample_rate,
input_frames_per_buffer),
media::AudioParameters(media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
output_channel_layout, output_sample_rate,
output_frames_per_buffer)};
}
INSTANTIATE_TEST_CASE_P(,
SnooperNodeTest,
testing::Values(MakeParams(media::CHANNEL_LAYOUT_STEREO,
48000,
480,
media::CHANNEL_LAYOUT_STEREO,
48000,
480),
MakeParams(media::CHANNEL_LAYOUT_STEREO,
48000,
64,
media::CHANNEL_LAYOUT_STEREO,
48000,
480),
MakeParams(media::CHANNEL_LAYOUT_STEREO,
44100,
64,
media::CHANNEL_LAYOUT_STEREO,
48000,
480),
MakeParams(media::CHANNEL_LAYOUT_STEREO,
48000,
512,
media::CHANNEL_LAYOUT_STEREO,
44100,
441),
MakeParams(media::CHANNEL_LAYOUT_MONO,
8000,
64,
media::CHANNEL_LAYOUT_STEREO,
48000,
480),
MakeParams(media::CHANNEL_LAYOUT_STEREO,
48000,
480,
media::CHANNEL_LAYOUT_MONO,
8000,
80)));
} // namespace
} // namespace audio
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