blob: f4d85ca4501b3462f4cb6f7d3d6a5aded0d19698 [file] [log] [blame]
// Copyright 2021 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 "third_party/blink/renderer/modules/webcodecs/audio_data.h"
#include "media/base/test_helpers.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/types/optional.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/core/typed_arrays/dom_array_buffer.h"
#include "third_party/blink/renderer/modules/webaudio/audio_buffer.h"
#include "third_party/blink/renderer/modules/webcodecs/allow_shared_buffer_source_util.h"
#include "third_party/blink/renderer/platform/heap/thread_state.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(ExceptionState& exception_state) {
auto* data = CreateDefaultData();
auto* audio_data_init = CreateDefaultAudioDataInit(data);
return MakeGarbageCollected<AudioData>(audio_data_init, exception_state);
}
AudioDataCopyToOptions* CreateCopyToOptions(int index,
absl::optional<uint32_t> offset,
absl::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,
absl::optional<uint32_t> frame_offset,
absl::optional<uint32_t> frame_count,
bool should_throw,
int expected_size,
std::string description) {
V8TestingScope scope;
auto* frame = CreateDefaultAudioData(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_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(), absl::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>(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>(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, absl::nullopt, absl::nullopt, false,
kTotalSizeInBytes, "Default");
VerifyAllocationSize(1, absl::nullopt, absl::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, absl::nullopt, false,
kTotalSizeInBytes / 2, "Valid offset, no count");
VerifyAllocationSize(0, kFrames / 2, kFrames / 4, false,
kTotalSizeInBytes / 4, "Valid offset and count");
VerifyAllocationSize(0, absl::nullopt, kFrames / 2, false,
kTotalSizeInBytes / 2, "No offset, valid count");
// Copying 0 frames is technically valid.
VerifyAllocationSize(0, absl::nullopt, 0, false, 0, "Frame count is 0");
// Failures
VerifyAllocationSize(2, absl::nullopt, absl::nullopt, true, 0,
"Invalid index.");
VerifyAllocationSize(0, kFrames, absl::nullopt, true, 0, "Offset too big");
VerifyAllocationSize(0, absl::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.GetExceptionState());
auto* options = CreateCopyToOptions(/*index=*/0, /*offset=*/absl::nullopt,
/*count=*/absl::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.GetExceptionState());
auto* options = CreateCopyToOptions(/*index=*/0, /*offset=*/absl::nullopt,
/*count=*/absl::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.GetExceptionState());
auto* options = CreateCopyToOptions(/*index=*/1, /*offset=*/absl::nullopt,
/*count=*/absl::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);
}
TEST_F(AudioDataTest, CopyTo_Offset) {
V8TestingScope scope;
auto* frame = CreateDefaultAudioData(scope.GetExceptionState());
auto* options =
CreateCopyToOptions(/*index=*/0, kOffset, /*count=*/absl::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.GetExceptionState());
auto* options = CreateCopyToOptions(/*index=*/0, /*offset=*/absl::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.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
}
}
} // namespace blink