blob: 2d0d8390eacdd5aea595dbb898df24f4d52b5e3a [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <array>
#include <limits>
#include "services/audio/mixing_graph.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace audio {
// Test fixture to verify the functionality of the mixing graph inputs.
class MixingGraphInputTest : public ::testing::Test {
protected:
void SetUp() override {
output_params_ =
media::AudioParameters(media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
media::ChannelLayoutConfig::Mono(), 48000, 480);
mixing_graph_ = MixingGraph::Create(
output_params_,
base::BindRepeating(&MixingGraphInputTest::OnMoreDataCallBack,
base::Unretained(this)),
base::BindRepeating(&MixingGraphInputTest::OnErrorCallback,
base::Unretained(this)));
dest_ = media::AudioBus::Create(output_params_);
}
void PullAndVerifyData(int num_runs,
float expected_first_sample,
float expected_sample_increment,
float epsilon) {
float expected_data = expected_first_sample;
for (int i = 0; i < num_runs; i++) {
mixing_graph_->OnMoreData(base::TimeDelta(), base::TimeTicks::Now(), 0,
dest_.get());
float* data = dest_.get()->channel(0);
for (int j = 0; j < dest_.get()->frames(); ++j) {
EXPECT_NEAR(data[j], expected_data, epsilon);
expected_data += expected_sample_increment;
}
}
}
void OnMoreDataCallBack(const media::AudioBus&, base::TimeDelta) {}
void OnErrorCallback(
media::AudioOutputStream::AudioSourceCallback::ErrorType) {}
media::AudioParameters output_params_;
std::unique_ptr<MixingGraph> mixing_graph_;
std::unique_ptr<media::AudioBus> dest_;
};
// Simple audio source callback where a sample value is the value of the
// previous sample plus |increment|. When using stereo the values of the right
// channel will be the values of the left channel plus |increment|.
class SampleCounter : public media::AudioOutputStream::AudioSourceCallback {
public:
explicit SampleCounter(float counter, float increment)
: counter_(counter), increment_(increment) {}
int OnMoreData(base::TimeDelta delay,
base::TimeTicks delay_timestamp,
int prior_frames_skipped,
media::AudioBus* dest) final {
// Fill the audio bus with a simple, predictable pattern.
for (int channel = 0; channel < dest->channels(); ++channel) {
float* data = dest->channel(channel);
for (int frame = 0; frame < dest->frames(); frame++) {
data[frame] = counter_ + increment_ * frame + increment_ * channel;
}
}
counter_ += static_cast<float>(increment_ * dest->frames());
return 0;
}
void OnError(ErrorType type) final {}
private:
float counter_ = 0.0f;
const float increment_;
};
// Simple audio source callback where all samples are set to the same value.
// The value is incremented by |increment| for each callback.
class CallbackCounter : public media::AudioOutputStream::AudioSourceCallback {
public:
explicit CallbackCounter(float counter, float increment)
: counter_(counter), increment_(increment) {}
int OnMoreData(base::TimeDelta delay,
base::TimeTicks delay_timestamp,
int prior_frames_skipped,
media::AudioBus* dest) final {
// Fill the audio bus with the counter value.
for (int channel = 0; channel < dest->channels(); ++channel) {
float* data = dest->channel(channel);
for (int frame = 0; frame < dest->frames(); frame++) {
data[frame] = counter_;
}
}
counter_ += increment_;
return 0;
}
void OnError(ErrorType type) final {}
private:
float counter_ = 0.0f;
const float increment_;
};
// Simple audio source callback where all samples are set to a specified value.
class ConstantInput : public media::AudioOutputStream::AudioSourceCallback {
public:
explicit ConstantInput(float value) : value_(value) {}
int OnMoreData(base::TimeDelta delay,
base::TimeTicks delay_timestamp,
int prior_frames_skipped,
media::AudioBus* dest) final {
// Fill the audio bus with the value specified at construction.
for (int channel = 0; channel < dest->channels(); ++channel) {
float* data = dest->channel(channel);
for (int frame = 0; frame < dest->frames(); frame++) {
data[frame] = value_;
}
}
return 0;
}
void OnError(ErrorType type) final {}
private:
const float value_;
};
// Verifies that the mixing graph outputs zeros when no inputs have been added.
TEST_F(MixingGraphInputTest, NoInputs) {
// The mixing graph is expected to output zeros when it has no inputs.
PullAndVerifyData(/*num_runs=*/2, /*expected_first_sample=*/0.0f,
/*expected_sample_increment=*/0.0f, /*epsilon=*/0.0f);
}
// Verifies the output of a single input with the same parameters as the mixing
// graph.
TEST_F(MixingGraphInputTest, SingleInput) {
constexpr float kInitialCounterValue = 0.0f;
constexpr float kCounterIncrement = 1e-4f;
SampleCounter source_callback(kInitialCounterValue, kCounterIncrement);
auto input = mixing_graph_->CreateInput(output_params_);
input->Start(&source_callback);
PullAndVerifyData(/*num_runs=*/2,
/*expected_first_sample=*/kInitialCounterValue,
/*expected_sample_increment=*/kCounterIncrement,
/*epsilon=*/1e-5f);
input->Stop();
}
// Verifies the output of the mixing graph when adding multiple inputs.
TEST_F(MixingGraphInputTest, MultipleInputs) {
constexpr float kInitialCounterValue1 = 0.1f;
constexpr float kInitialCounterValue2 = 0.5f;
constexpr float kInitialCounterValue3 = -0.7f;
constexpr float kCounterIncrement = 1e-4f;
SampleCounter source_callback1(kInitialCounterValue1, kCounterIncrement);
SampleCounter source_callback2(kInitialCounterValue2, kCounterIncrement);
SampleCounter source_callback3(kInitialCounterValue3, kCounterIncrement);
auto input1 = mixing_graph_->CreateInput(output_params_);
input1->Start(&source_callback1);
auto input2 = mixing_graph_->CreateInput(output_params_);
input2->Start(&source_callback2);
auto input3 = mixing_graph_->CreateInput(output_params_);
input3->Start(&source_callback3);
PullAndVerifyData(/*num_runs=*/2,
/*expected_first_sample=*/kInitialCounterValue1 +
kInitialCounterValue2 + kInitialCounterValue3,
/*expected_sample_increment=*/3.0f * kCounterIncrement,
/*epsilon=*/1e-5f);
input1->Stop();
input2->Stop();
input3->Stop();
}
// Verifies the mixing graph output when adding an input in need of channel
// mixing.
TEST_F(MixingGraphInputTest, ChannelMixing) {
media::AudioParameters input_params(
media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
media::ChannelLayoutConfig::Stereo(), 48000, 480);
constexpr float kInitialCounterValue = 0.0f;
constexpr float kCounterIncrement = 1e-4f;
SampleCounter source_callback(kInitialCounterValue, kCounterIncrement);
auto input = mixing_graph_->CreateInput(input_params);
input->Start(&source_callback);
// The right channel has the values of the left channel + kCounterIncrement.
// When down-mixing (averaging) the two channels this will cause a bias of
// kCounterIncrement/2.
PullAndVerifyData(
/*num_runs=*/2,
/*expected_first_sample=*/kInitialCounterValue + 0.5f * kCounterIncrement,
/*expected_sample_increment=*/kCounterIncrement, /*epsilon=*/1e-5f);
input->Stop();
}
// Verifies the mixing graph output when adding an input in need of resampling.
TEST_F(MixingGraphInputTest, Resampling) {
media::AudioParameters input_params(
media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
media::ChannelLayoutConfig::Mono(), 24000, 480);
constexpr float kInitialCounterValue = 0.0f;
constexpr float kCounterIncrement = 1e-4f;
SampleCounter source_callback(kInitialCounterValue, kCounterIncrement);
auto input = mixing_graph_->CreateInput(input_params);
input->Start(&source_callback);
// The input signal will increase by kCounterIncrement for each sample and be
// upsampled by a factor 2. The output will therefore increase by
// ~kCounterIncrement/2 per sample.
PullAndVerifyData(/*num_runs=*/2,
/*expected_first_sample=*/kInitialCounterValue,
/*expected_sample_increment=*/0.5f * kCounterIncrement,
/*epsilon=*/1e-5f);
input->Stop();
}
// Verifies the use of FIFO when an input produces less data per call than
// requested by the mixing graph.
TEST_F(MixingGraphInputTest, Buffering1) {
// Input produces 5 ms of audio. Output consumes 10 ms.
media::AudioParameters input_params(
media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
media::ChannelLayoutConfig::Mono(), 48000, 240);
constexpr float kInitialCounterValue = 0.0f;
constexpr float kCounterIncrement = 1e-4f;
SampleCounter source_callback(kInitialCounterValue, kCounterIncrement);
auto input = mixing_graph_->CreateInput(input_params);
input->Start(&source_callback);
PullAndVerifyData(/*num_runs=*/2,
/*expected_first_sample=*/kInitialCounterValue,
/*expected_sample_increment=*/kCounterIncrement,
/*epsilon=*/1e-5f);
input->Stop();
}
// Verifies the use of FIFO when an input produces more data per call than
// requested by the mixing graph.
TEST_F(MixingGraphInputTest, Buffering2) {
// Input produces 15 ms of audio. Output consumes 10 ms.
media::AudioParameters input_params(
media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
media::ChannelLayoutConfig::Mono(), 48000, 720);
constexpr float kInitialCounterValue = 0.0f;
constexpr float kCounterIncrement = 1e-4f;
SampleCounter source_callback(kInitialCounterValue, kCounterIncrement);
auto input = mixing_graph_->CreateInput(input_params);
input->Start(&source_callback);
PullAndVerifyData(/*num_runs=*/2,
/*expected_first_sample=*/kInitialCounterValue,
/*expected_sample_increment=*/kCounterIncrement,
/*epsilon=*/1e-5f);
input->Stop();
}
// Verifies that no left-over samples from the FIFO are pulled after stopping
// and restarting the input.
TEST_F(MixingGraphInputTest, BufferClearedAtRestart) {
// Input produces 15 ms of audio. Output consumes 10 ms.
media::AudioParameters input_params(
media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
media::ChannelLayoutConfig::Mono(), 48000, 720);
constexpr float kInitialCounterValue = 0.0f;
constexpr float kCounterIncrement = 1e-4f;
CallbackCounter source_callback(kInitialCounterValue, kCounterIncrement);
auto input = mixing_graph_->CreateInput(input_params);
input->Start(&source_callback);
// Get the last sample of the first output.
mixing_graph_->OnMoreData(base::TimeDelta(), base::TimeTicks::Now(), 0,
dest_.get());
float last_sample = dest_.get()->channel(0)[dest_.get()->frames() - 1];
// Stop and restart.
input->Stop();
input->Start(&source_callback);
// Get the first sample of the second output.
mixing_graph_->OnMoreData(base::TimeDelta(), base::TimeTicks::Now(), 0,
dest_.get());
float first_sample = dest_.get()->channel(0)[0];
// If the first sample of the second output is equal to the last sample of
// the first output left-over data has been consumed.
EXPECT_NE(first_sample, last_sample);
input->Stop();
}
// Verifies the output of the mixing graph when adding and removing inputs on
// the fly.
TEST_F(MixingGraphInputTest, AddingAndRemovingInputs) {
constexpr float kInitialCounterValue1 = -0.3f;
constexpr float kInitialCounterValue2 = 0.2f;
constexpr float kCounterIncrement = 1e-4f;
SampleCounter source_callback1(kInitialCounterValue1, kCounterIncrement);
SampleCounter source_callback2(kInitialCounterValue2, kCounterIncrement);
auto input1 = mixing_graph_->CreateInput(output_params_);
auto input2 = mixing_graph_->CreateInput(output_params_);
// Start the first input.
input1->Start(&source_callback1);
PullAndVerifyData(/*num_runs=*/1,
/*expected_first_sample=*/kInitialCounterValue1,
/*expected_sample_increment=*/kCounterIncrement,
/*epsilon=*/1e-5f);
// Start the second input.
input2->Start(&source_callback2);
PullAndVerifyData(/*num_runs=*/1,
/*expected_first_sample=*/kInitialCounterValue1 +
kCounterIncrement * dest_->frames() +
kInitialCounterValue2,
/*expected_sample_increment=*/2.0f * kCounterIncrement,
/*epsilon=*/1e-5f);
// Stop the first input.
input1->Stop();
PullAndVerifyData(
/*num_runs=*/1,
/*expected_first_sample=*/kInitialCounterValue2 +
kCounterIncrement * dest_->frames(),
/*expected_sample_increment=*/kCounterIncrement, /*epsilon=*/1e-5f);
// Stop the second input.
input2->Stop();
PullAndVerifyData(/*num_runs=*/1,
/*expected_first_sample=*/0.f,
/*expected_sample_increment=*/0.0f, /*epsilon=*/0.0f);
}
// Verifies that the volume is applied correctly.
TEST_F(MixingGraphInputTest, SetVolume) {
constexpr float kInitialCounterValue = 0.0f;
constexpr float kCounterIncrement = 1e-4f;
constexpr float kVolume1 = 0.77f;
constexpr float kVolume2 = 0.13f;
SampleCounter source_callback(kInitialCounterValue, kCounterIncrement);
auto input = mixing_graph_->CreateInput(output_params_);
input->Start(&source_callback);
input->SetVolume(kVolume1);
PullAndVerifyData(/*num_runs=*/1,
/*expected_first_sample=*/kInitialCounterValue * kVolume1,
/*expected_sample_increment=*/kVolume1 * kCounterIncrement,
/*epsilon=*/1e-5f);
input->SetVolume(kVolume2);
PullAndVerifyData(
/*num_runs=*/1,
/*expected_first_sample=*/
(kInitialCounterValue + kCounterIncrement * dest_->frames()) * kVolume2,
/*expected_sample_increment=*/kVolume2 * kCounterIncrement,
/*epsilon=*/1e-5f);
input->Stop();
}
// Verifies that out-of-range output values are sanitized.
TEST_F(MixingGraphInputTest, OutOfRange) {
constexpr float kInputValue1 = -0.6f;
constexpr float kInputValue2 = -0.5f;
ConstantInput source_callback1(kInputValue1);
ConstantInput source_callback2(kInputValue2);
auto input1 = mixing_graph_->CreateInput(output_params_);
input1->Start(&source_callback1);
auto input2 = mixing_graph_->CreateInput(output_params_);
input2->Start(&source_callback2);
// The two inputs should add to -1.1 and be clamped to -1.0.
PullAndVerifyData(/*num_runs=*/1,
/*expected_first_sample=*/-1.0f,
/*expected_sample_increment=*/0.0f, /*epsilon=*/0.0f);
// Lowering the volume of input 1 removes the need of clamping.
input1->SetVolume(0.5f);
PullAndVerifyData(
/*num_runs=*/1,
/*expected_first_sample=*/kInputValue1 * 0.5f + kInputValue2,
/*expected_sample_increment=*/0.0f, /*epsilon=*/0.0f);
input1->Stop();
input2->Stop();
}
// Verifies that invalid input is sanitized.
TEST_F(MixingGraphInputTest, InvalidInput) {
// Pairs of input values and expected output values.
std::array<std::pair<float, float>, 8> test_values = {{
{-1.5f, -1.0f}, // Negative overflow.
{2.0f, 1.0f}, // Positive overflow.
{-0.8, -0.8f}, // Valid.
{0.3, 0.3f}, // Valid.
{0.0, 0.0f}, // Valid.
{std::numeric_limits<float>::infinity(), 1.0f}, // Positive infinity.
{-std::numeric_limits<float>::infinity(), -1.0f}, // Negative infinity.
{NAN, 1.0f}, // NaN.
}};
for (const auto& test_pair : test_values) {
float input_value = test_pair.first;
float expected_output_value = test_pair.second;
auto input = mixing_graph_->CreateInput(output_params_);
ConstantInput source_callback(input_value);
input->Start(&source_callback);
PullAndVerifyData(
/*num_runs=*/1,
/*expected_first_sample=*/expected_output_value,
/*expected_sample_increment=*/0.0f, /*epsilon=*/0.0f);
input->Stop();
}
}
} // namespace audio