blob: 0dd866d1a1de0f391708a5306bc77f71d89e9880 [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.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/351564777): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
#include "third_party/blink/renderer/modules/webcodecs/audio_data.h"
#include <optional>
#include "media/base/audio_sample_types.h"
#include "media/base/test_helpers.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_audio_data_copy_to_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_audio_data_init.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_audio_sample_format.h"
#include "third_party/blink/renderer/core/typed_arrays/dom_array_buffer.h"
#include "third_party/blink/renderer/core/typed_arrays/dom_data_view.h"
#include "third_party/blink/renderer/modules/webaudio/audio_buffer.h"
#include "third_party/blink/renderer/modules/webcodecs/array_buffer_util.h"
#include "third_party/blink/renderer/platform/testing/task_environment.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size.h"
namespace blink {
namespace {
// Default test values
constexpr int64_t kTimestampInMicroSeconds = 1234;
constexpr int kChannels = 2;
constexpr int kFrames = 20;
constexpr int kSampleRate = 8000;
constexpr int kPartialFrameCount = 5;
constexpr int kOffset = 5;
constexpr int kMicrosecondPerSecond = 1E6;
constexpr uint64_t kExpectedDuration =
static_cast<int64_t>(kFrames * kMicrosecondPerSecond / kSampleRate);
constexpr float kIncrement = 1.0f / 1000;
constexpr float kEpsilon = kIncrement / 100;
} // namespace
class AudioDataTest : public testing::Test {
protected:
void VerifyPlanarData(float* data, float start_value, int count) {
for (int i = 0; i < count; ++i)
ASSERT_NEAR(data[i], start_value + i * kIncrement, kEpsilon) << "i=" << i;
}
AllowSharedBufferSource* CreateDefaultData() {
return CreateCustomData(kChannels, kFrames);
}
AllowSharedBufferSource* CreateCustomData(int channels, int frames) {
auto* buffer = DOMArrayBuffer::Create(channels * frames, sizeof(float));
for (int ch = 0; ch < channels; ++ch) {
float* plane_start =
reinterpret_cast<float*>(buffer->Data()) + ch * frames;
for (int i = 0; i < frames; ++i) {
plane_start[i] = static_cast<float>((i + ch * frames) * kIncrement);
}
}
return MakeGarbageCollected<AllowSharedBufferSource>(buffer);
}
AudioDataInit* CreateDefaultAudioDataInit(AllowSharedBufferSource* data) {
auto* audio_data_init = AudioDataInit::Create();
audio_data_init->setData(data);
audio_data_init->setTimestamp(kTimestampInMicroSeconds);
audio_data_init->setNumberOfChannels(kChannels);
audio_data_init->setNumberOfFrames(kFrames);
audio_data_init->setSampleRate(kSampleRate);
audio_data_init->setFormat("f32-planar");
return audio_data_init;
}
AudioData* CreateDefaultAudioData(ScriptState* script_state,
ExceptionState& exception_state) {
auto* data = CreateDefaultData();
auto* audio_data_init = CreateDefaultAudioDataInit(data);
return MakeGarbageCollected<AudioData>(script_state, audio_data_init,
exception_state);
}
AudioDataCopyToOptions* CreateCopyToOptions(int index,
std::optional<uint32_t> offset,
std::optional<uint32_t> count) {
auto* copy_to_options = AudioDataCopyToOptions::Create();
copy_to_options->setPlaneIndex(index);
if (offset.has_value())
copy_to_options->setFrameOffset(offset.value());
if (count.has_value())
copy_to_options->setFrameCount(count.value());
return copy_to_options;
}
void VerifyAllocationSize(int plane_index,
std::optional<uint32_t> frame_offset,
std::optional<uint32_t> frame_count,
bool should_throw,
int expected_size,
std::string description) {
V8TestingScope scope;
auto* frame = CreateDefaultAudioData(scope.GetScriptState(),
scope.GetExceptionState());
auto* options = CreateCopyToOptions(plane_index, frame_offset, frame_count);
{
SCOPED_TRACE(description);
int allocations_size =
frame->allocationSize(options, scope.GetExceptionState());
EXPECT_EQ(should_throw, scope.GetExceptionState().HadException());
EXPECT_EQ(allocations_size, expected_size);
}
}
test::TaskEnvironment task_environment_;
};
TEST_F(AudioDataTest, ConstructFromMediaBuffer) {
const media::ChannelLayout channel_layout =
media::ChannelLayout::CHANNEL_LAYOUT_STEREO;
const int channels = ChannelLayoutToChannelCount(channel_layout);
constexpr base::TimeDelta timestamp =
base::Microseconds(kTimestampInMicroSeconds);
constexpr int kValueStart = 1;
constexpr int kValueIncrement = 1;
scoped_refptr<media::AudioBuffer> media_buffer =
media::MakeAudioBuffer<int16_t>(
media::SampleFormat::kSampleFormatS16, channel_layout, channels,
kSampleRate, kValueStart, kValueIncrement, kFrames, timestamp);
auto* frame = MakeGarbageCollected<AudioData>(media_buffer);
EXPECT_EQ(frame->format(), "s16");
EXPECT_EQ(frame->sampleRate(), static_cast<uint32_t>(kSampleRate));
EXPECT_EQ(frame->numberOfFrames(), static_cast<uint32_t>(kFrames));
EXPECT_EQ(frame->numberOfChannels(), static_cast<uint32_t>(kChannels));
EXPECT_EQ(frame->duration(), kExpectedDuration);
EXPECT_EQ(frame->timestamp(), kTimestampInMicroSeconds);
// The media::AudioBuffer we receive should match the original |media_buffer|.
EXPECT_EQ(frame->data(), media_buffer);
frame->close();
EXPECT_EQ(frame->data(), nullptr);
EXPECT_EQ(frame->format(), std::nullopt);
EXPECT_EQ(frame->sampleRate(), 0u);
EXPECT_EQ(frame->numberOfFrames(), 0u);
EXPECT_EQ(frame->numberOfChannels(), 0u);
EXPECT_EQ(frame->duration(), 0u);
// Timestamp is preserved even after closing.
EXPECT_EQ(frame->timestamp(), kTimestampInMicroSeconds);
}
TEST_F(AudioDataTest, ConstructFromAudioDataInit) {
V8TestingScope scope;
auto* buffer_source = CreateDefaultData();
auto* audio_data_init = CreateDefaultAudioDataInit(buffer_source);
auto* frame = MakeGarbageCollected<AudioData>(
scope.GetScriptState(), audio_data_init, scope.GetExceptionState());
EXPECT_EQ(frame->format(), "f32-planar");
EXPECT_EQ(frame->sampleRate(), static_cast<uint32_t>(kSampleRate));
EXPECT_EQ(frame->numberOfFrames(), static_cast<uint32_t>(kFrames));
EXPECT_EQ(frame->numberOfChannels(), static_cast<uint32_t>(kChannels));
EXPECT_EQ(frame->duration(), kExpectedDuration);
EXPECT_EQ(frame->timestamp(), kTimestampInMicroSeconds);
}
TEST_F(AudioDataTest, ConstructFromAudioDataInit_HighChannelCount) {
V8TestingScope scope;
constexpr int kHighChannelCount = 25;
auto* buffer_source = CreateCustomData(kHighChannelCount, kFrames);
auto* audio_data_init = CreateDefaultAudioDataInit(buffer_source);
audio_data_init->setNumberOfChannels(kHighChannelCount);
auto* frame = MakeGarbageCollected<AudioData>(
scope.GetScriptState(), audio_data_init, scope.GetExceptionState());
EXPECT_EQ(frame->format(), "f32-planar");
EXPECT_EQ(frame->sampleRate(), static_cast<uint32_t>(kSampleRate));
EXPECT_EQ(frame->numberOfFrames(), static_cast<uint32_t>(kFrames));
EXPECT_EQ(frame->numberOfChannels(),
static_cast<uint32_t>(kHighChannelCount));
EXPECT_EQ(frame->duration(), kExpectedDuration);
EXPECT_EQ(frame->timestamp(), kTimestampInMicroSeconds);
}
TEST_F(AudioDataTest, AllocationSize) {
// We only support the "FLTP" format for now.
constexpr int kTotalSizeInBytes = kFrames * sizeof(float);
// Basic cases.
VerifyAllocationSize(0, std::nullopt, std::nullopt, false, kTotalSizeInBytes,
"Default");
VerifyAllocationSize(1, std::nullopt, std::nullopt, false, kTotalSizeInBytes,
"Valid index.");
VerifyAllocationSize(0, 0, kFrames, false, kTotalSizeInBytes,
"Specifying defaults");
// Cases where we cover a subset of samples.
VerifyAllocationSize(0, kFrames / 2, std::nullopt, false,
kTotalSizeInBytes / 2, "Valid offset, no count");
VerifyAllocationSize(0, kFrames / 2, kFrames / 4, false,
kTotalSizeInBytes / 4, "Valid offset and count");
VerifyAllocationSize(0, std::nullopt, kFrames / 2, false,
kTotalSizeInBytes / 2, "No offset, valid count");
// Copying 0 frames is technically valid.
VerifyAllocationSize(0, std::nullopt, 0, false, 0, "Frame count is 0");
// Failures
VerifyAllocationSize(2, std::nullopt, std::nullopt, true, 0,
"Invalid index.");
VerifyAllocationSize(0, kFrames, std::nullopt, true, 0, "Offset too big");
VerifyAllocationSize(0, std::nullopt, kFrames + 1, true, 0, "Count too big");
VerifyAllocationSize(0, 1, kFrames, true, 0, "Count too big, with offset");
}
TEST_F(AudioDataTest, CopyTo_DestinationTooSmall) {
V8TestingScope scope;
auto* frame =
CreateDefaultAudioData(scope.GetScriptState(), scope.GetExceptionState());
auto* options = CreateCopyToOptions(/*index=*/0, /*offset=*/std::nullopt,
/*count=*/std::nullopt);
AllowSharedBufferSource* small_dest =
MakeGarbageCollected<AllowSharedBufferSource>(
DOMArrayBuffer::Create(kFrames - 1, sizeof(float)));
frame->copyTo(small_dest, options, scope.GetExceptionState());
EXPECT_TRUE(scope.GetExceptionState().HadException());
}
TEST_F(AudioDataTest, CopyTo_FullFrames) {
V8TestingScope scope;
auto* frame =
CreateDefaultAudioData(scope.GetScriptState(), scope.GetExceptionState());
auto* options = CreateCopyToOptions(/*index=*/0, /*offset=*/std::nullopt,
/*count=*/std::nullopt);
DOMArrayBuffer* data_copy = DOMArrayBuffer::Create(kFrames, sizeof(float));
AllowSharedBufferSource* dest =
MakeGarbageCollected<AllowSharedBufferSource>(data_copy);
// All frames should have been copied.
frame->copyTo(dest, options, scope.GetExceptionState());
EXPECT_FALSE(scope.GetExceptionState().HadException());
VerifyPlanarData(static_cast<float*>(data_copy->Data()), /*start_value=*/0,
/*count=*/kFrames);
}
TEST_F(AudioDataTest, CopyTo_PlaneIndex) {
V8TestingScope scope;
auto* frame =
CreateDefaultAudioData(scope.GetScriptState(), scope.GetExceptionState());
auto* options = CreateCopyToOptions(/*index=*/1, /*offset=*/std::nullopt,
/*count=*/std::nullopt);
DOMArrayBuffer* data_copy = DOMArrayBuffer::Create(kFrames, sizeof(float));
AllowSharedBufferSource* dest =
MakeGarbageCollected<AllowSharedBufferSource>(data_copy);
// All frames should have been copied.
frame->copyTo(dest, options, scope.GetExceptionState());
EXPECT_FALSE(scope.GetExceptionState().HadException());
// The channel 1's start value is kFrames*in.
VerifyPlanarData(static_cast<float*>(data_copy->Data()),
/*start_value=*/kFrames * kIncrement,
/*count=*/kFrames);
}
// Check that sample-aligned ArrayBuffers can be transferred to AudioData
TEST_F(AudioDataTest, TransferBuffer) {
V8TestingScope scope;
std::string data = "audio data";
auto* buffer = DOMArrayBuffer::Create(data.data(), data.size());
auto* buffer_source = MakeGarbageCollected<AllowSharedBufferSource>(buffer);
const void* buffer_data_ptr = buffer->Data();
auto* audio_data_init = AudioDataInit::Create();
audio_data_init->setData(buffer_source);
audio_data_init->setTimestamp(0);
audio_data_init->setNumberOfChannels(1);
audio_data_init->setNumberOfFrames(static_cast<uint32_t>(data.size()));
audio_data_init->setSampleRate(kSampleRate);
audio_data_init->setFormat("u8");
HeapVector<Member<DOMArrayBuffer>> transfer;
transfer.push_back(Member<DOMArrayBuffer>(buffer));
audio_data_init->setTransfer(std::move(transfer));
auto* audio_data = MakeGarbageCollected<AudioData>(
scope.GetScriptState(), audio_data_init, scope.GetExceptionState());
EXPECT_EQ(audio_data->format(), "u8");
EXPECT_EQ(audio_data->numberOfFrames(), data.size());
EXPECT_EQ(audio_data->numberOfChannels(), 1u);
EXPECT_EQ(audio_data->data()->channel_data()[0], buffer_data_ptr);
auto* options = CreateCopyToOptions(0, 0, {});
uint32_t allocations_size =
audio_data->allocationSize(options, scope.GetExceptionState());
EXPECT_TRUE(buffer->IsDetached());
EXPECT_EQ(allocations_size, data.size());
}
// Check that not-sample-aligned ArrayBuffers are copied AudioData
TEST_F(AudioDataTest, FailToTransferUnAlignedBuffer) {
V8TestingScope scope;
const uint32_t frames = 3;
std::vector<float> data{0.0, 1.0, 2.0, 3.0, 4.0};
auto* buffer =
DOMArrayBuffer::Create(data.data(), data.size() * sizeof(float));
auto* view = DOMDataView::Create(
buffer, 1 /* offset one byte from the float ptr, that how we are sure that
the view is not aligned to sizeof(float) */
,
frames * sizeof(float));
auto* buffer_source = MakeGarbageCollected<AllowSharedBufferSource>(
MaybeShared<DOMArrayBufferView>(view));
MakeGarbageCollected<AllowSharedBufferSource>(buffer);
const void* buffer_data_ptr = buffer->Data();
auto* audio_data_init = AudioDataInit::Create();
audio_data_init->setData(buffer_source);
audio_data_init->setTimestamp(0);
audio_data_init->setNumberOfChannels(1);
audio_data_init->setNumberOfFrames(frames);
audio_data_init->setSampleRate(kSampleRate);
audio_data_init->setFormat("f32");
HeapVector<Member<DOMArrayBuffer>> transfer;
transfer.push_back(Member<DOMArrayBuffer>(buffer));
audio_data_init->setTransfer(std::move(transfer));
auto* audio_data = MakeGarbageCollected<AudioData>(
scope.GetScriptState(), audio_data_init, scope.GetExceptionState());
// Making sure that the data was copied, not just aliased.
EXPECT_NE(audio_data->data()->channel_data()[0], buffer_data_ptr);
EXPECT_EQ(audio_data->numberOfFrames(), frames);
auto* options = CreateCopyToOptions(0, 0, {});
uint32_t allocations_size =
audio_data->allocationSize(options, scope.GetExceptionState());
// Even though we copied the data, the buffer still needs to be aligned.
EXPECT_TRUE(buffer->IsDetached());
EXPECT_EQ(allocations_size, frames * sizeof(float));
}
TEST_F(AudioDataTest, CopyTo_Offset) {
V8TestingScope scope;
auto* frame =
CreateDefaultAudioData(scope.GetScriptState(), scope.GetExceptionState());
auto* options =
CreateCopyToOptions(/*index=*/0, kOffset, /*count=*/std::nullopt);
// |data_copy| is bigger than what we need, and that's ok.
DOMArrayBuffer* data_copy = DOMArrayBuffer::Create(kFrames, sizeof(float));
AllowSharedBufferSource* dest =
MakeGarbageCollected<AllowSharedBufferSource>(data_copy);
// All frames should have been copied.
frame->copyTo(dest, options, scope.GetExceptionState());
EXPECT_FALSE(scope.GetExceptionState().HadException());
VerifyPlanarData(static_cast<float*>(data_copy->Data()),
/*start_value=*/kOffset * kIncrement,
/*count=*/kFrames - kOffset);
}
TEST_F(AudioDataTest, CopyTo_PartialFrames) {
V8TestingScope scope;
auto* frame =
CreateDefaultAudioData(scope.GetScriptState(), scope.GetExceptionState());
auto* options = CreateCopyToOptions(/*index=*/0, /*offset=*/std::nullopt,
kPartialFrameCount);
DOMArrayBuffer* data_copy =
DOMArrayBuffer::Create(kPartialFrameCount, sizeof(float));
AllowSharedBufferSource* dest =
MakeGarbageCollected<AllowSharedBufferSource>(data_copy);
// All frames should have been copied.
frame->copyTo(dest, options, scope.GetExceptionState());
EXPECT_FALSE(scope.GetExceptionState().HadException());
VerifyPlanarData(static_cast<float*>(data_copy->Data()),
/*start_value=*/0, kPartialFrameCount);
}
TEST_F(AudioDataTest, CopyTo_PartialFramesAndOffset) {
V8TestingScope scope;
auto* frame =
CreateDefaultAudioData(scope.GetScriptState(), scope.GetExceptionState());
auto* options = CreateCopyToOptions(/*index=*/0, kOffset, kPartialFrameCount);
DOMArrayBuffer* data_copy =
DOMArrayBuffer::Create(kPartialFrameCount, sizeof(float));
AllowSharedBufferSource* dest =
MakeGarbageCollected<AllowSharedBufferSource>(data_copy);
// All frames should have been copied.
frame->copyTo(dest, options, scope.GetExceptionState());
EXPECT_FALSE(scope.GetExceptionState().HadException());
VerifyPlanarData(static_cast<float*>(data_copy->Data()),
/*start_value=*/kOffset * kIncrement, kPartialFrameCount);
}
TEST_F(AudioDataTest, Interleaved) {
V8TestingScope scope;
// Do not use a power of 2, to make it easier to verify the allocationSize()
// results.
constexpr int kInterleavedChannels = 3;
std::vector<int16_t> samples(kFrames * kInterleavedChannels);
// Populate samples.
for (int i = 0; i < kFrames; ++i) {
int block_index = i * kInterleavedChannels;
samples[block_index] = i; // channel 0
samples[block_index + 1] = i + kFrames; // channel 1
samples[block_index + 2] = i + 2 * kFrames; // channel 2
}
const uint8_t* data[] = {reinterpret_cast<const uint8_t*>(samples.data())};
auto media_buffer = media::AudioBuffer::CopyFrom(
media::SampleFormat::kSampleFormatS16,
media::GuessChannelLayout(kInterleavedChannels), kInterleavedChannels,
kSampleRate, kFrames, data, base::TimeDelta());
auto* frame = MakeGarbageCollected<AudioData>(media_buffer);
EXPECT_EQ("s16", frame->format());
// Verify that plane indexes > 1 throw, for interleaved formats.
auto* options = CreateCopyToOptions(/*index=*/1, kOffset, kPartialFrameCount);
int allocations_size =
frame->allocationSize(options, scope.GetExceptionState());
EXPECT_TRUE(scope.GetExceptionState().HadException());
scope.GetExceptionState().ClearException();
// Verify that copy conversion to a planar format supports indexes > 1,
// even if the source is interleaved.
options->setFormat(V8AudioSampleFormat::Enum::kF32Planar);
allocations_size = frame->allocationSize(options, scope.GetExceptionState());
EXPECT_FALSE(scope.GetExceptionState().HadException());
// Verify we get the expected allocation size, for valid formats.
options = CreateCopyToOptions(/*index=*/0, kOffset, kPartialFrameCount);
allocations_size = frame->allocationSize(options, scope.GetExceptionState());
EXPECT_FALSE(scope.GetExceptionState().HadException());
// Interleaved formats take into account the number of channels.
EXPECT_EQ(static_cast<unsigned int>(allocations_size),
kPartialFrameCount * kInterleavedChannels * sizeof(uint16_t));
DOMArrayBuffer* data_copy = DOMArrayBuffer::Create(
kPartialFrameCount * kInterleavedChannels, sizeof(uint16_t));
AllowSharedBufferSource* dest =
MakeGarbageCollected<AllowSharedBufferSource>(data_copy);
// All frames should have been copied.
frame->copyTo(dest, options, scope.GetExceptionState());
EXPECT_FALSE(scope.GetExceptionState().HadException());
// Verify we retrieved the right samples.
int16_t* copy = static_cast<int16_t*>(data_copy->Data());
for (int i = 0; i < kPartialFrameCount; ++i) {
int block_index = i * kInterleavedChannels;
int16_t base_value = kOffset + i;
EXPECT_EQ(copy[block_index], base_value); // channel 0
EXPECT_EQ(copy[block_index + 1], base_value + kFrames); // channel 1
EXPECT_EQ(copy[block_index + 2], base_value + 2 * kFrames); // channel 2
}
}
struct U8Traits {
static constexpr std::string Format = "u8";
static constexpr std::string PlanarFormat = "u8-planar";
using Traits = media::UnsignedInt8SampleTypeTraits;
};
struct S16Traits {
static constexpr std::string Format = "s16";
static constexpr std::string PlanarFormat = "s16-planar";
using Traits = media::SignedInt16SampleTypeTraits;
};
struct S32Traits {
static constexpr std::string Format = "s32";
static constexpr std::string PlanarFormat = "s32-planar";
using Traits = media::SignedInt32SampleTypeTraits;
};
struct F32Traits {
static constexpr std::string Format = "f32";
static constexpr std::string PlanarFormat = "f32-planar";
using Traits = media::Float32SampleTypeTraits;
};
template <typename SourceTraits, typename TargetTraits>
struct ConversionConfig {
using From = SourceTraits;
using To = TargetTraits;
static std::string config_name() {
return From::Format + "_to_" + To::Format;
}
};
template <typename TestConfig>
class AudioDataConversionTest : public testing::Test {
protected:
AudioDataInit* CreateAudioDataInit(AllowSharedBufferSource* data,
std::string format) {
auto* audio_data_init = AudioDataInit::Create();
audio_data_init->setData(data);
audio_data_init->setTimestamp(kTimestampInMicroSeconds);
audio_data_init->setNumberOfChannels(kChannels);
audio_data_init->setNumberOfFrames(kFrames);
audio_data_init->setSampleRate(kSampleRate);
audio_data_init->setFormat(String(format));
return audio_data_init;
}
// Creates test AudioData with kMinValue in channel 0 and kMaxValue in
// channel 1. If `use_offset`, the first sample of every channel will be
// kZeroPointValue; if `use_frame_count`, the last sample of every channel
// will be kZeroPointValue. This allows us to verify that we respect bounds
// when copying.
AudioData* CreateAudioData(std::string format,
bool planar,
bool use_offset,
bool use_frame_count,
ScriptState* script_state,
ExceptionState& exception_state) {
auto* data = planar ? CreatePlanarData(use_offset, use_frame_count)
: CreateInterleavedData(use_offset, use_frame_count);
auto* audio_data_init = CreateAudioDataInit(data, format);
return MakeGarbageCollected<AudioData>(script_state, audio_data_init,
exception_state);
}
// Creates CopyToOptions. If `use_offset` is true, we exclude the first
// sample. If `use_frame_count`, we exclude the last sample.
AudioDataCopyToOptions* CreateCopyToOptions(int plane_index,
std::string format,
bool use_offset,
bool use_frame_count) {
auto* copy_to_options = AudioDataCopyToOptions::Create();
copy_to_options->setPlaneIndex(plane_index);
copy_to_options->setFormat(String(format));
int total_frames = kFrames;
if (use_offset) {
copy_to_options->setFrameOffset(1);
--total_frames;
}
if (use_frame_count) {
copy_to_options->setFrameCount(total_frames - 1);
}
return copy_to_options;
}
// Returns planar data with the source sample type's min value in
// channel 0, and its max value in channel 1.
// If `use_offset` is true, the first sample of every channel will be 0.
// If `use_frame_count` is true, the last sample of every channel will be 0.
AllowSharedBufferSource* CreatePlanarData(bool use_offset,
bool use_frame_count) {
static_assert(kChannels == 2, "CreatePlanarData() assumes 2 channels");
using SourceTraits = TestConfig::From::Traits;
using ValueType = SourceTraits::ValueType;
auto* buffer =
DOMArrayBuffer::Create(kChannels * kFrames, sizeof(ValueType));
ValueType* plane_start = reinterpret_cast<ValueType*>(buffer->Data());
for (int i = 0; i < kFrames; ++i) {
plane_start[i] = SourceTraits::kMinValue;
}
if (use_offset) {
plane_start[0] = SourceTraits::kZeroPointValue;
}
if (use_frame_count) {
plane_start[kFrames - 1] = SourceTraits::kZeroPointValue;
}
plane_start += kFrames;
for (int i = 0; i < kFrames; ++i) {
plane_start[i] = SourceTraits::kMaxValue;
}
if (use_offset) {
plane_start[0] = SourceTraits::kZeroPointValue;
}
if (use_frame_count) {
plane_start[kFrames - 1] = SourceTraits::kZeroPointValue;
}
return MakeGarbageCollected<AllowSharedBufferSource>(buffer);
}
// Returns interleaved data the source sample type's min value in channel 0,
// and its max value in channel 1.
// If `use_offset` is true, the first sample of every channel will be 0.
// If `use_frame_count` is true, the last sample of every channel will be 0.
AllowSharedBufferSource* CreateInterleavedData(bool use_offset,
bool use_frame_count) {
static_assert(kChannels == 2,
"CreateInterleavedData() assumes 2 channels.");
using SourceTraits = TestConfig::From::Traits;
using ValueType = SourceTraits::ValueType;
constexpr int kTotalSamples = kChannels * kFrames;
auto* buffer = DOMArrayBuffer::Create(kTotalSamples, sizeof(ValueType));
ValueType* plane_start = reinterpret_cast<ValueType*>(buffer->Data());
for (int i = 0; i < kTotalSamples; i += 2) {
plane_start[i] = SourceTraits::kMinValue;
plane_start[i + 1] = SourceTraits::kMaxValue;
}
if (use_offset) {
plane_start[0] = SourceTraits::kZeroPointValue;
plane_start[1] = SourceTraits::kZeroPointValue;
}
if (use_frame_count) {
plane_start[kTotalSamples - 2] = SourceTraits::kZeroPointValue;
plane_start[kTotalSamples - 1] = SourceTraits::kZeroPointValue;
}
return MakeGarbageCollected<AllowSharedBufferSource>(buffer);
}
int GetFramesToCopy(bool use_offset, bool use_frame_count) {
int frames_to_copy = kFrames;
if (use_offset) {
--frames_to_copy;
}
if (use_frame_count) {
--frames_to_copy;
}
return frames_to_copy;
}
void TestConversionToPlanar(bool source_is_planar,
bool use_offset,
bool use_frame_count) {
using Config = TestConfig;
using TargetType = Config::To::Traits::ValueType;
constexpr int kChannelToCopy = 1;
std::string source_format =
source_is_planar ? Config::From::PlanarFormat : Config::From::Format;
// Create original data. The first and last frame will be zero'ed, if
// `use_offset` or `use_frame_count` are set, respectively.
V8TestingScope scope;
auto* audio_data = CreateAudioData(
source_format, source_is_planar, use_offset, use_frame_count,
scope.GetScriptState(), scope.GetExceptionState());
ASSERT_FALSE(scope.GetExceptionState().HadException())
<< scope.GetExceptionState().Message();
// Prepare to copy.
auto* copy_to_options = CreateCopyToOptions(
kChannelToCopy, Config::To::PlanarFormat, use_offset, use_frame_count);
const int frames_to_copy = GetFramesToCopy(use_offset, use_frame_count);
DOMArrayBuffer* data_copy =
DOMArrayBuffer::Create(frames_to_copy, sizeof(TargetType));
AllowSharedBufferSource* dest =
MakeGarbageCollected<AllowSharedBufferSource>(data_copy);
// Copy frames, potentially excluding the first and last frame if
// `use_offset` or `use_frame_count` are set, respectively.
audio_data->copyTo(dest, copy_to_options, scope.GetExceptionState());
EXPECT_FALSE(scope.GetExceptionState().HadException())
<< scope.GetExceptionState().Message();
TargetType* copied_data = static_cast<TargetType*>(data_copy->Data());
// `kChannelToCopy` should only contain kMaxValue
for (int i = 0; i < frames_to_copy; ++i) {
ASSERT_EQ(copied_data[i], Config::To::Traits::kMaxValue);
}
}
void TestConversionToInterleaved(bool source_is_planar,
bool use_offset,
bool use_frame_count) {
using Config = TestConfig;
using TargetType = Config::To::Traits::ValueType;
std::string source_format =
source_is_planar ? Config::From::PlanarFormat : Config::From::Format;
// Create original data. The first and last frame will be zero'ed, if
// `use_offset` or `use_frame_count` are set, respectively.
V8TestingScope scope;
auto* audio_data = CreateAudioData(
source_format, source_is_planar, use_offset, use_frame_count,
scope.GetScriptState(), scope.GetExceptionState());
ASSERT_FALSE(scope.GetExceptionState().HadException())
<< scope.GetExceptionState().Message();
// Prepare to copy.
auto* copy_to_options =
CreateCopyToOptions(0, Config::To::Format, use_offset, use_frame_count);
const int total_frames =
GetFramesToCopy(use_offset, use_frame_count) * kChannels;
DOMArrayBuffer* data_copy =
DOMArrayBuffer::Create(total_frames, sizeof(TargetType));
AllowSharedBufferSource* dest =
MakeGarbageCollected<AllowSharedBufferSource>(data_copy);
// Copy frames, potentially excluding the first and last frame if
// `use_offset` or `use_frame_count` are set, respectively.
audio_data->copyTo(dest, copy_to_options, scope.GetExceptionState());
EXPECT_FALSE(scope.GetExceptionState().HadException())
<< scope.GetExceptionState().Message();
TargetType* copied_data = static_cast<TargetType*>(data_copy->Data());
// The interleaved data should have kMinValue in
// channel 0 and kMaxValue in channel 1.
for (int i = 0; i < total_frames; i += 2) {
ASSERT_EQ(copied_data[i], Config::To::Traits::kMinValue);
ASSERT_EQ(copied_data[i + 1], Config::To::Traits::kMaxValue);
}
}
test::TaskEnvironment task_environment_;
};
TYPED_TEST_SUITE_P(AudioDataConversionTest);
TYPED_TEST_P(AudioDataConversionTest, PlanarToPlanar) {
const bool source_is_planar = true;
{
SCOPED_TRACE(TypeParam::config_name() + "_all_frames");
this->TestConversionToPlanar(source_is_planar, false, false);
}
{
SCOPED_TRACE(TypeParam::config_name() + "_with_offset");
this->TestConversionToPlanar(source_is_planar, true, false);
}
{
SCOPED_TRACE(TypeParam::config_name() + "_with_frame_count");
this->TestConversionToPlanar(source_is_planar, false, true);
}
{
SCOPED_TRACE(TypeParam::config_name() + "_with_offset_and_frame_count");
this->TestConversionToPlanar(source_is_planar, true, true);
}
}
TYPED_TEST_P(AudioDataConversionTest, InterleavedToPlanar) {
const bool source_is_planar = false;
{
SCOPED_TRACE(TypeParam::config_name() + "_all_frames");
this->TestConversionToPlanar(source_is_planar, false, false);
}
{
SCOPED_TRACE(TypeParam::config_name() + "_with_offset");
this->TestConversionToPlanar(source_is_planar, true, false);
}
{
SCOPED_TRACE(TypeParam::config_name() + "_with_frame_count");
this->TestConversionToPlanar(source_is_planar, false, true);
}
{
SCOPED_TRACE(TypeParam::config_name() + "_with_offset_and_frame_count");
this->TestConversionToPlanar(source_is_planar, true, true);
}
}
TYPED_TEST_P(AudioDataConversionTest, PlanarToInterleaved) {
const bool source_is_planar = true;
{
SCOPED_TRACE(TypeParam::config_name() + "_all_frames");
this->TestConversionToInterleaved(source_is_planar, false, false);
}
{
SCOPED_TRACE(TypeParam::config_name() + "_with_offset");
this->TestConversionToInterleaved(source_is_planar, true, false);
}
{
SCOPED_TRACE(TypeParam::config_name() + "_with_frame_count");
this->TestConversionToInterleaved(source_is_planar, false, true);
}
{
SCOPED_TRACE(TypeParam::config_name() + "_with_offset_and_frame_count");
this->TestConversionToInterleaved(source_is_planar, true, true);
}
}
TYPED_TEST_P(AudioDataConversionTest, InterleavedToInterleaved) {
const bool source_is_planar = false;
{
SCOPED_TRACE(TypeParam::config_name() + "_all_frames");
this->TestConversionToInterleaved(source_is_planar, false, false);
}
{
SCOPED_TRACE(TypeParam::config_name() + "_with_offset");
this->TestConversionToInterleaved(source_is_planar, true, false);
}
{
SCOPED_TRACE(TypeParam::config_name() + "_with_frame_count");
this->TestConversionToInterleaved(source_is_planar, false, true);
}
{
SCOPED_TRACE(TypeParam::config_name() + "_with_offset_and_frame_count");
this->TestConversionToInterleaved(source_is_planar, true, true);
}
}
REGISTER_TYPED_TEST_SUITE_P(AudioDataConversionTest,
PlanarToPlanar,
InterleavedToPlanar,
PlanarToInterleaved,
InterleavedToInterleaved);
typedef ::testing::Types<ConversionConfig<U8Traits, U8Traits>,
ConversionConfig<U8Traits, S16Traits>,
ConversionConfig<U8Traits, S32Traits>,
ConversionConfig<U8Traits, F32Traits>,
ConversionConfig<S16Traits, U8Traits>,
ConversionConfig<S16Traits, S16Traits>,
ConversionConfig<S16Traits, S32Traits>,
ConversionConfig<S16Traits, F32Traits>,
ConversionConfig<S32Traits, U8Traits>,
ConversionConfig<S32Traits, S16Traits>,
ConversionConfig<S32Traits, S32Traits>,
ConversionConfig<S32Traits, F32Traits>,
ConversionConfig<F32Traits, U8Traits>,
ConversionConfig<F32Traits, S16Traits>,
ConversionConfig<F32Traits, S32Traits>,
ConversionConfig<F32Traits, F32Traits>>
TestConfigs;
INSTANTIATE_TYPED_TEST_SUITE_P(CommonTypes,
AudioDataConversionTest,
TestConfigs);
} // namespace blink