blob: 2da80c88f88169a51c038a2cf1c76f9e648c9bb7 [file] [log] [blame]
// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <stdint.h>
#include <limits>
#include <memory>
#include <utility>
#include "base/files/file_util.h"
#include "base/memory/raw_ptr.h"
#include "base/test/task_environment.h"
#include "media/audio/audio_debug_file_writer.h"
#include "media/base/audio_bus.h"
#include "media/base/audio_bus_pool.h"
#include "media/base/audio_parameters.h"
#include "media/base/audio_sample_types.h"
#include "media/base/test_helpers.h"
#include "testing/gtest/include/gtest/gtest.h"
using testing::_;
namespace media {
namespace {
static const uint16_t kBytesPerSample = sizeof(uint16_t);
static const uint16_t kPcmEncoding = 1;
static const size_t kWavHeaderSize = 44;
uint16_t ReadLE2(const char* buf) {
return static_cast<uint8_t>(buf[0]) | (static_cast<uint8_t>(buf[1]) << 8);
}
uint32_t ReadLE4(const char* buf) {
return static_cast<uint8_t>(buf[0]) | (static_cast<uint8_t>(buf[1]) << 8) |
(static_cast<uint8_t>(buf[2]) << 16) |
(static_cast<uint8_t>(buf[3]) << 24);
}
base::File OpenFile(const base::FilePath& file_path) {
return base::File(file_path, base::File::FLAG_OPEN | base::File::FLAG_WRITE);
}
// Serves only to expose the protected Create method of AudioDebugFileWriter.
class AudioDebugFileWriterUnderTest : public AudioDebugFileWriter {
public:
// Should not be instantiated
AudioDebugFileWriterUnderTest() = delete;
static Ptr Create(const AudioParameters& params,
base::File file,
std::unique_ptr<AudioBusPool> audio_bus_pool) {
return AudioDebugFileWriter::Create(params, std::move(file),
std::move(audio_bus_pool));
}
};
class MockAudioBusPool : public AudioBusPool {
public:
explicit MockAudioBusPool(const AudioParameters& params) : params_(params) {}
MockAudioBusPool(const MockAudioBusPool&) = delete;
MockAudioBusPool& operator=(const MockAudioBusPool&) = delete;
~MockAudioBusPool() override = default;
std::unique_ptr<AudioBus> GetAudioBus() override {
if (audio_bus_to_return_) {
return std::move(audio_bus_to_return_);
}
return AudioBus::Create(params_);
}
MOCK_METHOD1(OnInsertAudioBus, void(AudioBus*));
void InsertAudioBus(std::unique_ptr<AudioBus> audio_bus) override {
OnInsertAudioBus(audio_bus.get());
}
void SetNextAudioBus(std::unique_ptr<AudioBus> audio_bus) {
audio_bus_to_return_ = std::move(audio_bus);
}
private:
const AudioParameters params_;
std::unique_ptr<AudioBus> audio_bus_to_return_;
};
} // namespace
// <channel layout, sample rate, frames per buffer, number of buffer writes
typedef std::tuple<ChannelLayoutConfig, int, int, int>
AudioDebugFileWriterTestData;
class AudioDebugFileWriterTest
: public testing::TestWithParam<AudioDebugFileWriterTestData> {
public:
explicit AudioDebugFileWriterTest(
base::test::TaskEnvironment::ThreadPoolExecutionMode execution_mode)
: task_environment_(base::test::TaskEnvironment::MainThreadType::DEFAULT,
execution_mode),
debug_writer_(nullptr, base::OnTaskRunnerDeleter(nullptr)),
params_(AudioParameters::Format::AUDIO_PCM_LINEAR,
std::get<0>(GetParam()),
std::get<1>(GetParam()),
std::get<2>(GetParam())),
writes_(std::get<3>(GetParam())),
source_samples_(params_.frames_per_buffer() * params_.channels() *
writes_),
source_interleaved_(source_samples_ ? new int16_t[source_samples_]
: nullptr) {
InitSourceInterleaved(source_interleaved_.get(), source_samples_);
}
AudioDebugFileWriterTest()
: AudioDebugFileWriterTest(
base::test::TaskEnvironment::ThreadPoolExecutionMode::ASYNC) {}
AudioDebugFileWriterTest(const AudioDebugFileWriterTest&) = delete;
AudioDebugFileWriterTest& operator=(const AudioDebugFileWriterTest&) = delete;
protected:
virtual ~AudioDebugFileWriterTest() = default;
static void InitSourceInterleaved(int16_t* source_interleaved,
int source_samples) {
if (source_samples) {
// equal steps to cover int16_t range of values
int16_t step = 0xffff / source_samples;
int16_t val = std::numeric_limits<int16_t>::min();
for (int i = 0; i < source_samples; ++i, val += step)
source_interleaved[i] = val;
}
}
static void VerifyHeader(const char (&wav_header)[kWavHeaderSize],
const AudioParameters& params,
int writes,
int64_t file_length) {
uint32_t block_align = params.channels() * kBytesPerSample;
uint32_t data_size =
static_cast<uint32_t>(params.frames_per_buffer() * params.channels() *
writes * kBytesPerSample);
// Offset Length Content
// 0 4 "RIFF"
EXPECT_EQ(0, strncmp(wav_header, "RIFF", 4));
// 4 4 <file length - 8>
ASSERT_GT(file_length, 8);
EXPECT_EQ(static_cast<uint64_t>(file_length - 8), ReadLE4(wav_header + 4));
EXPECT_EQ(static_cast<uint32_t>(data_size + kWavHeaderSize - 8),
ReadLE4(wav_header + 4));
// 8 4 "WAVE"
// 12 4 "fmt "
EXPECT_EQ(0, strncmp(wav_header + 8, "WAVEfmt ", 8));
// 16 4 <length of the fmt data> (=16)
EXPECT_EQ(16U, ReadLE4(wav_header + 16));
// 20 2 <WAVE file encoding tag>
EXPECT_EQ(kPcmEncoding, ReadLE2(wav_header + 20));
// 22 2 <channels>
EXPECT_EQ(params.channels(), ReadLE2(wav_header + 22));
// 24 4 <sample rate>
EXPECT_EQ(static_cast<uint32_t>(params.sample_rate()),
ReadLE4(wav_header + 24));
// 28 4 <bytes per second> (sample rate * block align)
EXPECT_EQ(static_cast<uint32_t>(params.sample_rate()) * block_align,
ReadLE4(wav_header + 28));
// 32 2 <block align> (channels * bits per sample / 8)
EXPECT_EQ(block_align, ReadLE2(wav_header + 32));
// 34 2 <bits per sample>
EXPECT_EQ(kBytesPerSample * 8, ReadLE2(wav_header + 34));
// 36 4 "data"
EXPECT_EQ(0, strncmp(wav_header + 36, "data", 4));
// 40 4 <sample data size(n)>
EXPECT_EQ(data_size, ReadLE4(wav_header + 40));
}
// |result_interleaved| is expected to be little-endian.
static void VerifyDataRecording(const int16_t* source_interleaved,
const int16_t* result_interleaved,
int16_t source_samples) {
// Allow mismatch by 1 due to rounding error in int->float->int
// calculations.
for (int i = 0; i < source_samples; ++i)
EXPECT_LE(std::abs(source_interleaved[i] - result_interleaved[i]), 1)
<< "i = " << i << " source " << source_interleaved[i] << " result "
<< result_interleaved[i];
}
void VerifyRecording(const base::FilePath& file_path) {
base::File file(file_path, base::File::FLAG_OPEN | base::File::FLAG_READ);
ASSERT_TRUE(file.IsValid());
char wav_header[kWavHeaderSize];
EXPECT_EQ(file.Read(0, wav_header, kWavHeaderSize),
static_cast<int>(kWavHeaderSize));
VerifyHeader(wav_header, params_, writes_, file.GetLength());
if (source_samples_ > 0) {
std::unique_ptr<int16_t[]> result_interleaved(
new int16_t[source_samples_]);
memset(result_interleaved.get(), 0, source_samples_ * kBytesPerSample);
// Recording is read from file as a byte sequence, so it stored as
// little-endian.
int read = file.Read(kWavHeaderSize,
reinterpret_cast<char*>(result_interleaved.get()),
source_samples_ * kBytesPerSample);
EXPECT_EQ(static_cast<int>(file.GetLength() - kWavHeaderSize), read);
VerifyDataRecording(source_interleaved_.get(), result_interleaved.get(),
source_samples_);
}
}
void DoDebugRecording(bool expect_buses_returned_to_pool = true) {
EXPECT_CALL(*mock_audio_bus_pool_, OnInsertAudioBus(_))
.Times(expect_buses_returned_to_pool ? writes_ : 0);
for (int i = 0; i < writes_; ++i) {
std::unique_ptr<AudioBus> bus =
AudioBus::Create(params_.channels(), params_.frames_per_buffer());
bus->FromInterleaved<media::SignedInt16SampleTypeTraits>(
source_interleaved_.get() +
i * params_.channels() * params_.frames_per_buffer(),
params_.frames_per_buffer());
debug_writer_->Write(*bus);
}
}
void CreateDebugWriter(base::File file) {
auto audio_bus_pool = std::make_unique<MockAudioBusPool>(params_);
mock_audio_bus_pool_ = audio_bus_pool.get();
debug_writer_ = AudioDebugFileWriterUnderTest::Create(
params_, std::move(file), std::move(audio_bus_pool));
}
void DestroyDebugWriter() {
// Drop unowned reference before deleting owner.
mock_audio_bus_pool_ = nullptr;
debug_writer_.reset();
}
protected:
// The test task environment.
base::test::TaskEnvironment task_environment_;
// Writer under test.
AudioDebugFileWriter::Ptr debug_writer_;
// Pointer to the AudioBusPool of the most recently created writer.
raw_ptr<MockAudioBusPool> mock_audio_bus_pool_;
// AudioBus parameters.
AudioParameters params_;
// Number of times to write AudioBus to the file.
int writes_;
// Number of samples in the source data.
int source_samples_;
// Source data.
std::unique_ptr<int16_t[]> source_interleaved_;
};
class AudioDebugFileWriterBehavioralTest : public AudioDebugFileWriterTest {};
class AudioDebugFileWriterSingleThreadTest : public AudioDebugFileWriterTest {
public:
AudioDebugFileWriterSingleThreadTest()
: AudioDebugFileWriterTest(
base::test::TaskEnvironment::ThreadPoolExecutionMode::QUEUED) {}
};
TEST_P(AudioDebugFileWriterTest, WaveRecordingTest) {
base::FilePath file_path;
ASSERT_TRUE(base::CreateTemporaryFile(&file_path));
base::File file = OpenFile(file_path);
ASSERT_TRUE(file.IsValid());
CreateDebugWriter(std::move(file));
DoDebugRecording();
DestroyDebugWriter();
task_environment_.RunUntilIdle();
VerifyRecording(file_path);
if (::testing::Test::HasFailure()) {
LOG(ERROR) << "Test failed; keeping recording(s) at ["
<< file_path.value().c_str() << "].";
} else {
ASSERT_TRUE(base::DeleteFile(file_path));
}
}
TEST_P(AudioDebugFileWriterBehavioralTest, ShouldReuseAudioBusesWithPool) {
base::FilePath file_path;
ASSERT_TRUE(base::CreateTemporaryFile(&file_path));
base::File file = OpenFile(file_path);
ASSERT_TRUE(file.IsValid());
CreateDebugWriter(std::move(file));
// Set a specific audio bus to be returned by the pool.
std::unique_ptr<AudioBus> reference_audio_bus = AudioBus::Create(params_);
AudioBus* reference_audio_bus_ptr = reference_audio_bus.get();
mock_audio_bus_pool_->SetNextAudioBus(std::move(reference_audio_bus));
// Expect that same audio bus to be returned to the pool.
EXPECT_CALL(*mock_audio_bus_pool_, OnInsertAudioBus(reference_audio_bus_ptr));
std::unique_ptr<AudioBus> bus = AudioBus::Create(params_);
bus->FromInterleaved<media::SignedInt16SampleTypeTraits>(
source_interleaved_.get(), params_.frames_per_buffer());
debug_writer_->Write(*bus);
task_environment_.RunUntilIdle();
}
TEST_P(AudioDebugFileWriterSingleThreadTest,
DeletedBeforeRecordingFinishedOnFileThread) {
base::FilePath file_path;
ASSERT_TRUE(base::CreateTemporaryFile(&file_path));
base::File file = OpenFile(file_path);
ASSERT_TRUE(file.IsValid());
CreateDebugWriter(std::move(file));
DoDebugRecording();
DestroyDebugWriter();
task_environment_.RunUntilIdle();
VerifyRecording(file_path);
if (::testing::Test::HasFailure()) {
LOG(ERROR) << "Test failed; keeping recording(s) at ["
<< file_path.value().c_str() << "].";
} else {
ASSERT_TRUE(base::DeleteFile(file_path));
}
}
TEST_P(AudioDebugFileWriterBehavioralTest, StartWithInvalidFile) {
base::File file; // Invalid file, recording should not crash
CreateDebugWriter(std::move(file));
DoDebugRecording(/*expect_buses_returned_to_pool = */ false);
task_environment_.RunUntilIdle();
}
TEST_P(AudioDebugFileWriterBehavioralTest, StartStopStartStop) {
base::FilePath file_path1;
ASSERT_TRUE(base::CreateTemporaryFile(&file_path1));
base::File file1 = OpenFile(file_path1);
ASSERT_TRUE(file1.IsValid());
base::FilePath file_path2;
ASSERT_TRUE(base::CreateTemporaryFile(&file_path2));
base::File file2 = OpenFile(file_path2);
ASSERT_TRUE(file2.IsValid());
CreateDebugWriter(std::move(file1));
DoDebugRecording();
DestroyDebugWriter();
CreateDebugWriter(std::move(file2));
DoDebugRecording();
DestroyDebugWriter();
task_environment_.RunUntilIdle();
VerifyRecording(file_path1);
VerifyRecording(file_path2);
if (::testing::Test::HasFailure()) {
LOG(ERROR) << "Test failed; keeping recording(s) at ["
<< file_path1.value().c_str() << ", "
<< file_path2.value().c_str() << "].";
} else {
ASSERT_TRUE(base::DeleteFile(file_path1));
ASSERT_TRUE(base::DeleteFile(file_path2));
}
}
TEST_P(AudioDebugFileWriterBehavioralTest, DestroyStarted) {
base::FilePath file_path;
ASSERT_TRUE(base::CreateTemporaryFile(&file_path));
base::File file = OpenFile(file_path);
ASSERT_TRUE(file.IsValid());
CreateDebugWriter(std::move(file));
DestroyDebugWriter();
task_environment_.RunUntilIdle();
}
INSTANTIATE_TEST_SUITE_P(
AudioDebugFileWriterTest,
AudioDebugFileWriterTest,
// Using 10ms frames per buffer everywhere.
testing::Values(
// No writes.
std::make_tuple(ChannelLayoutConfig::Mono(), 44100, 44100 / 100, 0),
// 1 write of mono.
std::make_tuple(ChannelLayoutConfig::Mono(), 44100, 44100 / 100, 1),
// 1 second of mono.
std::make_tuple(ChannelLayoutConfig::Mono(), 44100, 44100 / 100, 100),
// 1 second of mono, higher rate.
std::make_tuple(ChannelLayoutConfig::Mono(), 48000, 48000 / 100, 100),
// 1 second of stereo.
std::make_tuple(ChannelLayoutConfig::Stereo(), 44100, 44100 / 100, 100),
// 15 seconds of stereo, higher rate.
std::make_tuple(ChannelLayoutConfig::Stereo(),
48000,
48000 / 100,
1500)));
INSTANTIATE_TEST_SUITE_P(
AudioDebugFileWriterBehavioralTest,
AudioDebugFileWriterBehavioralTest,
// Using 10ms frames per buffer everywhere.
testing::Values(
// No writes.
std::make_tuple(ChannelLayoutConfig::Mono(), 44100, 44100 / 100, 100)));
INSTANTIATE_TEST_SUITE_P(
AudioDebugFileWriterSingleThreadTest,
AudioDebugFileWriterSingleThreadTest,
// Using 10ms frames per buffer everywhere.
testing::Values(
// No writes.
std::make_tuple(ChannelLayoutConfig::Mono(), 44100, 44100 / 100, 100)));
} // namespace media