Unit test to validate audio effects

The current test applies audio effect on a multi-tone signal. The
tones are selected according to the filter under test. For equalizer
effect, the center frequencies of all equalizer bands are chosen.
For bassboost effect, a bass frequency and speech tone is selected.
The output signal (effect applied signal) is captured and analysed.
This signal is expected to show the effects of filter characteristics

Bug: 258176224
Test: atest audioeffect_analysis

Change-Id: I848cd79cf1515be721e6e1b4d0240db0c762a91e
diff --git a/media/libaudioclient/TEST_MAPPING b/media/libaudioclient/TEST_MAPPING
index 51080ef..10f9d9b 100644
--- a/media/libaudioclient/TEST_MAPPING
+++ b/media/libaudioclient/TEST_MAPPING
@@ -32,5 +32,10 @@
     {
       "name": "audiosystem_tests"
     }
+  ],
+  "postsubmit": [
+    {
+       "name": "audioeffect_analysis"
+    }
   ]
 }
diff --git a/media/libaudioclient/tests/Android.bp b/media/libaudioclient/tests/Android.bp
index 1b09173..df1506a 100644
--- a/media/libaudioclient/tests/Android.bp
+++ b/media/libaudioclient/tests/Android.bp
@@ -176,6 +176,22 @@
 }
 
 cc_test {
+    name: "audioeffect_analysis",
+    defaults: ["libaudioclient_gtests_defaults"],
+    // flag needed for pfft/pffft.hpp
+    cflags: [
+        "-Wno-error=unused-parameter",
+    ],
+    srcs: [
+        "audioeffect_analyser.cpp",
+        "audio_test_utils.cpp",
+    ],
+    static_libs: [
+        "libpffft",
+    ],
+}
+
+cc_test {
     name: "audiorouting_tests",
     defaults: ["libaudioclient_gtests_defaults"],
     srcs: [
diff --git a/media/libaudioclient/tests/audioeffect_analyser.cpp b/media/libaudioclient/tests/audioeffect_analyser.cpp
new file mode 100644
index 0000000..94accae
--- /dev/null
+++ b/media/libaudioclient/tests/audioeffect_analyser.cpp
@@ -0,0 +1,419 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// #define LOG_NDEBUG 0
+#define LOG_TAG "AudioEffectAnalyser"
+
+#include <android-base/file.h>
+#include <android-base/stringprintf.h>
+#include <gtest/gtest.h>
+#include <media/AudioEffect.h>
+#include <system/audio_effects/effect_bassboost.h>
+#include <system/audio_effects/effect_equalizer.h>
+#include <fstream>
+#include <iostream>
+#include <string>
+#include <tuple>
+#include <vector>
+
+#include "audio_test_utils.h"
+#include "pffft.hpp"
+
+#define CHECK_OK(expr, msg) \
+    mStatus = (expr);       \
+    if (OK != mStatus) {    \
+        mMsg = (msg);       \
+        return;             \
+    }
+
+using namespace android;
+
+constexpr float kDefAmplitude = 0.60f;
+
+constexpr float kPlayBackDurationSec = 1.5;
+constexpr float kCaptureDurationSec = 1.0;
+constexpr float kPrimeDurationInSec = 0.5;
+
+// chosen to safely sample largest center freq of eq bands
+constexpr uint32_t kSamplingFrequency = 48000;
+
+// allows no fmt conversion before fft
+constexpr audio_format_t kFormat = AUDIO_FORMAT_PCM_FLOAT;
+
+// playback and capture are done with channel mask configured to mono.
+// effect analysis should not depend on mask, mono makes it easier.
+
+constexpr int kNPointFFT = 16384;
+constexpr float kBinWidth = (float)kSamplingFrequency / kNPointFFT;
+
+const char* gPackageName = "AudioEffectAnalyser";
+
+static_assert(kPrimeDurationInSec + 2 * kNPointFFT / kSamplingFrequency < kCaptureDurationSec,
+              "capture at least, prime, pad, nPointFft size of samples");
+static_assert(kPrimeDurationInSec + 2 * kNPointFFT / kSamplingFrequency < kPlayBackDurationSec,
+              "playback needs to be active during capture");
+
+struct CaptureEnv {
+    // input args
+    uint32_t mSampleRate{kSamplingFrequency};
+    audio_format_t mFormat{kFormat};
+    audio_channel_mask_t mChannelMask{AUDIO_CHANNEL_IN_MONO};
+    float mCaptureDuration{kCaptureDurationSec};
+    // output val
+    status_t mStatus{OK};
+    std::string mMsg;
+    std::string mDumpFileName;
+
+    ~CaptureEnv();
+    void capture();
+};
+
+CaptureEnv::~CaptureEnv() {
+    if (!mDumpFileName.empty()) {
+        std::ifstream f(mDumpFileName);
+        if (f.good()) {
+            f.close();
+            remove(mDumpFileName.c_str());
+        }
+    }
+}
+
+void CaptureEnv::capture() {
+    audio_port_v7 port;
+    CHECK_OK(getPortByAttributes(AUDIO_PORT_ROLE_SOURCE, AUDIO_PORT_TYPE_DEVICE,
+                                 AUDIO_DEVICE_IN_REMOTE_SUBMIX, "0", port),
+             "Could not find port")
+    const auto capture =
+            sp<AudioCapture>::make(AUDIO_SOURCE_REMOTE_SUBMIX, mSampleRate, mFormat, mChannelMask);
+    CHECK_OK(capture->create(), "record creation failed")
+    CHECK_OK(capture->setRecordDuration(mCaptureDuration), "set record duration failed")
+    CHECK_OK(capture->enableRecordDump(), "enable record dump failed")
+    auto cbCapture = sp<OnAudioDeviceUpdateNotifier>::make();
+    CHECK_OK(capture->getAudioRecordHandle()->addAudioDeviceCallback(cbCapture),
+             "addAudioDeviceCallback failed")
+    CHECK_OK(capture->start(), "start recording failed")
+    CHECK_OK(capture->audioProcess(), "recording process failed")
+    CHECK_OK(cbCapture->waitForAudioDeviceCb(), "audio device callback notification timed out");
+    if (port.id != capture->getAudioRecordHandle()->getRoutedDeviceId()) {
+        CHECK_OK(BAD_VALUE, "Capture NOT routed on expected port")
+    }
+    CHECK_OK(getPortByAttributes(AUDIO_PORT_ROLE_SINK, AUDIO_PORT_TYPE_DEVICE,
+                                 AUDIO_DEVICE_OUT_REMOTE_SUBMIX, "0", port),
+             "Could not find port")
+    CHECK_OK(capture->stop(), "record stop failed")
+    mDumpFileName = capture->getRecordDumpFileName();
+}
+
+struct PlaybackEnv {
+    // input args
+    uint32_t mSampleRate{kSamplingFrequency};
+    audio_format_t mFormat{kFormat};
+    audio_channel_mask_t mChannelMask{AUDIO_CHANNEL_OUT_MONO};
+    audio_session_t mSessionId{AUDIO_SESSION_NONE};
+    std::string mRes;
+    // output val
+    status_t mStatus{OK};
+    std::string mMsg;
+
+    void play();
+};
+
+void PlaybackEnv::play() {
+    const auto ap =
+            sp<AudioPlayback>::make(mSampleRate, mFormat, mChannelMask, AUDIO_OUTPUT_FLAG_NONE,
+                                    mSessionId, AudioTrack::TRANSFER_OBTAIN);
+    CHECK_OK(ap->loadResource(mRes.c_str()), "Unable to open Resource")
+    const auto cbPlayback = sp<OnAudioDeviceUpdateNotifier>::make();
+    CHECK_OK(ap->create(), "track creation failed")
+    ap->getAudioTrackHandle()->setVolume(1.0f);
+    CHECK_OK(ap->getAudioTrackHandle()->addAudioDeviceCallback(cbPlayback),
+             "addAudioDeviceCallback failed")
+    CHECK_OK(ap->start(), "audio track start failed")
+    CHECK_OK(cbPlayback->waitForAudioDeviceCb(), "audio device callback notification timed out")
+    CHECK_OK(ap->onProcess(), "playback process failed")
+    ap->stop();
+}
+
+void generateMultiTone(const std::vector<int>& toneFrequencies, float samplingFrequency,
+                       float duration, float amplitude, float* buffer, int numSamples) {
+    int totalFrameCount = (samplingFrequency * duration);
+    int limit = std::min(totalFrameCount, numSamples);
+
+    for (auto i = 0; i < limit; i++) {
+        buffer[i] = 0;
+        for (auto j = 0; j < toneFrequencies.size(); j++) {
+            buffer[i] += sin(2 * M_PI * toneFrequencies[j] * i / samplingFrequency);
+        }
+        buffer[i] *= (amplitude / toneFrequencies.size());
+    }
+}
+
+sp<AudioEffect> createEffect(const effect_uuid_t* type,
+                             audio_session_t sessionId = AUDIO_SESSION_OUTPUT_MIX) {
+    std::string packageName{gPackageName};
+    AttributionSourceState attributionSource;
+    attributionSource.packageName = packageName;
+    attributionSource.uid = VALUE_OR_FATAL(legacy2aidl_uid_t_int32_t(getuid()));
+    attributionSource.pid = VALUE_OR_FATAL(legacy2aidl_pid_t_int32_t(getpid()));
+    attributionSource.token = sp<BBinder>::make();
+    sp<AudioEffect> effect = sp<AudioEffect>::make(attributionSource);
+    effect->set(type, nullptr, 0, nullptr, sessionId, AUDIO_IO_HANDLE_NONE, {}, false, false);
+    return effect;
+}
+
+void computeFilterGainsAtTones(float captureDuration, int nPointFft, std::vector<int>& binOffsets,
+                               float* inputMag, float* gaindB, const char* res,
+                               audio_session_t sessionId) {
+    int totalFrameCount = captureDuration * kSamplingFrequency;
+    auto output = pffft::AlignedVector<float>(totalFrameCount);
+    auto fftOutput = pffft::AlignedVector<float>(nPointFft);
+    PlaybackEnv argsP;
+    argsP.mRes = std::string{res};
+    argsP.mSessionId = sessionId;
+    CaptureEnv argsR;
+    argsR.mCaptureDuration = captureDuration;
+    std::thread playbackThread(&PlaybackEnv::play, &argsP);
+    std::thread captureThread(&CaptureEnv::capture, &argsR);
+    captureThread.join();
+    playbackThread.join();
+    ASSERT_EQ(OK, argsR.mStatus) << argsR.mMsg;
+    ASSERT_EQ(OK, argsP.mStatus) << argsP.mMsg;
+    ASSERT_FALSE(argsR.mDumpFileName.empty()) << "recorded not written to file";
+    std::ifstream fin(argsR.mDumpFileName, std::ios::in | std::ios::binary);
+    fin.read((char*)output.data(), totalFrameCount * sizeof(output[0]));
+    fin.close();
+    PFFFT_Setup* handle = pffft_new_setup(nPointFft, PFFFT_REAL);
+    // ignore first few samples. This is to not analyse until audio track is re-routed to remote
+    // submix source, also for the effect filter response to reach steady-state (priming / pruning
+    // samples).
+    int rerouteOffset = kPrimeDurationInSec * kSamplingFrequency;
+    pffft_transform_ordered(handle, output.data() + rerouteOffset, fftOutput.data(), nullptr,
+                            PFFFT_FORWARD);
+    pffft_destroy_setup(handle);
+    for (auto i = 0; i < binOffsets.size(); i++) {
+        auto k = binOffsets[i];
+        auto outputMag = sqrt((fftOutput[k * 2] * fftOutput[k * 2]) +
+                              (fftOutput[k * 2 + 1] * fftOutput[k * 2 + 1]));
+        gaindB[i] = 20 * log10(outputMag / inputMag[i]);
+    }
+}
+
+std::tuple<int, int> roundToFreqCenteredToFftBin(float binWidth, float freq) {
+    int bin_index = std::round(freq / binWidth);
+    int cfreq = std::round(bin_index * binWidth);
+    return std::make_tuple(bin_index, cfreq);
+}
+
+TEST(AudioEffectTest, CheckEqualizerEffect) {
+    audio_session_t sessionId =
+            (audio_session_t)AudioSystem::newAudioUniqueId(AUDIO_UNIQUE_ID_USE_SESSION);
+    sp<AudioEffect> equalizer = createEffect(SL_IID_EQUALIZER, sessionId);
+    ASSERT_EQ(OK, equalizer->initCheck());
+    ASSERT_EQ(NO_ERROR, equalizer->setEnabled(true));
+    if ((equalizer->descriptor().flags & EFFECT_FLAG_HW_ACC_MASK) != 0) {
+        GTEST_SKIP() << "effect processed output inaccessible, skipping test";
+    }
+#define MAX_PARAMS 64
+    uint32_t buf32[sizeof(effect_param_t) / sizeof(uint32_t) + MAX_PARAMS];
+    effect_param_t* eqParam = (effect_param_t*)(&buf32);
+
+    // get num of presets
+    eqParam->psize = sizeof(uint32_t);
+    eqParam->vsize = sizeof(uint16_t);
+    *(int32_t*)eqParam->data = EQ_PARAM_GET_NUM_OF_PRESETS;
+    EXPECT_EQ(0, equalizer->getParameter(eqParam));
+    EXPECT_EQ(0, eqParam->status);
+    int numPresets = *((uint16_t*)((int32_t*)eqParam->data + 1));
+
+    // get num of bands
+    eqParam->psize = sizeof(uint32_t);
+    eqParam->vsize = sizeof(uint16_t);
+    *(int32_t*)eqParam->data = EQ_PARAM_NUM_BANDS;
+    EXPECT_EQ(0, equalizer->getParameter(eqParam));
+    EXPECT_EQ(0, eqParam->status);
+    int numBands = *((uint16_t*)((int32_t*)eqParam->data + 1));
+
+    const int totalFrameCount = kSamplingFrequency * kPlayBackDurationSec;
+
+    // get band center frequencies
+    std::vector<int> centerFrequencies;
+    std::vector<int> binOffsets;
+    for (auto i = 0; i < numBands; i++) {
+        eqParam->psize = sizeof(uint32_t) * 2;
+        eqParam->vsize = sizeof(uint32_t);
+        *(int32_t*)eqParam->data = EQ_PARAM_CENTER_FREQ;
+        *((uint16_t*)((int32_t*)eqParam->data + 1)) = i;
+        EXPECT_EQ(0, equalizer->getParameter(eqParam));
+        EXPECT_EQ(0, eqParam->status);
+        float cfreq = *((int32_t*)eqParam->data + 2) / 1000;  // milli hz
+        // pick frequency close to bin center frequency
+        auto [bin_index, bin_freq] = roundToFreqCenteredToFftBin(kBinWidth, cfreq);
+        centerFrequencies.push_back(bin_freq);
+        binOffsets.push_back(bin_index);
+    }
+
+    // input for effect module
+    auto input = pffft::AlignedVector<float>(totalFrameCount);
+    generateMultiTone(centerFrequencies, kSamplingFrequency, kPlayBackDurationSec, kDefAmplitude,
+                      input.data(), totalFrameCount);
+    auto fftInput = pffft::AlignedVector<float>(kNPointFFT);
+    PFFFT_Setup* handle = pffft_new_setup(kNPointFFT, PFFFT_REAL);
+    pffft_transform_ordered(handle, input.data(), fftInput.data(), nullptr, PFFFT_FORWARD);
+    pffft_destroy_setup(handle);
+    float inputMag[numBands];
+    for (auto i = 0; i < numBands; i++) {
+        auto k = binOffsets[i];
+        inputMag[i] = sqrt((fftInput[k * 2] * fftInput[k * 2]) +
+                           (fftInput[k * 2 + 1] * fftInput[k * 2 + 1]));
+    }
+    TemporaryFile tf("/data/local/tmp");
+    close(tf.release());
+    std::ofstream fout(tf.path, std::ios::out | std::ios::binary);
+    fout.write((char*)input.data(), input.size() * sizeof(input[0]));
+    fout.close();
+
+    float expGaindB[numBands], actGaindB[numBands];
+
+    std::string msg = "";
+    int numPresetsOk = 0;
+    for (auto preset = 0; preset < numPresets; preset++) {
+        // set preset
+        eqParam->psize = sizeof(uint32_t);
+        eqParam->vsize = sizeof(uint32_t);
+        *(int32_t*)eqParam->data = EQ_PARAM_CUR_PRESET;
+        *((uint16_t*)((int32_t*)eqParam->data + 1)) = preset;
+        EXPECT_EQ(0, equalizer->setParameter(eqParam));
+        EXPECT_EQ(0, eqParam->status);
+        // get preset gains
+        eqParam->psize = sizeof(uint32_t);
+        eqParam->vsize = (numBands + 1) * sizeof(uint32_t);
+        *(int32_t*)eqParam->data = EQ_PARAM_PROPERTIES;
+        EXPECT_EQ(0, equalizer->getParameter(eqParam));
+        EXPECT_EQ(0, eqParam->status);
+        t_equalizer_settings* settings =
+                reinterpret_cast<t_equalizer_settings*>((int32_t*)eqParam->data + 1);
+        EXPECT_EQ(preset, settings->curPreset);
+        EXPECT_EQ(numBands, settings->numBands);
+        for (auto i = 0; i < numBands; i++) {
+            expGaindB[i] = ((int16_t)settings->bandLevels[i]) / 100.0f;  // gain in milli bels
+        }
+        memset(actGaindB, 0, sizeof(actGaindB));
+        ASSERT_NO_FATAL_FAILURE(computeFilterGainsAtTones(kCaptureDurationSec, kNPointFFT,
+                                                          binOffsets, inputMag, actGaindB, tf.path,
+                                                          sessionId));
+        bool isOk = true;
+        for (auto i = 0; i < numBands - 1; i++) {
+            auto diffA = expGaindB[i] - expGaindB[i + 1];
+            auto diffB = actGaindB[i] - actGaindB[i + 1];
+            if (diffA == 0 && fabs(diffA - diffB) > 1.0f) {
+                msg += (android::base::StringPrintf(
+                        "For eq preset : %d, between bands %d and %d, expected relative gain is : "
+                        "%f, got relative gain is : %f, error : %f \n",
+                        preset, i, i + 1, diffA, diffB, diffA - diffB));
+                isOk = false;
+            } else if (diffA * diffB < 0) {
+                msg += (android::base::StringPrintf(
+                        "For eq preset : %d, between bands %d and %d, expected relative gain and "
+                        "seen relative gain are of opposite signs \n. Expected relative gain is : "
+                        "%f, seen relative gain is : %f \n",
+                        preset, i, i + 1, diffA, diffB));
+                isOk = false;
+            }
+        }
+        if (isOk) numPresetsOk++;
+    }
+    EXPECT_EQ(numPresetsOk, numPresets) << msg;
+}
+
+TEST(AudioEffectTest, CheckBassBoostEffect) {
+    audio_session_t sessionId =
+            (audio_session_t)AudioSystem::newAudioUniqueId(AUDIO_UNIQUE_ID_USE_SESSION);
+    sp<AudioEffect> bassboost = createEffect(SL_IID_BASSBOOST, sessionId);
+    ASSERT_EQ(OK, bassboost->initCheck());
+    ASSERT_EQ(NO_ERROR, bassboost->setEnabled(true));
+    if ((bassboost->descriptor().flags & EFFECT_FLAG_HW_ACC_MASK) != 0) {
+        GTEST_SKIP() << "effect processed output inaccessible, skipping test";
+    }
+    int32_t buf32[sizeof(effect_param_t) / sizeof(int32_t) + MAX_PARAMS];
+    effect_param_t* bbParam = (effect_param_t*)(&buf32);
+
+    bbParam->psize = sizeof(int32_t);
+    bbParam->vsize = sizeof(int32_t);
+    *(int32_t*)bbParam->data = BASSBOOST_PARAM_STRENGTH_SUPPORTED;
+    EXPECT_EQ(0, bassboost->getParameter(bbParam));
+    EXPECT_EQ(0, bbParam->status);
+    bool strengthSupported = *((int32_t*)bbParam->data + 1);
+
+    const int totalFrameCount = kSamplingFrequency * kPlayBackDurationSec;
+
+    // selecting bass frequency, speech tone (for relative gain)
+    std::vector<int> testFrequencies{100, 1200};
+    std::vector<int> binOffsets;
+    for (auto i = 0; i < testFrequencies.size(); i++) {
+        // pick frequency close to bin center frequency
+        auto [bin_index, bin_freq] = roundToFreqCenteredToFftBin(kBinWidth, testFrequencies[i]);
+        testFrequencies[i] = bin_freq;
+        binOffsets.push_back(bin_index);
+    }
+
+    // input for effect module
+    auto input = pffft::AlignedVector<float>(totalFrameCount);
+    generateMultiTone(testFrequencies, kSamplingFrequency, kPlayBackDurationSec, kDefAmplitude,
+                      input.data(), totalFrameCount);
+    auto fftInput = pffft::AlignedVector<float>(kNPointFFT);
+    PFFFT_Setup* handle = pffft_new_setup(kNPointFFT, PFFFT_REAL);
+    pffft_transform_ordered(handle, input.data(), fftInput.data(), nullptr, PFFFT_FORWARD);
+    pffft_destroy_setup(handle);
+    float inputMag[testFrequencies.size()];
+    for (auto i = 0; i < testFrequencies.size(); i++) {
+        auto k = binOffsets[i];
+        inputMag[i] = sqrt((fftInput[k * 2] * fftInput[k * 2]) +
+                           (fftInput[k * 2 + 1] * fftInput[k * 2 + 1]));
+    }
+    TemporaryFile tf("/data/local/tmp");
+    close(tf.release());
+    std::ofstream fout(tf.path, std::ios::out | std::ios::binary);
+    fout.write((char*)input.data(), input.size() * sizeof(input[0]));
+    fout.close();
+
+    float gainWithOutFilter[testFrequencies.size()];
+    memset(gainWithOutFilter, 0, sizeof(gainWithOutFilter));
+    ASSERT_NO_FATAL_FAILURE(computeFilterGainsAtTones(kCaptureDurationSec, kNPointFFT, binOffsets,
+                                                      inputMag, gainWithOutFilter, tf.path,
+                                                      AUDIO_SESSION_OUTPUT_MIX));
+    float diffA = gainWithOutFilter[0] - gainWithOutFilter[1];
+    float prevGain = -100.f;
+    for (auto strength = 150; strength < 1000; strength += strengthSupported ? 150 : 1000) {
+        // configure filter strength
+        if (strengthSupported) {
+            bbParam->psize = sizeof(int32_t);
+            bbParam->vsize = sizeof(int16_t);
+            *(int32_t*)bbParam->data = BASSBOOST_PARAM_STRENGTH;
+            *((int16_t*)((int32_t*)bbParam->data + 1)) = strength;
+            EXPECT_EQ(0, bassboost->setParameter(bbParam));
+            EXPECT_EQ(0, bbParam->status);
+        }
+        float gainWithFilter[testFrequencies.size()];
+        memset(gainWithFilter, 0, sizeof(gainWithFilter));
+        ASSERT_NO_FATAL_FAILURE(computeFilterGainsAtTones(kCaptureDurationSec, kNPointFFT,
+                                                          binOffsets, inputMag, gainWithFilter,
+                                                          tf.path, sessionId));
+        float diffB = gainWithFilter[0] - gainWithFilter[1];
+        EXPECT_GT(diffB, diffA) << "bassboost effect not seen";
+        EXPECT_GE(diffB, prevGain) << "increase in boost strength causing fall in gain";
+        prevGain = diffB;
+    }
+}