blob: d4cee0d0347822355a20f8ba2cd07a4b8848367f [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "media/base/limiting_audio_queue.h"
#include <algorithm>
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/test/bind.h"
#include "media/audio/simple_sources.h"
#include "media/base/audio_bus.h"
#include "media/base/audio_timestamp_helper.h"
#include "testing/gtest/include/gtest/gtest.h"
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/41494069): Update these tests once AudioBus is spanified..
#pragma allow_unsafe_buffers
#endif
namespace media {
namespace {
constexpr int kSampleRate = 48000;
constexpr int kChannels = 2;
constexpr int kBufferSize = 960; // 20ms at 48khz
constexpr int kFrequency = 20;
const ChannelLayout kChannelLayout = ChannelLayout::CHANNEL_LAYOUT_STEREO;
void VerifyAudioBuffer(scoped_refptr<AudioBuffer> buffer,
int number_frames,
AudioBus* expected_data) {
EXPECT_EQ(buffer->sample_rate(), kSampleRate);
EXPECT_EQ(buffer->channel_layout(), kChannelLayout);
EXPECT_EQ(buffer->channel_count(), kChannels);
EXPECT_EQ(buffer->sample_format(), kSampleFormatPlanarF32);
EXPECT_EQ(buffer->frame_count(), number_frames);
EXPECT_EQ(buffer->duration(),
AudioTimestampHelper::FramesToTime(number_frames, kSampleRate));
for (int ch = 0; ch < kChannels; ++ch) {
const size_t kSpanSize = sizeof(float) * static_cast<size_t>(number_frames);
base::span<const uint8_t> input_span(base::as_byte_span(
base::allow_nonunique_obj, expected_data->channel_span(ch)));
base::span<uint8_t> output_span(buffer->channel_data()[ch], kSpanSize);
EXPECT_EQ(input_span, output_span);
}
}
std::unique_ptr<AudioBus> CopyFirstFrames(AudioBus* bus, int num_frames) {
auto result = AudioBus::Create(bus->channels(), num_frames);
bus->CopyPartialFramesTo(0, num_frames, 0, result.get());
return result;
}
} // namespace
class LimitingAudioQueueTest : public testing::Test {
public:
using AudioBusVector = std::vector<std::unique_ptr<AudioBus>>;
LimitingAudioQueueTest()
: audio_source_(kChannels, kFrequency, kSampleRate),
limiting_queue_(std::make_unique<LimitingAudioQueue>(kChannelLayout,
kSampleRate,
kChannels,
kBufferSize)),
input_bus_(AudioBus::Create(kChannels, kBufferSize)) {}
LimitingAudioQueueTest(const LimitingAudioQueueTest&) = delete;
LimitingAudioQueueTest& operator=(const LimitingAudioQueueTest&) = delete;
~LimitingAudioQueueTest() override = default;
void FillWithSine(AudioBus* bus, float scale = 1.0f) {
audio_source_.OnMoreData(base::TimeDelta(), current_timestamp_, {}, bus);
current_timestamp_ +=
AudioTimestampHelper::FramesToTime(kBufferSize, kSampleRate);
if (scale != 1.0f) {
for (auto channel : bus->AllChannels()) {
std::ranges::transform(channel, channel.begin(), [scale](float sample) {
return sample * scale;
});
}
}
}
protected:
base::TimeTicks current_timestamp_;
SineWaveAudioSource audio_source_;
std::unique_ptr<LimitingAudioQueue> limiting_queue_;
std::unique_ptr<AudioBus> input_bus_;
};
// Makes sure we can flush a queue that has never had any input.
TEST_F(LimitingAudioQueueTest, EmptyFlush) {
limiting_queue_->Flush();
}
// Makes sure we can flush a queue that has been cleared.
TEST_F(LimitingAudioQueueTest, FlushClearFlush) {
limiting_queue_->Flush();
limiting_queue_->Clear();
limiting_queue_->Flush();
}
// Makes sure inputs and outputs are bit-wise identical when the limiter isn't
// adjusting gain.
TEST_F(LimitingAudioQueueTest, NoLimiting_IsPassthrough) {
FillWithSine(input_bus_.get());
scoped_refptr<AudioBuffer> result;
auto verify_buffer = [&](scoped_refptr<AudioBuffer> buffer) {
VerifyAudioBuffer(buffer, kBufferSize, input_bus_.get());
result = std::move(buffer);
};
limiting_queue_->Push(*input_bus_, kBufferSize, base::TimeDelta(),
base::BindLambdaForTesting(std::move(verify_buffer)));
limiting_queue_->Flush();
EXPECT_TRUE(result);
}
// Makes sure that calling Clear() drops both pending output callbacks, and does
// not include past data in the following buffers.
TEST_F(LimitingAudioQueueTest, Clear_DropsPendingInputs) {
constexpr float kGuardValue = 0.5f;
// Fill the first channel with kGuardValue.
input_bus_->Zero();
std::ranges::fill(input_bus_->channel_span(0), kGuardValue);
// Feed in data into the queue and clear it.
bool first_buffer_emitted = false;
limiting_queue_->Push(
*input_bus_, kBufferSize, base::TimeDelta(),
base::BindLambdaForTesting(
[&](scoped_refptr<AudioBuffer>) { first_buffer_emitted = true; }));
limiting_queue_->Clear();
// Feed zeros into the limiter queue. There shouldn't be any `kGuardValues`
// from the first buffer in the output.
input_bus_->Zero();
bool second_bufer_emitted = false;
bool has_values_from_first_buffer = false;
limiting_queue_->Push(
*input_bus_, kBufferSize, base::TimeDelta(),
base::BindLambdaForTesting([&](scoped_refptr<AudioBuffer> buffer) {
const float* channel_data =
reinterpret_cast<const float*>(buffer->channel_data()[0]);
for (int i = 0; i < buffer->frame_count(); ++i) {
has_values_from_first_buffer |= channel_data[i] == kGuardValue;
}
second_bufer_emitted = true;
}));
limiting_queue_->Flush();
EXPECT_FALSE(first_buffer_emitted);
EXPECT_TRUE(second_bufer_emitted);
EXPECT_FALSE(has_values_from_first_buffer);
}
// Makes sure inputs and outputs are bit-wise identical when the limiter isn't
// adjusting gain.
TEST_F(LimitingAudioQueueTest, NoLimiting_PartialBuffer_IsPassthrough) {
FillWithSine(input_bus_.get());
constexpr int kPartialBuffer = kBufferSize / 4;
auto partial_buffer = CopyFirstFrames(input_bus_.get(), kPartialBuffer);
scoped_refptr<AudioBuffer> result;
auto verify_buffer = [&](scoped_refptr<AudioBuffer> buffer) {
VerifyAudioBuffer(buffer, kPartialBuffer, partial_buffer.get());
result = std::move(buffer);
};
limiting_queue_->Push(*input_bus_, kPartialBuffer, base::TimeDelta(),
base::BindLambdaForTesting(std::move(verify_buffer)));
limiting_queue_->Flush();
EXPECT_TRUE(result);
}
TEST_F(LimitingAudioQueueTest, Limiting_CompressesGain) {
FillWithSine(input_bus_.get(), 2.0f);
bool has_out_of_range_value = false;
scoped_refptr<AudioBuffer> result;
auto verify_buffer = [&](scoped_refptr<AudioBuffer> buffer) {
for (int ch = 0; ch < kChannels; ++ch) {
const float* channel_data =
reinterpret_cast<const float*>(buffer->channel_data()[ch]);
for (int i = 0; i < kBufferSize; ++i) {
has_out_of_range_value |= std::abs(channel_data[i]) > 1.0f;
}
}
result = std::move(buffer);
};
limiting_queue_->Push(*input_bus_, kBufferSize, base::TimeDelta(),
base::BindLambdaForTesting(std::move(verify_buffer)));
limiting_queue_->Flush();
EXPECT_FALSE(has_out_of_range_value);
EXPECT_TRUE(result);
}
TEST_F(LimitingAudioQueueTest, MultipleBuffers) {
FillWithSine(input_bus_.get(), 2.0f);
// Use arbitrary buffer sizes.
const std::vector<int> kBufferSizeSequence = {kBufferSize, kBufferSize / 2,
kBufferSize / 4, kBufferSize,
kBufferSize - 16};
int buffer_count = 0;
base::TimeDelta current_timestamp = base::TimeDelta();
for (int size : kBufferSizeSequence) {
const base::TimeDelta duration =
AudioTimestampHelper::FramesToTime(size, kSampleRate);
limiting_queue_->Push(
*input_bus_, size, current_timestamp,
base::BindOnce(
[](int expected_size, base::TimeDelta expected_duration,
base::TimeDelta expected_timestamp, int* buffer_count,
scoped_refptr<AudioBuffer> buffer) {
EXPECT_EQ(buffer->frame_count(), expected_size);
EXPECT_EQ(buffer->duration(), expected_duration);
EXPECT_EQ(buffer->timestamp(), expected_timestamp);
++(*buffer_count);
},
size, duration, current_timestamp, &buffer_count));
current_timestamp += duration;
}
limiting_queue_->Flush();
EXPECT_EQ(static_cast<size_t>(buffer_count), kBufferSizeSequence.size());
}
} // namespace media