Index: media/filters/audio_renderer_algorithm_unittest.cc |
diff --git a/media/filters/audio_renderer_algorithm_unittest.cc b/media/filters/audio_renderer_algorithm_unittest.cc |
index d5119c00c2b116022f095c868bd059b7f5d67a5b..738e9f4f12b31efcbcdc04bc51c03d4773d328fc 100644 |
--- a/media/filters/audio_renderer_algorithm_unittest.cc |
+++ b/media/filters/audio_renderer_algorithm_unittest.cc |
@@ -8,23 +8,57 @@ |
// correct rate. We always pass in a very large destination buffer with the |
// expectation that FillBuffer() will fill as much as it can but no more. |
+#include <algorithm> // For std::min(). |
#include <cmath> |
#include "base/bind.h" |
#include "base/callback.h" |
+#include "base/memory/scoped_ptr.h" |
#include "media/base/audio_buffer.h" |
#include "media/base/audio_bus.h" |
#include "media/base/buffers.h" |
#include "media/base/channel_layout.h" |
#include "media/base/test_helpers.h" |
#include "media/filters/audio_renderer_algorithm.h" |
+#include "media/filters/wsola_internals.h" |
#include "testing/gtest/include/gtest/gtest.h" |
- |
namespace media { |
static const int kFrameSize = 250; |
static const int kSamplesPerSecond = 3000; |
static const SampleFormat kSampleFormat = kSampleFormatS16; |
+static const int kOutputDurationInSec = 10; |
+ |
+static void FillWithSquarePulseTrain( |
+ int half_pulse_width, int offset, int channel, AudioBus* audio_bus) { |
+ ASSERT_GE(offset, 0); |
+ ASSERT_LT(offset, audio_bus->frames()); |
+ |
+ float* ch = audio_bus->channel(channel); |
+ |
+ // Fill backward from |offset| - 1 toward zero, starting with -1, alternating |
+ // between -1 and 1 every |pulse_width| samples. |
+ float pulse = -1.0f; |
+ for (int n = offset - 1, k = 0; n >= 0; --n, ++k) { |
+ if (k >= half_pulse_width) { |
+ pulse = -pulse; |
+ k = 0; |
+ } |
+ ch[n] = pulse; |
+ } |
+ |
+ // Fill forward from |offset| towards the end, starting with 1, alternating |
+ // between 1 and -1 every |pulse_width| samples. |
+ pulse = 1.0f; |
+ for (int n = offset, k = 0; n < audio_bus->frames(); ++n, ++k) { |
+ if (k >= half_pulse_width) { |
+ pulse = -pulse; |
+ k = 0; |
+ } |
+ ch[n] = pulse; |
+ } |
+} |
+ |
class AudioRendererAlgorithmTest : public testing::Test { |
public: |
@@ -118,7 +152,8 @@ class AudioRendererAlgorithmTest : public testing::Test { |
void TestPlaybackRate(double playback_rate) { |
const int kDefaultBufferSize = algorithm_.samples_per_second() / 100; |
- const int kDefaultFramesRequested = 2 * algorithm_.samples_per_second(); |
+ const int kDefaultFramesRequested = kOutputDurationInSec * |
+ algorithm_.samples_per_second(); |
TestPlaybackRate( |
playback_rate, kDefaultBufferSize, kDefaultFramesRequested); |
@@ -175,6 +210,89 @@ class AudioRendererAlgorithmTest : public testing::Test { |
EXPECT_NEAR(playback_rate, actual_playback_rate, playback_rate / 100.0); |
} |
+ void WsolaTest(float playback_rate) { |
+ const int kSampleRateHz = 48000; |
+ const media::ChannelLayout kChannelLayout = CHANNEL_LAYOUT_STEREO; |
+ const int kBytesPerSample = 2; |
+ const int kNumFrames = kSampleRateHz / 100; // 10 milliseconds. |
+ |
+ channels_ = ChannelLayoutToChannelCount(kChannelLayout); |
+ AudioParameters params(AudioParameters::AUDIO_PCM_LINEAR, kChannelLayout, |
+ kSampleRateHz, kBytesPerSample * 8, kNumFrames); |
+ algorithm_.Initialize(playback_rate, params); |
+ |
+ // A pulse is 6 milliseconds (even number of samples). |
+ const int kPulseWidthSamples = 6 * kSampleRateHz / 1000; |
+ const int kHalfPulseWidthSamples = kPulseWidthSamples / 2; |
+ |
+ // For the ease of implementation get 1 frame every call to FillBuffer(). |
+ scoped_ptr<AudioBus> output = AudioBus::Create(channels_, 1); |
+ |
+ scoped_ptr<AudioBus> ref = |
+ AudioBus::Create(channels_, kPulseWidthSamples); |
+ |
+ FillWithSquarePulseTrain(kHalfPulseWidthSamples, 0, 0, ref.get()); |
+ FillWithSquarePulseTrain(kHalfPulseWidthSamples, kHalfPulseWidthSamples, 1, |
+ ref.get()); |
+ |
+ const int all_channels_samples = channels_ * kPulseWidthSamples; |
+ scoped_ptr<int16_t[]> ref_memory_buffer(new int16_t[all_channels_samples]); |
+ uint8_t* pp[] = { reinterpret_cast<uint8_t*>(ref_memory_buffer.get()) }; |
+ |
+ ref->ToInterleaved(kPulseWidthSamples, sizeof(*(ref_memory_buffer.get())), |
+ ref_memory_buffer.get()); |
+ |
+ base::TimeDelta timestamp = base::TimeDelta::FromInternalValue(0); |
+ base::TimeDelta duration = base::TimeDelta::FromInternalValue(5000); |
+ |
+ // |input| has a whole pulse, therefore, we can inject it |
+ // into |algorithm_| multiple of times to create a periodic input. |
+ scoped_refptr<AudioBuffer> input = AudioBuffer::CopyFrom( |
+ kSampleFormatS16, channels_, kPulseWidthSamples, pp, timestamp, |
+ duration); |
+ |
+ // Equivalent of 4 seconds. |
+ const int kNumRequestedPulses = kSampleRateHz * 4 / kPulseWidthSamples; |
+ for (int n = 0; n < kNumRequestedPulses; ++n) { |
+ // The output is buffered here until we have a pulse is created. Then |
+ // reference file is compared with this buffer. |
+ scoped_ptr<AudioBus> out_buffer = |
+ AudioBus::Create(channels_, kPulseWidthSamples); |
+ |
+ int num_buffered_frames = 0; |
+ while (num_buffered_frames < kPulseWidthSamples) { |
+ int num_samples = algorithm_.FillBuffer(output.get(), 1); |
+ EXPECT_LE(num_samples, 1); |
+ if (num_samples > 0) { |
+ output->CopyPartialFramesTo(0, num_samples, num_buffered_frames, |
+ out_buffer.get()); |
+ num_buffered_frames++; |
+ } else { |
+ algorithm_.EnqueueBuffer(input); |
+ } |
+ } |
+ |
+ // Pulses in the first half of WSOLA AOL frame are not constructed |
+ // perfectly. Do not check them. |
+ if (n > 3) { |
+ scoped_ptr<int16_t[]> test_memory_buffer( |
+ new int16_t[all_channels_samples]); |
+ out_buffer->ToInterleaved(kPulseWidthSamples, |
+ sizeof(*(test_memory_buffer.get())), |
+ test_memory_buffer.get()); |
+ |
+ // Because of overlap-and-add we might have round off error. |
+ for (int k = 0; k < all_channels_samples - 1; ++k) { |
+ ASSERT_NEAR(ref_memory_buffer[k], test_memory_buffer[k], 1) |
+ << " loop " << n << " at sample " << k; |
+ } |
+ } |
+ |
+ // Zero out the buffer to be sure the next comparison is relevant. |
+ out_buffer->Zero(); |
+ } |
+ } |
+ |
protected: |
AudioRendererAlgorithm algorithm_; |
int frames_enqueued_; |
@@ -270,7 +388,7 @@ TEST_F(AudioRendererAlgorithmTest, FillBuffer_JumpAroundSpeeds) { |
TEST_F(AudioRendererAlgorithmTest, FillBuffer_SmallBufferSize) { |
Initialize(); |
static const int kBufferSizeInFrames = 1; |
- static const int kFramesRequested = 2 * kSamplesPerSecond; |
+ static const int kFramesRequested = kOutputDurationInSec * kSamplesPerSecond; |
TestPlaybackRate(1.0, kBufferSizeInFrames, kFramesRequested); |
TestPlaybackRate(0.5, kBufferSizeInFrames, kFramesRequested); |
TestPlaybackRate(1.5, kBufferSizeInFrames, kFramesRequested); |
@@ -297,4 +415,185 @@ TEST_F(AudioRendererAlgorithmTest, FillBuffer_HigherQualityAudio) { |
TestPlaybackRate(1.5); |
} |
+TEST_F(AudioRendererAlgorithmTest, DotProduct) { |
+ const int kChannels = 3; |
+ const int kFrames = 20; |
+ const int kHalfPulseWidth = 2; |
+ |
+ scoped_ptr<AudioBus> a = AudioBus::Create(kChannels, kFrames); |
+ scoped_ptr<AudioBus> b = AudioBus::Create(kChannels, kFrames); |
+ |
+ scoped_ptr<float[]> dot_prod(new float[kChannels]); |
+ |
+ FillWithSquarePulseTrain(kHalfPulseWidth, 0, 0, a.get()); |
+ FillWithSquarePulseTrain(kHalfPulseWidth, 1, 1, a.get()); |
+ FillWithSquarePulseTrain(kHalfPulseWidth, 2, 2, a.get()); |
+ |
+ FillWithSquarePulseTrain(kHalfPulseWidth, 0, 0, b.get()); |
+ FillWithSquarePulseTrain(kHalfPulseWidth, 0, 1, b.get()); |
+ FillWithSquarePulseTrain(kHalfPulseWidth, 0, 2, b.get()); |
+ |
+ internal::MultiChannelDotProduct(a.get(), 0, b.get(), 0, kFrames, |
+ dot_prod.get()); |
+ |
+ EXPECT_FLOAT_EQ(kFrames, dot_prod[0]); |
+ EXPECT_FLOAT_EQ(0, dot_prod[1]); |
+ EXPECT_FLOAT_EQ(-kFrames, dot_prod[2]); |
+ |
+ internal::MultiChannelDotProduct(a.get(), 4, b.get(), 8, kFrames / 2, |
+ dot_prod.get()); |
+ |
+ EXPECT_FLOAT_EQ(kFrames / 2, dot_prod[0]); |
+ EXPECT_FLOAT_EQ(0, dot_prod[1]); |
+ EXPECT_FLOAT_EQ(-kFrames / 2, dot_prod[2]); |
+} |
+ |
+TEST_F(AudioRendererAlgorithmTest, MovingWindowEnergy) { |
+ const int kChannels = 2; |
+ const int kFrames = 20; |
+ const int kFramesPerBlock = 3; |
+ const int kNumBlocks = kFrames - (kFramesPerBlock - 1); |
+ scoped_ptr<AudioBus> a = AudioBus::Create(kChannels, kFrames); |
+ scoped_ptr<float[]> energies(new float[kChannels * kNumBlocks]); |
+ float* ch_left = a->channel(0); |
+ float* ch_right = a->channel(1); |
+ |
+ // Fill up both channels. |
+ for (int n = 0; n < kFrames; ++n) { |
+ ch_left[n] = n; |
+ ch_right[n] = kFrames - 1 - n; |
+ } |
+ |
+ internal::MultiChannelMovingWindowEnergies(a.get(), kFramesPerBlock, |
+ energies.get()); |
+ |
+ for (int n = 0; n < kNumBlocks; ++n) { |
+ float expected_energy = 0; |
+ for (int k = 0; k < kFramesPerBlock; ++k) |
+ expected_energy += ch_left[n + k] * ch_left[n + k]; |
+ EXPECT_FLOAT_EQ(expected_energy, energies[2 * n]); |
+ |
+ expected_energy = 0; |
+ for (int k = 0; k < kFramesPerBlock; ++k) |
+ expected_energy += ch_right[n + k] * ch_right[n + k]; |
+ EXPECT_FLOAT_EQ(expected_energy, energies[2 * n + 1]); |
+ } |
+} |
+ |
+TEST_F(AudioRendererAlgorithmTest, PartialAndDecimatedSearch) { |
+ const int kFramesInSearchRegion = 12; |
+ const int kChannels = 2; |
+ float ch_0[] = {0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0}; |
+ float ch_1[] = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 1.0, 0.1, 0.0, 0.0}; |
+ ASSERT_EQ(sizeof(ch_0), sizeof(ch_1)); |
+ ASSERT_EQ(static_cast<size_t>(kFramesInSearchRegion), |
+ sizeof(ch_0) / sizeof(*ch_0)); |
+ scoped_ptr<AudioBus> search_region = AudioBus::CreateWrapper(kChannels); |
+ |
+ search_region->SetChannelData(0, ch_0); |
+ search_region->SetChannelData(1, ch_1); |
+ search_region->set_frames(kFramesInSearchRegion); |
+ ASSERT_EQ(kFramesInSearchRegion, search_region->frames()); |
+ |
+ const int kFramePerBlock = 4; |
+ float target_0[] = {1.0, 1.0, 1.0, 0.0}; |
+ float target_1[] = {0.0, 1.0, 0.1, 1.0}; |
+ ASSERT_EQ(sizeof(target_0), sizeof(target_1)); |
+ ASSERT_EQ(static_cast<size_t>(kFramePerBlock), |
+ sizeof(target_0) / sizeof(*target_0)); |
+ |
+ scoped_ptr<AudioBus> target = AudioBus::CreateWrapper(2); |
+ target->SetChannelData(0, target_0); |
+ target->SetChannelData(1, target_1); |
+ target->set_frames(kFramePerBlock); |
+ ASSERT_EQ(kFramePerBlock, target->frames()); |
+ |
+ scoped_ptr<float[]> energy_target(new float[kChannels]); |
+ |
+ internal::MultiChannelDotProduct(target.get(), 0, target.get(), 0, |
+ kFramePerBlock, energy_target.get()); |
+ |
+ ASSERT_EQ(3.f, energy_target[0]); |
+ ASSERT_EQ(2.01f, energy_target[1]); |
+ |
+ const int kNumCandidBlocks = kFramesInSearchRegion - (kFramePerBlock - 1); |
+ scoped_ptr<float[]> energy_candid_blocks(new float[kNumCandidBlocks * |
+ kChannels]); |
+ |
+ internal::MultiChannelMovingWindowEnergies( |
+ search_region.get(), kFramePerBlock, energy_candid_blocks.get()); |
+ |
+ ASSERT_FLOAT_EQ(0, energy_candid_blocks[0]); |
+ ASSERT_FLOAT_EQ(0, energy_candid_blocks[2]); |
+ ASSERT_FLOAT_EQ(1, energy_candid_blocks[4]); |
+ ASSERT_FLOAT_EQ(2, energy_candid_blocks[6]); |
+ ASSERT_FLOAT_EQ(3, energy_candid_blocks[8]); |
+ ASSERT_FLOAT_EQ(3, energy_candid_blocks[10]); |
+ ASSERT_FLOAT_EQ(2, energy_candid_blocks[12]); |
+ ASSERT_FLOAT_EQ(1, energy_candid_blocks[14]); |
+ ASSERT_FLOAT_EQ(0, energy_candid_blocks[16]); |
+ |
+ ASSERT_FLOAT_EQ(0, energy_candid_blocks[1]); |
+ ASSERT_FLOAT_EQ(0, energy_candid_blocks[3]); |
+ ASSERT_FLOAT_EQ(0, energy_candid_blocks[5]); |
+ ASSERT_FLOAT_EQ(0, energy_candid_blocks[7]); |
+ ASSERT_FLOAT_EQ(0.01, energy_candid_blocks[9]); |
+ ASSERT_FLOAT_EQ(1.01, energy_candid_blocks[11]); |
+ ASSERT_FLOAT_EQ(1.02, energy_candid_blocks[13]); |
+ ASSERT_FLOAT_EQ(1.02, energy_candid_blocks[15]); |
+ ASSERT_FLOAT_EQ(1.01, energy_candid_blocks[17]); |
+ |
+ // An interval which is of no effect. |
+ internal::Interval exclude_interval = std::make_pair(-100, -10); |
+ EXPECT_EQ(5, internal::PartialSearch( |
+ 0, kNumCandidBlocks - 1, exclude_interval, target.get(), |
+ search_region.get(), energy_target.get(), energy_candid_blocks.get())); |
+ |
+ // Exclude the the best match. |
+ exclude_interval = std::make_pair(2, 5); |
+ EXPECT_EQ(7, internal::PartialSearch( |
+ 0, kNumCandidBlocks - 1, exclude_interval, target.get(), |
+ search_region.get(), energy_target.get(), energy_candid_blocks.get())); |
+ |
+ // An interval which is of no effect. |
+ exclude_interval = std::make_pair(-100, -10); |
+ EXPECT_EQ(4, internal::DecimatedSearch( |
+ 4, exclude_interval, target.get(), search_region.get(), |
+ energy_target.get(), energy_candid_blocks.get())); |
+ |
+ EXPECT_EQ(5, internal::OptimalIndex(search_region.get(), target.get(), |
+ exclude_interval)); |
+} |
+ |
+TEST_F(AudioRendererAlgorithmTest, CubicInterpolation) { |
+ // Arbitrary coefficients. |
+ const float kA = 0.7; |
+ const float kB = 1.2; |
+ const float kC = 0.8; |
+ |
+ float y_values[3]; |
+ y_values[0] = kA - kB + kC; |
+ y_values[1] = kC; |
+ y_values[2] = kA + kB + kC; |
+ |
+ float extremum; |
+ float extremum_value; |
+ |
+ internal::CubicInterpolation(y_values, &extremum, &extremum_value); |
+ |
+ float x_star = -kB / (2.f * kA); |
+ float y_star = kA * x_star * x_star + kB * x_star + kC; |
+ |
+ EXPECT_FLOAT_EQ(x_star, extremum); |
+ EXPECT_FLOAT_EQ(y_star, extremum_value); |
+} |
+ |
+TEST_F(AudioRendererAlgorithmTest, WsolaSlowdown) { |
+ WsolaTest(1.6f); |
+} |
+ |
+TEST_F(AudioRendererAlgorithmTest, WsolaSpeedup) { |
+ WsolaTest(0.6f); |
+} |
+ |
} // namespace media |