| // Copyright 2018 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 <dlfcn.h> |
| |
| #include <cmath> |
| |
| #include <gmock/gmock.h> |
| #include <gtest/gtest.h> |
| |
| #include "src/lib/fxl/strings/string_printf.h" |
| #include "src/media/audio/examples/effects/delay_effect.h" |
| #include "src/media/audio/examples/effects/effect_base.h" |
| #include "src/media/audio/examples/effects/rechannel_effect.h" |
| #include "src/media/audio/examples/effects/swap_effect.h" |
| #include "src/media/audio/lib/effects_loader/effects_loader_v1.h" |
| #include "src/media/audio/lib/effects_loader/effects_processor_v1.h" |
| |
| namespace media::audio_effects_example { |
| |
| static constexpr const char* kDelayEffectConfig = "{\"delay_frames\": 0}"; |
| |
| // |
| // Tests EffectsLoaderV1, which directly calls the shared library interface. |
| // |
| class EffectsLoaderV1Test : public testing::Test { |
| protected: |
| std::unique_ptr<audio::EffectsLoaderV1> effects_loader_; |
| |
| void SetUp() override { |
| ASSERT_EQ(ZX_OK, audio::EffectsLoaderV1::CreateWithModule("audio_effects_example.so", |
| &effects_loader_)); |
| ASSERT_TRUE(effects_loader_); |
| } |
| }; |
| |
| // |
| // These child classes may not differentiate, but we use different classes for |
| // Delay/Rechannel/Swap in order to group related test results accordingly. |
| // |
| class DelayEffectTest : public EffectsLoaderV1Test { |
| protected: |
| void TestDelayBounds(uint32_t frame_rate, uint32_t channels, uint32_t delay_frames); |
| }; |
| class RechannelEffectTest : public EffectsLoaderV1Test {}; |
| class SwapEffectTest : public EffectsLoaderV1Test {}; |
| |
| // We test the delay effect with certain configuration values, making assumptions |
| // about how those values relate to the allowed range for this effect. |
| constexpr uint32_t kTestDelay1 = 1u; |
| constexpr uint32_t kTestDelay2 = 2u; |
| static_assert(DelayEffect::kMaxDelayFrames >= kTestDelay2, "Test value too high"); |
| static_assert(DelayEffect::kMinDelayFrames <= kTestDelay1, "Test value too low"); |
| |
| // For the most part, the below tests use a specific channel_count. |
| constexpr uint16_t kTestChans = 2; |
| |
| // When testing or using the delay effect, we make certain channel assumptions. |
| static_assert(DelayEffect::kNumChannelsIn == kTestChans || |
| DelayEffect::kNumChannelsIn == FUCHSIA_AUDIO_EFFECTS_CHANNELS_ANY, |
| "DelayEffect::kNumChannelsIn must match kTestChans"); |
| static_assert(DelayEffect::kNumChannelsOut == kTestChans || |
| DelayEffect::kNumChannelsOut == FUCHSIA_AUDIO_EFFECTS_CHANNELS_ANY || |
| DelayEffect::kNumChannelsOut == FUCHSIA_AUDIO_EFFECTS_CHANNELS_SAME_AS_IN, |
| "DelayEffect::kNumChannelsOut must match kTestChans"); |
| |
| // When testing or using rechannel effect, we make certain channel assumptions. |
| static_assert(RechannelEffect::kNumChannelsIn != 2 || RechannelEffect::kNumChannelsOut != 2, |
| "RechannelEffect must not be stereo-in/-out"); |
| static_assert(RechannelEffect::kNumChannelsIn != RechannelEffect::kNumChannelsOut && |
| RechannelEffect::kNumChannelsOut != FUCHSIA_AUDIO_EFFECTS_CHANNELS_ANY && |
| RechannelEffect::kNumChannelsOut != FUCHSIA_AUDIO_EFFECTS_CHANNELS_SAME_AS_IN, |
| "RechannelEffect must not be in-place"); |
| |
| // When testing or using the swap effect, we make certain channel assumptions. |
| static_assert(SwapEffect::kNumChannelsIn == kTestChans || |
| SwapEffect::kNumChannelsIn == FUCHSIA_AUDIO_EFFECTS_CHANNELS_ANY, |
| "SwapEffect::kNumChannelsIn must match kTestChans"); |
| static_assert(SwapEffect::kNumChannelsOut == kTestChans || |
| SwapEffect::kNumChannelsOut == FUCHSIA_AUDIO_EFFECTS_CHANNELS_ANY || |
| SwapEffect::kNumChannelsOut == FUCHSIA_AUDIO_EFFECTS_CHANNELS_SAME_AS_IN, |
| "SwapEffect::kNumChannelsOut must match kTestChans"); |
| |
| // Tests the get_parameters ABI, and that the effect behaves as expected. |
| TEST_F(DelayEffectTest, GetParameters) { |
| fuchsia_audio_effects_parameters effect_params; |
| |
| uint32_t frame_rate = 48000; |
| media::audio::EffectV1 effect = effects_loader_->CreateEffect( |
| Effect::Delay, "", frame_rate, kTestChans, kTestChans, kDelayEffectConfig); |
| ASSERT_TRUE(effect); |
| |
| EXPECT_EQ(effect.GetParameters(&effect_params), ZX_OK); |
| EXPECT_EQ(effect_params.frame_rate, frame_rate); |
| EXPECT_EQ(effect_params.channels_in, kTestChans); |
| EXPECT_EQ(effect_params.channels_out, kTestChans); |
| EXPECT_TRUE(effect_params.signal_latency_frames == DelayEffect::kLatencyFrames); |
| EXPECT_TRUE(effect_params.max_frames_per_buffer == DelayEffect::kLatencyFrames); |
| |
| // Verify null struct* |
| EXPECT_NE(effect.GetParameters(nullptr), ZX_OK); |
| } |
| |
| // Tests the get_parameters ABI, and that the effect behaves as expected. |
| TEST_F(RechannelEffectTest, GetParameters) { |
| fuchsia_audio_effects_parameters effect_params; |
| |
| uint32_t frame_rate = 48000; |
| media::audio::EffectV1 effect = effects_loader_->CreateEffect( |
| Effect::Rechannel, "", frame_rate, RechannelEffect::kNumChannelsIn, |
| RechannelEffect::kNumChannelsOut, {}); |
| ASSERT_TRUE(effect); |
| |
| effect_params.frame_rate = 44100; // should be overwritten |
| EXPECT_EQ(effect.GetParameters(&effect_params), ZX_OK); |
| EXPECT_EQ(effect_params.frame_rate, frame_rate); |
| EXPECT_TRUE(effect_params.channels_in == RechannelEffect::kNumChannelsIn); |
| EXPECT_TRUE(effect_params.channels_out == RechannelEffect::kNumChannelsOut); |
| EXPECT_TRUE(effect_params.signal_latency_frames == RechannelEffect::kLatencyFrames); |
| EXPECT_TRUE(effect_params.max_frames_per_buffer == RechannelEffect::kOutputBufferSizeFrames); |
| } |
| |
| // Tests the get_parameters ABI, and that the effect behaves as expected. |
| TEST_F(SwapEffectTest, GetParameters) { |
| fuchsia_audio_effects_parameters effect_params; |
| |
| uint32_t frame_rate = 44100; |
| media::audio::EffectV1 effect = |
| effects_loader_->CreateEffect(Effect::Swap, "", frame_rate, kTestChans, kTestChans, {}); |
| ASSERT_TRUE(effect); |
| |
| effect_params.frame_rate = 48000; // should be overwritten |
| EXPECT_EQ(effect.GetParameters(&effect_params), ZX_OK); |
| EXPECT_EQ(effect_params.frame_rate, frame_rate); |
| EXPECT_EQ(effect_params.channels_in, kTestChans); |
| EXPECT_EQ(effect_params.channels_out, kTestChans); |
| EXPECT_TRUE(effect_params.signal_latency_frames == SwapEffect::kLatencyFrames); |
| EXPECT_TRUE(effect_params.max_frames_per_buffer == SwapEffect::kLatencyFrames); |
| } |
| |
| TEST_F(SwapEffectTest, UpdateConfiguration) { |
| media::audio::EffectV1 effect = |
| effects_loader_->CreateEffect(Effect::Swap, "", 48000, kTestChans, kTestChans, {}); |
| ASSERT_TRUE(effect); |
| |
| EXPECT_NE(effect.UpdateConfiguration({}), ZX_OK); |
| } |
| |
| TEST_F(RechannelEffectTest, UpdateConfiguration) { |
| media::audio::EffectV1 effect = |
| effects_loader_->CreateEffect(Effect::Rechannel, "", 48000, RechannelEffect::kNumChannelsIn, |
| RechannelEffect::kNumChannelsOut, {}); |
| ASSERT_TRUE(effect); |
| EXPECT_NE(effect.UpdateConfiguration({}), ZX_OK); |
| } |
| |
| TEST_F(DelayEffectTest, UpdateConfiguration) { |
| media::audio::EffectV1 effect = effects_loader_->CreateEffect( |
| Effect::Delay, "", 48000, kTestChans, kTestChans, kDelayEffectConfig); |
| ASSERT_TRUE(effect); |
| |
| // Validate min/max values are accepted. |
| EXPECT_EQ(effect.UpdateConfiguration("{\"delay_frames\": 0}"), ZX_OK); |
| EXPECT_EQ(effect.UpdateConfiguration( |
| fxl::StringPrintf("{\"delay_frames\": %u}", DelayEffect::kMaxDelayFrames)), |
| ZX_OK); |
| |
| // Some invalid configs |
| EXPECT_NE(effect.UpdateConfiguration({}), ZX_OK); |
| EXPECT_NE(effect.UpdateConfiguration("{}"), ZX_OK); |
| EXPECT_NE(effect.UpdateConfiguration("{\"delay_frames\": -1}"), ZX_OK); |
| EXPECT_NE(effect.UpdateConfiguration("{\"delay_frames\": \"foobar\"}"), ZX_OK); |
| EXPECT_NE(effect.UpdateConfiguration("{\"delay_frames\": false}"), ZX_OK); |
| EXPECT_NE(effect.UpdateConfiguration("{\"delay_frames\": {}}"), ZX_OK); |
| EXPECT_NE(effect.UpdateConfiguration("{\"delay_frames\": []}"), ZX_OK); |
| EXPECT_NE(effect.UpdateConfiguration( |
| fxl::StringPrintf("{\"delay_frames\": %u}", DelayEffect::kMaxDelayFrames + 1)), |
| ZX_OK); |
| EXPECT_NE(effect.UpdateConfiguration("[]"), ZX_OK); |
| EXPECT_NE(effect.UpdateConfiguration("This is not JSON"), ZX_OK); |
| EXPECT_NE(effect.UpdateConfiguration("]["), ZX_OK); |
| EXPECT_NE(effect.UpdateConfiguration("{\"delay_frames\": 0"), ZX_OK); |
| } |
| |
| // Tests the process_inplace ABI, and that the effect behaves as expected. |
| TEST_F(DelayEffectTest, ProcessInPlace) { |
| uint32_t num_samples = 12 * kTestChans; |
| uint32_t delay_samples = 6 * kTestChans; |
| float delay_buff_in_out[num_samples]; |
| float expect[num_samples]; |
| |
| for (uint32_t i = 0; i < delay_samples; ++i) { |
| delay_buff_in_out[i] = static_cast<float>(i + 1); |
| expect[i] = 0.0f; |
| } |
| for (uint32_t i = delay_samples; i < num_samples; ++i) { |
| delay_buff_in_out[i] = static_cast<float>(i + 1); |
| expect[i] = delay_buff_in_out[i - delay_samples]; |
| } |
| |
| media::audio::EffectV1 effect = effects_loader_->CreateEffect( |
| Effect::Delay, "", 48000, kTestChans, kTestChans, kDelayEffectConfig); |
| ASSERT_TRUE(effect); |
| |
| ASSERT_EQ(effect.UpdateConfiguration("{\"delay_frames\": 6}"), ZX_OK); |
| EXPECT_EQ(effect.ProcessInPlace(4, delay_buff_in_out), ZX_OK); |
| EXPECT_EQ(effect.ProcessInPlace(4, delay_buff_in_out + (4 * kTestChans)), ZX_OK); |
| EXPECT_EQ(effect.ProcessInPlace(4, delay_buff_in_out + (8 * kTestChans)), ZX_OK); |
| |
| for (uint32_t sample = 0; sample < num_samples; ++sample) { |
| EXPECT_EQ(delay_buff_in_out[sample], expect[sample]) << sample; |
| } |
| EXPECT_EQ(effect.ProcessInPlace(0, delay_buff_in_out), ZX_OK); |
| } |
| |
| // Tests cases in which we expect process_inplace to fail. |
| TEST_F(RechannelEffectTest, ProcessInPlace) { |
| constexpr uint32_t kNumFrames = 1; |
| float buff_in_out[kNumFrames * RechannelEffect::kNumChannelsIn] = {0}; |
| |
| // Effects that change the channelization should not process in-place. |
| media::audio::EffectV1 effect = |
| effects_loader_->CreateEffect(Effect::Rechannel, "", 48000, RechannelEffect::kNumChannelsIn, |
| RechannelEffect::kNumChannelsOut, {}); |
| ASSERT_TRUE(effect); |
| |
| EXPECT_NE(effect.ProcessInPlace(kNumFrames, buff_in_out), ZX_OK); |
| } |
| |
| // Tests the process_inplace ABI, and that the effect behaves as expected. |
| TEST_F(SwapEffectTest, ProcessInPlace) { |
| constexpr uint32_t kNumFrames = 4; |
| float swap_buff_in_out[kNumFrames * kTestChans] = {1.0f, -1.0f, 1.0f, -1.0f, |
| 1.0f, -1.0f, 1.0f, -1.0f}; |
| |
| media::audio::EffectV1 effect = |
| effects_loader_->CreateEffect(Effect::Swap, "", 48000, kTestChans, kTestChans, {}); |
| ASSERT_TRUE(effect); |
| |
| EXPECT_EQ(effect.ProcessInPlace(kNumFrames, swap_buff_in_out), ZX_OK); |
| for (uint32_t sample_num = 0; sample_num < kNumFrames * kTestChans; ++sample_num) { |
| EXPECT_EQ(swap_buff_in_out[sample_num], (sample_num % 2 ? 1.0f : -1.0f)); |
| } |
| |
| EXPECT_EQ(effect.ProcessInPlace(0, swap_buff_in_out), ZX_OK); |
| |
| // Calls with null buff_ptr should fail. |
| EXPECT_NE(effect.ProcessInPlace(kNumFrames, nullptr), ZX_OK); |
| EXPECT_NE(effect.ProcessInPlace(0, nullptr), ZX_OK); |
| } |
| |
| // Tests cases in which we expect process to fail. |
| TEST_F(DelayEffectTest, Process) { |
| constexpr uint32_t kNumFrames = 1; |
| float audio_buff_in[kNumFrames * kTestChans] = {0.0f}; |
| float* audio_buff_out = nullptr; |
| |
| // These stereo-to-stereo effects should ONLY process in-place |
| media::audio::EffectV1 effect = effects_loader_->CreateEffect( |
| Effect::Delay, "", 48000, kTestChans, kTestChans, kDelayEffectConfig); |
| ASSERT_TRUE(effect); |
| EXPECT_NE(effect.Process(kNumFrames, audio_buff_in, &audio_buff_out), ZX_OK); |
| } |
| |
| // Tests the process ABI, and that the effect behaves as expected. |
| TEST_F(RechannelEffectTest, Process) { |
| constexpr uint32_t kNumFrames = 1; |
| float audio_buff_in[kNumFrames * RechannelEffect::kNumChannelsIn] = { |
| 1.0f, -1.0f, 0.25f, -1.0f, 0.98765432f, -0.09876544f}; |
| float* audio_buff_out = nullptr; |
| float expected[kNumFrames * RechannelEffect::kNumChannelsOut] = {0.799536645f, -0.340580851f}; |
| |
| media::audio::EffectV1 effect = |
| effects_loader_->CreateEffect(Effect::Rechannel, "", 48000, RechannelEffect::kNumChannelsIn, |
| RechannelEffect::kNumChannelsOut, {}); |
| ASSERT_TRUE(effect); |
| |
| EXPECT_EQ(effect.Process(kNumFrames, audio_buff_in, &audio_buff_out), ZX_OK); |
| EXPECT_EQ(audio_buff_out[0], expected[0]) << std::setprecision(9) << audio_buff_out[0]; |
| EXPECT_EQ(audio_buff_out[1], expected[1]) << std::setprecision(9) << audio_buff_out[1]; |
| |
| EXPECT_EQ(effect.Process(0, audio_buff_in, &audio_buff_out), ZX_OK); |
| |
| // Test null buffer_in, buffer_out |
| EXPECT_NE(effect.Process(kNumFrames, nullptr, &audio_buff_out), ZX_OK); |
| EXPECT_NE(effect.Process(kNumFrames, audio_buff_in, nullptr), ZX_OK); |
| EXPECT_NE(effect.Process(0, nullptr, &audio_buff_out), ZX_OK); |
| EXPECT_NE(effect.Process(0, audio_buff_in, nullptr), ZX_OK); |
| } |
| |
| // Tests cases in which we expect process to fail. |
| TEST_F(SwapEffectTest, Process) { |
| constexpr uint32_t kNumFrames = 1; |
| float audio_buff_in[kNumFrames * kTestChans] = {0.0f}; |
| float* audio_buff_out = nullptr; |
| |
| // These stereo-to-stereo effects should ONLY process in-place |
| media::audio::EffectV1 effect = |
| effects_loader_->CreateEffect(Effect::Swap, "", 48000, kTestChans, kTestChans, {}); |
| ASSERT_TRUE(effect); |
| EXPECT_NE(effect.Process(kNumFrames, audio_buff_in, &audio_buff_out), ZX_OK); |
| } |
| |
| // Tests the process_inplace ABI thru successive in-place calls. |
| TEST_F(DelayEffectTest, ProcessInPlace_Chain) { |
| constexpr uint32_t kNumFrames = 6; |
| |
| std::vector<float> buff_in_out = {1.0f, -0.1f, -0.2f, 2.0f, 0.3f, -3.0f, |
| -4.0f, 0.4f, 5.0f, -0.5f, -0.6f, 6.0f}; |
| std::vector<float> expected = {0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, |
| -0.1f, 1.0f, 2.0f, -0.2f, -3.0f, 0.3f}; |
| |
| media::audio::EffectV1 delay1 = effects_loader_->CreateEffect( |
| Effect::Delay, "", 44100, kTestChans, kTestChans, kDelayEffectConfig); |
| media::audio::EffectV1 swap = |
| effects_loader_->CreateEffect(Effect::Swap, "", 44100, kTestChans, kTestChans, {}); |
| media::audio::EffectV1 delay2 = effects_loader_->CreateEffect( |
| Effect::Delay, "", 44100, kTestChans, kTestChans, kDelayEffectConfig); |
| |
| ASSERT_TRUE(delay1); |
| ASSERT_TRUE(swap); |
| ASSERT_TRUE(delay2); |
| |
| ASSERT_EQ(delay1.UpdateConfiguration(fxl::StringPrintf("{\"delay_frames\": %u}", kTestDelay1)), |
| ZX_OK); |
| ASSERT_EQ(delay2.UpdateConfiguration(fxl::StringPrintf("{\"delay_frames\": %u}", kTestDelay2)), |
| ZX_OK); |
| |
| EXPECT_EQ(delay1.ProcessInPlace(kNumFrames, buff_in_out.data()), ZX_OK); |
| EXPECT_EQ(swap.ProcessInPlace(kNumFrames, buff_in_out.data()), ZX_OK); |
| EXPECT_EQ(delay2.ProcessInPlace(kNumFrames, buff_in_out.data()), ZX_OK); |
| |
| EXPECT_THAT(buff_in_out, testing::ContainerEq(expected)); |
| |
| EXPECT_EQ(delay2.ProcessInPlace(0, buff_in_out.data()), ZX_OK); |
| EXPECT_EQ(swap.ProcessInPlace(0, buff_in_out.data()), ZX_OK); |
| EXPECT_EQ(delay1.ProcessInPlace(0, buff_in_out.data()), ZX_OK); |
| } |
| |
| // Tests the flush ABI, and effect discards state. |
| TEST_F(DelayEffectTest, Flush) { |
| constexpr uint32_t kNumFrames = 1; |
| float buff_in_out[kTestChans] = {1.0f, -1.0f}; |
| |
| media::audio::EffectV1 effect = |
| effects_loader_->CreateEffect(Effect::Delay, "", 44100, kTestChans, kTestChans, |
| fxl::StringPrintf("{\"delay_frames\": %u}", kTestDelay1)); |
| |
| ASSERT_EQ(effect.ProcessInPlace(kNumFrames, buff_in_out), ZX_OK); |
| ASSERT_EQ(buff_in_out[0], 0.0f); |
| |
| EXPECT_EQ(effect.Flush(), ZX_OK); |
| |
| // Validate that cached samples are flushed. |
| EXPECT_EQ(effect.ProcessInPlace(kNumFrames, buff_in_out), ZX_OK); |
| EXPECT_EQ(buff_in_out[0], 0.0f); |
| } |
| |
| // |
| // We use this subfunction to test the outer limits allowed by ProcessInPlace. |
| void DelayEffectTest::TestDelayBounds(uint32_t frame_rate, uint32_t channels, |
| uint32_t delay_frames) { |
| uint32_t delay_samples = delay_frames * channels; |
| uint32_t num_frames = frame_rate; |
| uint32_t num_samples = num_frames * channels; |
| |
| std::vector<float> delay_buff_in_out(num_samples); |
| std::vector<float> expect(num_samples); |
| |
| media::audio::EffectV1 effect = effects_loader_->CreateEffect( |
| Effect::Delay, "", frame_rate, channels, channels, kDelayEffectConfig); |
| ASSERT_TRUE(effect); |
| |
| ASSERT_EQ(effect.UpdateConfiguration(fxl::StringPrintf("{\"delay_frames\": %u}", delay_frames)), |
| ZX_OK); |
| |
| for (uint32_t pass = 0; pass < 2; ++pass) { |
| for (uint32_t i = 0; i < num_samples; ++i) { |
| delay_buff_in_out[i] = static_cast<float>(i + pass * num_samples + 1); |
| expect[i] = fmax(delay_buff_in_out[i] - static_cast<float>(delay_samples), 0.0f); |
| } |
| ASSERT_EQ(effect.ProcessInPlace(num_frames, delay_buff_in_out.data()), ZX_OK); |
| |
| EXPECT_THAT(delay_buff_in_out, testing::ContainerEq(expect)); |
| } |
| } |
| |
| // Verifies DelayEffect at the outer allowed bounds (largest delays and buffers). |
| TEST_F(DelayEffectTest, ProcessInPlace_Bounds) { |
| TestDelayBounds(192000, 2, DelayEffect::kMaxDelayFrames); |
| TestDelayBounds(2000, FUCHSIA_AUDIO_EFFECTS_CHANNELS_MAX, DelayEffect::kMaxDelayFrames); |
| } |
| |
| } // namespace media::audio_effects_example |