| // Copyright 2022 The Fuchsia 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 "src/media/audio/lib/processing/sinc_sampler.h" |
| |
| #include <fidl/fuchsia.audio/cpp/wire_types.h> |
| #include <lib/syslog/cpp/macros.h> |
| |
| #include <cstdint> |
| #include <utility> |
| #include <vector> |
| |
| #include <ffl/string.h> |
| #include <gmock/gmock.h> |
| #include <gtest/gtest.h> |
| |
| #include "src/media/audio/lib/format2/channel_mapper.h" |
| #include "src/media/audio/lib/format2/fixed.h" |
| #include "src/media/audio/lib/format2/sample_converter.h" |
| #include "src/media/audio/lib/processing/filter.h" |
| #include "src/media/audio/lib/processing/gain.h" |
| #include "src/media/audio/lib/timeline/timeline_rate.h" |
| |
| namespace media_audio { |
| namespace { |
| |
| using ::fuchsia_audio::SampleType; |
| using ::media::TimelineRate; |
| using ::testing::Each; |
| using ::testing::FloatEq; |
| using ::testing::IsNull; |
| using ::testing::NotNull; |
| using ::testing::Pointwise; |
| |
| constexpr std::pair<uint32_t, uint32_t> kChannelConfigs[] = { |
| {1, 1}, {1, 2}, {1, 3}, {1, 4}, {2, 1}, {2, 2}, {2, 3}, {2, 4}, |
| {3, 1}, {3, 2}, {3, 3}, {3, 4}, {4, 1}, {4, 2}, {4, 3}, {4, 4}, |
| }; |
| |
| constexpr uint32_t kFrameRates[] = { |
| 8000, 11025, 16000, 22050, 24000, 32000, 44100, 48000, 88200, 96000, 176400, 192000, |
| }; |
| |
| constexpr SampleType kSampleTypes[] = { |
| SampleType::kUint8, |
| SampleType::kInt16, |
| SampleType::kInt32, |
| SampleType::kFloat32, |
| }; |
| |
| Format CreateFormat(int64_t channel_count, int64_t frame_rate, SampleType sample_type) { |
| return Format::CreateOrDie({sample_type, channel_count, frame_rate}); |
| } |
| |
| TEST(SincSamplerTest, CreateWithValidConfigs) { |
| for (const auto& [source_channel_count, dest_channel_count] : kChannelConfigs) { |
| for (const auto& source_frame_rate : kFrameRates) { |
| for (const auto& dest_frame_rate : kFrameRates) { |
| for (const auto& sample_type : kSampleTypes) { |
| EXPECT_THAT(SincSampler::Create( |
| CreateFormat(source_channel_count, source_frame_rate, sample_type), |
| CreateFormat(dest_channel_count, dest_frame_rate, SampleType::kFloat32)), |
| NotNull()); |
| } |
| } |
| } |
| } |
| } |
| |
| TEST(SincSamplerTest, CreateFailsWithUnsupportedChannelConfigs) { |
| const std::pair<uint32_t, uint32_t> unsupported_channel_configs[] = { |
| {1, 5}, {2, 5}, {3, 5}, {4, 5}, {5, 1}, {5, 5}, {6, 2}, {6, 6}, {9, 1}, |
| }; |
| for (const auto& [source_channel_count, dest_channel_count] : unsupported_channel_configs) { |
| for (const auto& frame_rate : kFrameRates) { |
| for (const auto& sample_type : kSampleTypes) { |
| EXPECT_THAT( |
| SincSampler::Create(CreateFormat(source_channel_count, frame_rate, sample_type), |
| CreateFormat(dest_channel_count, frame_rate, SampleType::kFloat32)), |
| IsNull()); |
| } |
| } |
| } |
| } |
| |
| TEST(SincSamplerTest, CreateFailsWithUnsupportedDestSampleFormats) { |
| const uint32_t frame_rate = 44100; |
| for (const auto& [source_channel_count, dest_channel_count] : kChannelConfigs) { |
| for (const auto& source_sample_type : kSampleTypes) { |
| for (const auto& dest_sample_type : kSampleTypes) { |
| if (dest_sample_type != SampleType::kFloat32) { |
| EXPECT_THAT(SincSampler::Create( |
| CreateFormat(source_channel_count, frame_rate, source_sample_type), |
| CreateFormat(dest_channel_count, frame_rate, dest_sample_type)), |
| IsNull()); |
| } |
| } |
| } |
| } |
| } |
| |
| TEST(SincSamplerTest, Process) { |
| auto sampler = SincSampler::Create(CreateFormat(1, 48000, SampleType::kFloat32), |
| CreateFormat(1, 48000, SampleType::kFloat32)); |
| ASSERT_THAT(sampler, NotNull()); |
| |
| const int64_t dest_frame_count = 5; |
| // Make sure to provide enough samples to compensate for the filter length. |
| const int64_t source_frame_count = dest_frame_count + sampler->pos_filter_length().Floor(); |
| |
| std::vector<float> source_samples(source_frame_count); |
| for (int i = 0; i < source_frame_count; ++i) { |
| source_samples[i] = static_cast<float>(i + 1); |
| } |
| std::vector<float> dest_samples(dest_frame_count, 1.0f); |
| |
| Fixed source_offset = Fixed(0); |
| int64_t dest_offset = 0; |
| |
| // All source samples should be accumulated into destination samples as-is. |
| sampler->Process({source_samples.data(), &source_offset, source_frame_count}, |
| {dest_samples.data(), &dest_offset, dest_frame_count}, |
| {.type = GainType::kUnity, .scale = kUnityGainScale}, |
| /*accumulate=*/true); |
| for (int i = 0; i < dest_frame_count; ++i) { |
| EXPECT_FLOAT_EQ(dest_samples[i], source_samples[i] + 1.0f) << i; |
| } |
| } |
| |
| TEST(SincSamplerTest, ProcessDownSample) { |
| const uint32_t kSourceFrameRate = 48000; |
| const uint32_t kDestFrameRate = 44100; |
| auto sampler = SincSampler::Create(CreateFormat(1, kSourceFrameRate, SampleType::kFloat32), |
| CreateFormat(1, kDestFrameRate, SampleType::kFloat32)); |
| ASSERT_THAT(sampler, NotNull()); |
| |
| const int64_t dest_frame_count = 512; |
| std::vector<float> dest_samples(dest_frame_count, 0.0f); |
| int64_t dest_offset = 0; |
| |
| const int64_t source_frame_count = dest_frame_count / 2; |
| const std::vector<float> source_samples(source_frame_count, 1.0f); |
| Fixed source_offset = Fixed(0); |
| |
| sampler->state().ResetSourceStride( |
| TimelineRate(Fixed(kSourceFrameRate).raw_value(), kDestFrameRate)); |
| |
| // Process the first half of the destination. |
| sampler->Process({source_samples.data(), &source_offset, source_frame_count}, |
| {dest_samples.data(), &dest_offset, dest_frame_count}, |
| {.type = GainType::kUnity, .scale = kUnityGainScale}, |
| /*accumulate=*/false); |
| EXPECT_GT(source_offset + sampler->pos_filter_length(), Fixed(source_frame_count)); |
| const int64_t first_half_dest_offset = dest_offset; |
| |
| // Now process the rest. |
| source_offset -= Fixed(source_frame_count); |
| sampler->Process({source_samples.data(), &source_offset, source_frame_count}, |
| {dest_samples.data(), &dest_offset, dest_frame_count}, |
| {.type = GainType::kUnity, .scale = kUnityGainScale}, |
| /*accumulate=*/false); |
| EXPECT_GT(source_offset + sampler->pos_filter_length(), Fixed(source_frame_count)); |
| |
| // The "seam" between the source and destination samples should be invisible. |
| const float kSeamThreshold = 0.001f; |
| for (int64_t i = first_half_dest_offset - 2; i < first_half_dest_offset + 2; ++i) { |
| EXPECT_NEAR(dest_samples[i], 1.0f, kSeamThreshold) << i; |
| } |
| } |
| |
| TEST(SincSamplerTest, ProcessUpSample) { |
| const uint32_t kSourceFrameRate = 12000; |
| const uint32_t kDestFrameRate = 48000; |
| auto sampler = SincSampler::Create(CreateFormat(1, kSourceFrameRate, SampleType::kFloat32), |
| CreateFormat(1, kDestFrameRate, SampleType::kFloat32)); |
| ASSERT_THAT(sampler, NotNull()); |
| |
| const int64_t dest_frame_count = 1024; |
| std::vector<float> dest_samples(dest_frame_count, 0.0f); |
| int64_t dest_offset = 0; |
| |
| const int64_t source_frame_count = dest_frame_count / 8; |
| const std::vector<float> source_samples(source_frame_count, 1.0f); |
| Fixed source_offset = Fixed(0); |
| |
| sampler->state().ResetSourceStride( |
| TimelineRate(Fixed(kSourceFrameRate).raw_value(), kDestFrameRate)); |
| |
| // Process the first half of the destination. |
| sampler->Process({source_samples.data(), &source_offset, source_frame_count}, |
| {dest_samples.data(), &dest_offset, dest_frame_count}, |
| {.type = GainType::kUnity, .scale = kUnityGainScale}, |
| /*accumulate=*/false); |
| EXPECT_GT(source_offset + sampler->pos_filter_length(), Fixed(source_frame_count)); |
| EXPECT_EQ(Fixed(source_offset * 4).Floor(), dest_offset); |
| const int64_t first_half_dest_offset = dest_offset; |
| |
| // Now process the rest. |
| source_offset -= Fixed(source_frame_count); |
| sampler->Process({source_samples.data(), &source_offset, source_frame_count}, |
| {dest_samples.data(), &dest_offset, dest_frame_count}, |
| {.type = GainType::kUnity, .scale = kUnityGainScale}, |
| /*accumulate=*/false); |
| EXPECT_GT(source_offset + sampler->pos_filter_length(), Fixed(source_frame_count)); |
| |
| // The "seam" between the source and destination samples should be invisible. |
| const float kSeamThreshold = 0.001f; |
| for (int64_t i = first_half_dest_offset - 2; i < first_half_dest_offset + 2; ++i) { |
| EXPECT_NEAR(dest_samples[i], 1.0f, kSeamThreshold) << i; |
| } |
| } |
| |
| TEST(SincSamplerTest, ProcessWithConstantGain) { |
| auto sampler = SincSampler::Create(CreateFormat(1, 48000, SampleType::kFloat32), |
| CreateFormat(1, 48000, SampleType::kFloat32)); |
| ASSERT_THAT(sampler, NotNull()); |
| |
| const int64_t dest_frame_count = 5; |
| // Make sure to provide enough samples to compensate for the filter length. |
| const int64_t source_frame_count = dest_frame_count + sampler->pos_filter_length().Floor(); |
| |
| std::vector<float> source_samples(source_frame_count); |
| for (int i = 0; i < source_frame_count; ++i) { |
| source_samples[i] = static_cast<float>(i + 1); |
| } |
| std::vector<float> dest_samples(dest_frame_count, 1.0f); |
| |
| Fixed source_offset = Fixed(0); |
| int64_t dest_offset = 0; |
| |
| // Source samples should be scaled with constant gain and accumulated into destination samples. |
| sampler->Process({source_samples.data(), &source_offset, source_frame_count}, |
| {dest_samples.data(), &dest_offset, dest_frame_count}, |
| {.type = GainType::kNonUnity, .scale = 10.0f}, |
| /*accumulate=*/true); |
| for (int i = 0; i < dest_frame_count; ++i) { |
| EXPECT_FLOAT_EQ(dest_samples[i], 10.0f * static_cast<float>(i + 1) + 1.0f) << i; |
| } |
| } |
| |
| TEST(SincSamplerTest, ProcessWithRampingGain) { |
| auto sampler = SincSampler::Create(CreateFormat(1, 48000, SampleType::kFloat32), |
| CreateFormat(1, 48000, SampleType::kFloat32)); |
| ASSERT_THAT(sampler, NotNull()); |
| |
| const int64_t dest_frame_count = 5; |
| // Make sure to provide enough samples to compensate for the filter length. |
| const int64_t source_frame_count = dest_frame_count + sampler->pos_filter_length().Floor(); |
| |
| std::vector<float> source_samples(source_frame_count); |
| for (int i = 0; i < source_frame_count; ++i) { |
| source_samples[i] = static_cast<float>(i + 1); |
| } |
| std::vector<float> dest_samples(dest_frame_count, 1.0f); |
| |
| Fixed source_offset = Fixed(0); |
| int64_t dest_offset = 0; |
| |
| // Source samples should be scaled with ramping gain and accumulated into destination samples. |
| const std::vector<float> scale_ramp = {2.0f, 4.0f, 6.0f, 8.0f, 10.0f}; |
| sampler->Process({source_samples.data(), &source_offset, source_frame_count}, |
| {dest_samples.data(), &dest_offset, dest_frame_count}, |
| {.type = GainType::kRamping, .scale_ramp = scale_ramp.data()}, |
| /*accumulate=*/true); |
| for (int i = 0; i < dest_frame_count; ++i) { |
| EXPECT_FLOAT_EQ(dest_samples[i], scale_ramp[i] * static_cast<float>(i + 1) + 1.0f) << i; |
| } |
| } |
| |
| TEST(SincSamplerTest, ProcessWithSilentGain) { |
| auto sampler = SincSampler::Create(CreateFormat(1, 48000, SampleType::kFloat32), |
| CreateFormat(1, 48000, SampleType::kFloat32)); |
| ASSERT_THAT(sampler, NotNull()); |
| |
| const int64_t dest_frame_count = 5; |
| // Make sure to provide enough samples to compensate for the filter length. |
| const int64_t source_frame_count = dest_frame_count + sampler->pos_filter_length().Floor(); |
| |
| std::vector<float> source_samples(source_frame_count); |
| for (int i = 0; i < source_frame_count; ++i) { |
| source_samples[i] = static_cast<float>(i + 1); |
| } |
| std::vector<float> dest_samples(dest_frame_count, 1.0f); |
| |
| Fixed source_offset = Fixed(0); |
| int64_t dest_offset = 0; |
| |
| // Nothing should be accumulated into destination samples when gain is silent. |
| sampler->Process({source_samples.data(), &source_offset, source_frame_count}, |
| {dest_samples.data(), &dest_offset, dest_frame_count}, |
| {.type = GainType::kSilent, .scale = kMinGainScale}, |
| /*accumulate=*/true); |
| EXPECT_THAT(dest_samples, Each(1.0f)); |
| |
| // If no accumulation, destination samples should be filled with zeros. |
| source_offset = Fixed(0); |
| dest_offset = 0; |
| sampler->Process({source_samples.data(), &source_offset, source_frame_count}, |
| {dest_samples.data(), &dest_offset, dest_frame_count}, |
| {.type = GainType::kSilent, .scale = kMinGainScale}, |
| /*accumulate=*/false); |
| EXPECT_THAT(dest_samples, Each(0.0f)); |
| } |
| |
| TEST(SincSamplerTest, ProcessWithSourceOffsetAtFrameBoundary) { |
| auto sampler = SincSampler::Create(CreateFormat(1, 44100, SampleType::kFloat32), |
| CreateFormat(1, 44100, SampleType::kFloat32)); |
| ASSERT_THAT(sampler, NotNull()); |
| |
| // Source offset is 46 of 50 frames, destination offset is 1 of 10, which should advance by 4. |
| const int64_t dest_frame_count = 10; |
| std::vector<float> dest_samples(dest_frame_count); |
| int64_t dest_offset = 1; |
| |
| const int64_t source_frame_count = 50; |
| std::vector<float> source_samples(source_frame_count); |
| Fixed source_offset = |
| Fixed(source_frame_count - 4) - sampler->pos_filter_length() + Fixed::FromRaw(1); |
| |
| const int64_t expected_advance = 4; |
| const Fixed expected_source_offset = source_offset + Fixed(expected_advance); |
| const int64_t expected_dest_offset = dest_offset + expected_advance; |
| |
| sampler->Process({source_samples.data(), &source_offset, source_frame_count}, |
| {dest_samples.data(), &dest_offset, dest_frame_count}, |
| {.type = GainType::kSilent, .scale = kMinGainScale}, |
| /*accumulate=*/false); |
| EXPECT_EQ(dest_offset, expected_dest_offset); |
| EXPECT_EQ(source_offset, expected_source_offset) << ffl::String::DecRational << source_offset; |
| } |
| |
| TEST(SincSamplerTest, ProcessWithSourceOffsetJustBeforeFrameBoundary) { |
| auto sampler = SincSampler::Create(CreateFormat(1, 44100, SampleType::kFloat32), |
| CreateFormat(1, 44100, SampleType::kFloat32)); |
| ASSERT_THAT(sampler, NotNull()); |
| |
| // Source offset is 45.99 of 50 frames, destination offset is 1 of 10, which should advance by 5. |
| const int64_t dest_frame_count = 10; |
| std::vector<float> dest_samples(dest_frame_count); |
| int64_t dest_offset = 1; |
| |
| const int64_t source_frame_count = 50; |
| std::vector<float> source_samples(source_frame_count); |
| Fixed source_offset = Fixed(source_frame_count - 4) - sampler->pos_filter_length(); |
| |
| const int64_t expected_advance = 5; |
| const Fixed expected_source_offset = source_offset + Fixed(expected_advance); |
| const int64_t expected_dest_offset = dest_offset + expected_advance; |
| |
| sampler->Process({source_samples.data(), &source_offset, source_frame_count}, |
| {dest_samples.data(), &dest_offset, dest_frame_count}, |
| {.type = GainType::kSilent, .scale = kMinGainScale}, |
| /*accumulate=*/false); |
| EXPECT_EQ(dest_offset, expected_dest_offset); |
| EXPECT_EQ(source_offset, expected_source_offset) << ffl::String::DecRational << source_offset; |
| } |
| |
| TEST(SincSamplerTest, ProcessWithSourceOffsetAtEnd) { |
| auto sampler = SincSampler::Create(CreateFormat(1, 44100, SampleType::kFloat32), |
| CreateFormat(1, 44100, SampleType::kFloat32)); |
| ASSERT_THAT(sampler, NotNull()); |
| |
| // Source offset is at the end already, destination offset is 0 of 50, which should not advance. |
| const int64_t dest_frame_count = 50; |
| std::vector<float> dest_samples(dest_frame_count); |
| int64_t dest_offset = 0; |
| |
| const int64_t source_frame_count = 50; |
| std::vector<float> source_samples(source_frame_count); |
| Fixed source_offset = |
| Fixed(source_frame_count) - sampler->pos_filter_length() + Fixed::FromRaw(1); |
| const Fixed expected_source_offset = source_offset; |
| |
| sampler->Process({source_samples.data(), &source_offset, source_frame_count}, |
| {dest_samples.data(), &dest_offset, dest_frame_count}, |
| {.type = GainType::kSilent, .scale = kMinGainScale}, |
| /*accumulate=*/false); |
| EXPECT_EQ(dest_offset, 0); |
| EXPECT_EQ(source_offset, expected_source_offset) << ffl::String::DecRational << source_offset; |
| } |
| |
| TEST(SincSamplerTest, ProcessWithZeroStepSizeModuloNoRollover) { |
| auto sampler = SincSampler::Create(CreateFormat(1, 44100, SampleType::kFloat32), |
| CreateFormat(1, 44100, SampleType::kFloat32)); |
| ASSERT_THAT(sampler, NotNull()); |
| |
| auto& state = sampler->state(); |
| state.ResetSourceStride(TimelineRate(Fixed(10000).raw_value() + 3333, 10000)); |
| EXPECT_EQ(state.step_size(), kOneFrame); |
| EXPECT_EQ(state.step_size_modulo(), 3333ul); |
| EXPECT_EQ(state.step_size_denominator(), 10000ul); |
| |
| const int64_t dest_frame_count = 3; |
| std::vector<float> dest_samples(dest_frame_count); |
| int64_t dest_offset = 0; |
| |
| const int64_t source_frame_count = 50; |
| std::vector<float> source_samples(source_frame_count); |
| Fixed source_offset = Fixed(0); |
| |
| sampler->Process({source_samples.data(), &source_offset, source_frame_count}, |
| {dest_samples.data(), &dest_offset, dest_frame_count}, |
| {.type = GainType::kSilent, .scale = kMinGainScale}, |
| /*accumulate=*/false); |
| EXPECT_EQ(dest_offset, dest_frame_count); |
| EXPECT_EQ(source_offset, Fixed(3)) << ffl::String::DecRational << source_offset; |
| EXPECT_EQ(state.source_pos_modulo(), 9999u); |
| } |
| |
| TEST(SincSamplerTest, ProcessWithZeroStepSizeModuloWithRollover) { |
| auto sampler = SincSampler::Create(CreateFormat(1, 44100, SampleType::kFloat32), |
| CreateFormat(1, 44100, SampleType::kFloat32)); |
| ASSERT_THAT(sampler, NotNull()); |
| |
| auto& state = sampler->state(); |
| state.ResetSourceStride(TimelineRate(Fixed(10000).raw_value() + 5000, 10000)); |
| EXPECT_EQ(state.step_size(), kOneFrame); |
| EXPECT_EQ(state.step_size_modulo(), 1ul); |
| EXPECT_EQ(state.step_size_denominator(), 2ul); |
| |
| const int64_t dest_frame_count = 3; |
| std::vector<float> dest_samples(dest_frame_count); |
| int64_t dest_offset = 1; |
| |
| const int64_t source_frame_count = 50; |
| std::vector<float> source_samples(source_frame_count); |
| Fixed source_offset = kOneFrame - Fixed::FromRaw(1); |
| |
| sampler->Process({source_samples.data(), &source_offset, source_frame_count}, |
| {dest_samples.data(), &dest_offset, dest_frame_count}, |
| {.type = GainType::kSilent, .scale = kMinGainScale}, |
| /*accumulate=*/false); |
| EXPECT_EQ(dest_offset, dest_frame_count); |
| EXPECT_EQ(source_offset, Fixed(3)) << ffl::String::DecRational << source_offset; |
| EXPECT_EQ(state.source_pos_modulo(), 0u); |
| } |
| |
| TEST(SincSamplerTest, ProcessWithNonZeroStepSizeModuloNoRollover) { |
| auto sampler = SincSampler::Create(CreateFormat(1, 44100, SampleType::kFloat32), |
| CreateFormat(1, 44100, SampleType::kFloat32)); |
| ASSERT_THAT(sampler, NotNull()); |
| |
| auto& state = sampler->state(); |
| state.ResetSourceStride(TimelineRate(Fixed(10000).raw_value() + 3331, 10000)); |
| EXPECT_EQ(state.step_size(), kOneFrame); |
| EXPECT_EQ(state.step_size_modulo(), 3331ul); |
| EXPECT_EQ(state.step_size_denominator(), 10000ul); |
| state.set_source_pos_modulo(6); |
| |
| const int64_t dest_frame_count = 3; |
| std::vector<float> dest_samples(dest_frame_count); |
| int64_t dest_offset = 0; |
| |
| const int64_t source_frame_count = 50; |
| std::vector<float> source_samples(source_frame_count); |
| Fixed source_offset = Fixed(0); |
| |
| sampler->Process({source_samples.data(), &source_offset, source_frame_count}, |
| {dest_samples.data(), &dest_offset, dest_frame_count}, |
| {.type = GainType::kSilent, .scale = kMinGainScale}, |
| /*accumulate=*/false); |
| EXPECT_EQ(dest_offset, dest_frame_count); |
| EXPECT_EQ(source_offset, Fixed(3)) << ffl::String::DecRational << source_offset; |
| EXPECT_EQ(state.source_pos_modulo(), 9999u); |
| } |
| |
| TEST(SincSamplerTest, ProcessWithNonZeroStepSizeModuloRollover) { |
| auto sampler = SincSampler::Create(CreateFormat(1, 44100, SampleType::kFloat32), |
| CreateFormat(1, 44100, SampleType::kFloat32)); |
| ASSERT_THAT(sampler, NotNull()); |
| |
| auto& state = sampler->state(); |
| state.ResetSourceStride(TimelineRate(Fixed(10000).raw_value() + 3331, 10000)); |
| EXPECT_EQ(state.step_size(), kOneFrame); |
| EXPECT_EQ(state.step_size_modulo(), 3331ul); |
| EXPECT_EQ(state.step_size_denominator(), 10000ul); |
| state.set_source_pos_modulo(3338); |
| |
| const int64_t dest_frame_count = 3; |
| std::vector<float> dest_samples(dest_frame_count); |
| int64_t dest_offset = 1; |
| |
| const int64_t source_frame_count = 50; |
| std::vector<float> source_samples(source_frame_count); |
| Fixed source_offset = Fixed(1) - Fixed::FromRaw(1); |
| |
| sampler->Process({source_samples.data(), &source_offset, source_frame_count}, |
| {dest_samples.data(), &dest_offset, dest_frame_count}, |
| {.type = GainType::kSilent, .scale = kMinGainScale}, |
| /*accumulate=*/false); |
| EXPECT_EQ(dest_offset, dest_frame_count); |
| EXPECT_EQ(source_offset, Fixed(3)) << ffl::String::DecRational << source_offset; |
| EXPECT_EQ(state.source_pos_modulo(), 0u); |
| } |
| |
| TEST(SincSamplerTest, ProcessWithSourcePostModuloExactRollover) { |
| auto sampler = SincSampler::Create(CreateFormat(1, 44100, SampleType::kFloat32), |
| CreateFormat(1, 44100, SampleType::kFloat32)); |
| ASSERT_THAT(sampler, NotNull()); |
| |
| auto& state = sampler->state(); |
| state.ResetSourceStride(TimelineRate(Fixed(3).raw_value() - 1, 3)); |
| EXPECT_EQ(state.step_size(), kOneFrame - Fixed::FromRaw(1)); |
| EXPECT_EQ(state.step_size_modulo(), 2ul); |
| EXPECT_EQ(state.step_size_denominator(), 3ul); |
| state.set_source_pos_modulo(2); |
| |
| const int64_t dest_frame_count = 3; |
| std::vector<float> dest_samples(dest_frame_count); |
| int64_t dest_offset = 0; |
| |
| const int64_t source_frame_count = 10; |
| std::vector<float> source_samples(source_frame_count); |
| Fixed source_offset = |
| Fixed(source_frame_count - 2) - sampler->pos_filter_length() + Fixed::FromRaw(1); |
| |
| sampler->Process({source_samples.data(), &source_offset, source_frame_count}, |
| {dest_samples.data(), &dest_offset, dest_frame_count}, |
| {.type = GainType::kSilent, .scale = kMinGainScale}, |
| /*accumulate=*/false); |
| EXPECT_EQ(dest_offset, 2); |
| EXPECT_EQ(source_offset, |
| Fixed(Fixed(source_frame_count) - sampler->pos_filter_length() + Fixed::FromRaw(1))) |
| << ffl::String::DecRational << source_offset; |
| EXPECT_EQ(state.source_pos_modulo(), 0u); |
| } |
| |
| class SincSamplerOutputTest : public testing::Test { |
| protected: |
| // Based on an arbitrary near-zero source position (-1/128), with a sinc curve for unity rate |
| // conversion, we use data values calculated so that if these first 13 values (the filter's |
| // negative wing) are ignored, we expect a generated output value of |
| // `kValueWithoutPreviousFrames`. If they are NOT ignored, then we expect the result |
| // `kValueWithPreviousFrames`. |
| static constexpr float kSource[] = { |
| 1330.10897f, -1330.10897f, 1330.10897f, -1330.10897f, 1330.10897f, -1330.10897f, |
| 1330.10897f, -1330.10897f, 1330.10897f, -1330.10897f, 1330.10897f, -1330.10897f, |
| 1330.10897f, // ... source frames to satisfy negative filter length. |
| -10.001010f, // Center source frame |
| 268.88298f, // Source frames to satisfy positive filter length ... |
| -268.88298f, 268.88298f, -268.88298f, 268.88298f, -268.88298f, 268.88298f, |
| -268.88298f, 268.88298f, -268.88298f, 268.88298f, -268.88298f, 268.88298f, |
| }; |
| static constexpr Fixed kProcessOneFrameSourceOffset = ffl::FromRatio(1, 128); |
| // The center frame should contribute -10.0, the positive wing -5.0, and the negative wing +25.0. |
| static constexpr float kValueWithoutPreviousFrames = -15.0; |
| static constexpr float kValueWithPreviousFrames = 10.0; |
| |
| // Processes a single frame of output based on `kSource[0]`. |
| static float ProcessOneFrame(Sampler& sampler, Fixed source_offset) { |
| auto neg_length = sampler.neg_filter_length().Floor(); |
| auto pos_length = sampler.pos_filter_length().Floor(); |
| EXPECT_NE(Fixed(pos_length).raw_value(), sampler.neg_filter_length().raw_value() - 1) |
| << "This test assumes SincSampler is symmetric, and that negative width includes a " |
| "fraction"; |
| |
| float dest_sample = 0.0f; |
| int64_t dest_offset = 0; |
| |
| sampler.Process(Sampler::Source{&kSource[neg_length - 1], &source_offset, pos_length}, |
| Sampler::Dest{&dest_sample, &dest_offset, 1}, {}, false); |
| EXPECT_EQ(dest_offset, 1u) << "No output frame was produced"; |
| |
| FX_LOGS(INFO) << "Coefficients " << std::setprecision(12) << kSource[12] << " " << kSource[13] |
| << " " << kSource[14] << ", value " << dest_sample; |
| |
| return dest_sample; |
| } |
| |
| // Tests sampler with a passthrough process of a given `source_samples` with a given |
| // `channel_count` and `source_sample_type`. |
| template <typename SourceSampleType> |
| static void TestPassthrough(uint32_t channel_count, SampleType source_sample_type, |
| const std::vector<SourceSampleType>& source_samples) { |
| // Create sampler. |
| auto sampler = SincSampler::Create(CreateFormat(channel_count, 48000, source_sample_type), |
| CreateFormat(channel_count, 48000, SampleType::kFloat32)); |
| ASSERT_THAT(sampler, NotNull()); |
| EXPECT_EQ(sampler->pos_filter_length().raw_value(), SincFilter::kFracSideLength); |
| EXPECT_EQ(sampler->neg_filter_length().raw_value(), SincFilter::kFracSideLength); |
| |
| // Process samples with unity gain. |
| const int64_t dest_frame_count = static_cast<int64_t>(source_samples.size() / channel_count); |
| // Make sure to provide enough samples to compensate for the filter length. |
| const int64_t source_frame_count = dest_frame_count + sampler->pos_filter_length().Floor(); |
| auto padded_source_samples = source_samples; |
| padded_source_samples.insert(padded_source_samples.end(), |
| channel_count * sampler->pos_filter_length().Floor(), 0.0f); |
| |
| Fixed source_offset = Fixed(); |
| std::vector<float> dest_samples(source_samples.size(), 0.0f); |
| int64_t dest_offset = 0; |
| |
| sampler->Process({padded_source_samples.data(), &source_offset, source_frame_count}, |
| {dest_samples.data(), &dest_offset, dest_frame_count}, |
| {.type = GainType::kUnity, .scale = kUnityGainScale}, |
| /*accumulate=*/false); |
| EXPECT_EQ(dest_offset, dest_frame_count); |
| EXPECT_EQ((source_offset), Fixed(dest_frame_count)); |
| for (int i = 0; i < dest_frame_count; ++i) { |
| EXPECT_FLOAT_EQ(SampleConverter<SourceSampleType>::ToFloat(source_samples[i]), |
| dest_samples[i]) |
| << i; |
| } |
| } |
| |
| // Tests sampler with a rechannelization process of a given `source_samples` against |
| // `expected_dest_samples` for a given `SourceChannelCount` and `DestChannelCount`. |
| template <uint32_t SourceChannelCount, uint32_t DestChannelCount> |
| static void TestRechannelization(const std::vector<float>& source_samples, |
| const std::vector<float>& expected_dest_samples) { |
| // Create sampler. |
| auto sampler = |
| SincSampler::Create(CreateFormat(SourceChannelCount, 48000, SampleType::kFloat32), |
| CreateFormat(DestChannelCount, 48000, SampleType::kFloat32)); |
| EXPECT_EQ(sampler->pos_filter_length().raw_value(), SincFilter::kFracSideLength); |
| EXPECT_EQ(sampler->neg_filter_length().raw_value(), SincFilter::kFracSideLength); |
| |
| // Process samples with unity gain. |
| const int64_t dest_frame_count = |
| static_cast<int64_t>(source_samples.size() / SourceChannelCount); |
| ASSERT_EQ(dest_frame_count * DestChannelCount, |
| static_cast<int64_t>(expected_dest_samples.size())); |
| // Make sure to provide enough samples to compensate for the filter length. |
| const int64_t source_frame_count = dest_frame_count + sampler->pos_filter_length().Floor(); |
| auto padded_source_samples = source_samples; |
| padded_source_samples.insert(padded_source_samples.end(), |
| SourceChannelCount * sampler->pos_filter_length().Floor(), 0.0f); |
| |
| Fixed source_offset = Fixed(0); |
| std::vector<float> dest_samples(expected_dest_samples.size(), 0.0f); |
| int64_t dest_offset = 0; |
| |
| sampler->Process({padded_source_samples.data(), &source_offset, source_frame_count}, |
| {dest_samples.data(), &dest_offset, dest_frame_count}, |
| {.type = GainType::kUnity, .scale = kUnityGainScale}, |
| /*accumulate=*/false); |
| EXPECT_EQ(dest_offset, dest_frame_count); |
| EXPECT_EQ((source_offset), Fixed(dest_frame_count)); |
| EXPECT_THAT(dest_samples, Pointwise(FloatEq(), expected_dest_samples)); |
| } |
| }; |
| |
| TEST_F(SincSamplerOutputTest, ProcessOneNoCache) { |
| auto sampler = SincSampler::Create(CreateFormat(1, 44100, SampleType::kFloat32), |
| CreateFormat(1, 44100, SampleType::kFloat32)); |
| ASSERT_THAT(sampler, NotNull()); |
| |
| // Process a single frame. We use a slightly non-zero position because at true 0, only the sample |
| // (not the positive or negative wings) are used. In this case we not provided previous frames. |
| const float dest_sample = ProcessOneFrame(*sampler, -kProcessOneFrameSourceOffset); |
| |
| // If we incorrectly shifted/retained even a single frame of the above data, this won't match. |
| EXPECT_FLOAT_EQ(dest_sample, kValueWithoutPreviousFrames) << std::setprecision(12) << dest_sample; |
| } |
| |
| TEST_F(SincSamplerOutputTest, ProcessOneWithCache) { |
| auto sampler = SincSampler::Create(CreateFormat(1, 44100, SampleType::kFloat32), |
| CreateFormat(1, 44100, SampleType::kFloat32)); |
| ASSERT_THAT(sampler, NotNull()); |
| auto neg_length = sampler->neg_filter_length().Floor(); |
| |
| // Now, populate the cache with previous frames, instead of using default (silence) values. |
| // The output value of `source_offset` tells us the cache is populated with `neg_length - 1` |
| // frames, which is ideal for sampling a subsequent source buffer starting at source position 0. |
| float dest_sample = 0.0f; |
| int64_t dest_offset = 0; |
| const auto source_frame_count = neg_length - 1; |
| Fixed source_offset = Fixed(source_frame_count) - kProcessOneFrameSourceOffset; |
| |
| sampler->Process(Sampler::Source{&kSource[0], &source_offset, source_frame_count}, |
| Sampler::Dest{&dest_sample, &dest_offset, 1}, {}, false); |
| EXPECT_EQ(source_offset, Fixed(source_frame_count) - kProcessOneFrameSourceOffset); |
| EXPECT_EQ(dest_offset, 0u) << "Unexpectedly produced output " << dest_sample; |
| |
| // Process a single frame. We use a slightly non-zero position because at true 0, only the sample |
| // itself (not positive or negative widths) are needed. In this case we provide previous frames. |
| dest_sample = ProcessOneFrame(*sampler, -kProcessOneFrameSourceOffset); |
| |
| // If we incorrectly shifted/retained even a single frame of the above data, this won't match. |
| EXPECT_FLOAT_EQ(dest_sample, kValueWithPreviousFrames) << std::setprecision(12) << dest_sample; |
| } |
| |
| TEST_F(SincSamplerOutputTest, ProcessFrameByFrameCached) { |
| auto sampler = SincSampler::Create(CreateFormat(1, 44100, SampleType::kFloat32), |
| CreateFormat(1, 44100, SampleType::kFloat32)); |
| ASSERT_THAT(sampler, NotNull()); |
| auto neg_length = sampler->neg_filter_length().Floor(); |
| |
| // Now, populate the cache with previous data, one frame at a time. |
| float dest_sample = 0.0f; |
| int64_t dest_offset = 0; |
| const auto source_frame_count = 1; |
| Fixed source_offset = Fixed(source_frame_count) - kProcessOneFrameSourceOffset; |
| |
| for (auto neg_idx = 0; neg_idx < neg_length - 1; ++neg_idx) { |
| sampler->Process(Sampler::Source{&kSource[neg_idx], &source_offset, source_frame_count}, |
| Sampler::Dest{&dest_sample, &dest_offset, 1}, {}, false); |
| EXPECT_EQ(source_offset, Fixed(source_frame_count) - kProcessOneFrameSourceOffset); |
| EXPECT_EQ(dest_offset, 0u) << "Unexpectedly produced output " << dest_sample; |
| } |
| |
| // Process a single frame. We use a slightly non-zero position because at true 0, only the sample |
| // itself (not positive or negative widths) are needed. In this case we provide previous frames. |
| dest_sample = ProcessOneFrame(*sampler, -kProcessOneFrameSourceOffset); |
| |
| // If we incorrectly shifted/retained even a single frame of the above data, this won't match. |
| EXPECT_FLOAT_EQ(dest_sample, kValueWithPreviousFrames) << std::setprecision(12) << dest_sample; |
| } |
| |
| TEST_F(SincSamplerOutputTest, ProcessPassthroughUint8) { |
| const std::vector<uint8_t> source_samples = {0x00, 0xFF, 0x27, 0xCD, 0x7F, 0x80, 0xA6, 0x6D}; |
| |
| // Test mono. |
| TestPassthrough<uint8_t>(/*channel_count=*/1, SampleType::kUint8, source_samples); |
| |
| // Test stereo. |
| TestPassthrough<uint8_t>(/*channel_count=*/2, SampleType::kUint8, source_samples); |
| |
| // Test 4 channels. |
| TestPassthrough<uint8_t>(/*channel_count=*/4, SampleType::kUint8, source_samples); |
| } |
| |
| TEST_F(SincSamplerOutputTest, ProcessPassthroughInt16) { |
| const std::vector<int16_t> source_samples = {-0x8000, 0x7FFF, -0x67A7, 0x4D4D, |
| -0x123, 0, 0x2600, -0x2DCB}; |
| |
| // Test mono. |
| TestPassthrough<int16_t>(/*channel_count=*/1, SampleType::kInt16, source_samples); |
| |
| // Test stereo. |
| TestPassthrough<int16_t>(/*channel_count=*/2, SampleType::kInt16, source_samples); |
| |
| // Test 4 channels. |
| TestPassthrough<int16_t>(/*channel_count=*/4, SampleType::kInt16, source_samples); |
| } |
| |
| TEST_F(SincSamplerOutputTest, ProcessPassthroughInt24In32) { |
| const std::vector<int32_t> source_samples = {kMinInt24In32, kMaxInt24In32, -0x67A7E700, |
| 0x4D4D4D00, -0x1234500, 0, |
| 0x26006200, -0x2DCBA900}; |
| |
| // Test mono. |
| TestPassthrough<int32_t>(/*channel_count=*/1, SampleType::kInt32, source_samples); |
| |
| // Test stereo. |
| TestPassthrough<int32_t>(/*channel_count=*/2, SampleType::kInt32, source_samples); |
| |
| // Test 4 channels. |
| TestPassthrough<int32_t>(/*channel_count=*/4, SampleType::kInt32, source_samples); |
| } |
| |
| TEST_F(SincSamplerOutputTest, ProcessPassthroughFloat) { |
| const std::vector<float> source_samples = { |
| -1.0, 1.0f, -0.809783935f, 0.603912353f, -0.00888061523f, 0.0f, 0.296875f, -0.357757568f}; |
| |
| // Test mono. |
| TestPassthrough<float>(/*channel_count=*/1, SampleType::kFloat32, source_samples); |
| |
| // Test stereo. |
| TestPassthrough<float>(/*channel_count=*/2, SampleType::kFloat32, source_samples); |
| |
| // Test 4 channels. |
| TestPassthrough<float>(/*channel_count=*/4, SampleType::kFloat32, source_samples); |
| } |
| |
| TEST_F(SincSamplerOutputTest, ProcessRechannelizationMono) { |
| const std::vector<float> source_samples = {-1.0f, 1.0f, 0.3f}; |
| |
| // Test mono to stereo. |
| TestRechannelization<1, 2>(source_samples, {-1.0f, -1.0f, 1.0f, 1.0f, 0.3f, 0.3f}); |
| |
| // Test mono to 3 channels. |
| TestRechannelization<1, 3>(source_samples, |
| {-1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 0.3f, 0.3f, 0.3f}); |
| |
| // Test mono to quad. |
| TestRechannelization<1, 4>( |
| source_samples, {-1.0f, -1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.3f, 0.3f, 0.3f, 0.3f}); |
| } |
| |
| TEST_F(SincSamplerOutputTest, ProcessRechannelizationStereo) { |
| const std::vector<float> source_samples = {-1.0f, 1.0f, 0.3f, 0.1f}; |
| |
| // Test stereo to mono. |
| TestRechannelization<2, 1>(source_samples, {0.0f, 0.2f}); |
| |
| // Test stereo to 3 channels. |
| TestRechannelization<2, 3>(source_samples, {-1.0f, 1.0f, 0.0f, 0.3f, 0.1f, 0.2f}); |
| |
| // Test stereo to quad. |
| TestRechannelization<2, 4>(source_samples, {-1.0f, 1.0f, -1.0f, 1.0f, 0.3f, 0.1f, 0.3f, 0.1f}); |
| } |
| |
| TEST_F(SincSamplerOutputTest, ProcessRechannelizationQuad) { |
| const std::vector<float> source_samples = {-1.0f, 0.8f, 1.0f, -0.8f, 0.1f, 0.3f, -0.3f, -0.9f}; |
| |
| // Test quad to mono. |
| if constexpr (kEnable4ChannelWorkaround) { |
| TestRechannelization<4, 1>(source_samples, {-0.1f, 0.2f}); |
| } else { |
| TestRechannelization<4, 1>(source_samples, {0.0f, -0.2f}); |
| } |
| |
| // Test quad to stereo. |
| if constexpr (kEnable4ChannelWorkaround) { |
| TestRechannelization<4, 2>(source_samples, {-1.0f, 0.8f, 0.1f, 0.3f}); |
| } else { |
| TestRechannelization<4, 2>(source_samples, {0.0f, 0.0f, -0.1f, -0.3f}); |
| } |
| } |
| |
| } // namespace |
| } // namespace media_audio |