blob: f4d37bc133fe5e9ad6f3a22ffd3fcf96f16795af [file] [log] [blame]
Ram Mohan58263ad2022-11-15 21:26:37 +05301/*
2 * Copyright (C) 2022 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Ram Mohan58263ad2022-11-15 21:26:37 +053017#include <fstream>
18#include <iostream>
19#include <string>
20#include <tuple>
21#include <vector>
22
Mikhail Naganovf84a5582024-02-15 11:26:33 -080023// #define LOG_NDEBUG 0
24#define LOG_TAG "AudioEffectAnalyser"
25
26#include <android-base/file.h>
27#include <android-base/stringprintf.h>
28#include <binder/ProcessState.h>
29#include <gtest/gtest.h>
30#include <media/AudioEffect.h>
31#include <system/audio_effects/effect_bassboost.h>
32#include <system/audio_effects/effect_equalizer.h>
33
Ram Mohan58263ad2022-11-15 21:26:37 +053034#include "audio_test_utils.h"
35#include "pffft.hpp"
Mikhail Naganovf84a5582024-02-15 11:26:33 -080036#include "test_execution_tracer.h"
Ram Mohan58263ad2022-11-15 21:26:37 +053037
38#define CHECK_OK(expr, msg) \
39 mStatus = (expr); \
40 if (OK != mStatus) { \
41 mMsg = (msg); \
42 return; \
43 }
44
45using namespace android;
46
47constexpr float kDefAmplitude = 0.60f;
48
49constexpr float kPlayBackDurationSec = 1.5;
50constexpr float kCaptureDurationSec = 1.0;
51constexpr float kPrimeDurationInSec = 0.5;
52
53// chosen to safely sample largest center freq of eq bands
54constexpr uint32_t kSamplingFrequency = 48000;
55
56// allows no fmt conversion before fft
57constexpr audio_format_t kFormat = AUDIO_FORMAT_PCM_FLOAT;
58
59// playback and capture are done with channel mask configured to mono.
60// effect analysis should not depend on mask, mono makes it easier.
61
62constexpr int kNPointFFT = 16384;
63constexpr float kBinWidth = (float)kSamplingFrequency / kNPointFFT;
64
65const char* gPackageName = "AudioEffectAnalyser";
66
67static_assert(kPrimeDurationInSec + 2 * kNPointFFT / kSamplingFrequency < kCaptureDurationSec,
68 "capture at least, prime, pad, nPointFft size of samples");
69static_assert(kPrimeDurationInSec + 2 * kNPointFFT / kSamplingFrequency < kPlayBackDurationSec,
70 "playback needs to be active during capture");
71
72struct CaptureEnv {
73 // input args
74 uint32_t mSampleRate{kSamplingFrequency};
75 audio_format_t mFormat{kFormat};
76 audio_channel_mask_t mChannelMask{AUDIO_CHANNEL_IN_MONO};
77 float mCaptureDuration{kCaptureDurationSec};
78 // output val
79 status_t mStatus{OK};
80 std::string mMsg;
81 std::string mDumpFileName;
82
83 ~CaptureEnv();
84 void capture();
85};
86
87CaptureEnv::~CaptureEnv() {
88 if (!mDumpFileName.empty()) {
89 std::ifstream f(mDumpFileName);
90 if (f.good()) {
91 f.close();
92 remove(mDumpFileName.c_str());
93 }
94 }
95}
96
97void CaptureEnv::capture() {
98 audio_port_v7 port;
99 CHECK_OK(getPortByAttributes(AUDIO_PORT_ROLE_SOURCE, AUDIO_PORT_TYPE_DEVICE,
100 AUDIO_DEVICE_IN_REMOTE_SUBMIX, "0", port),
101 "Could not find port")
102 const auto capture =
103 sp<AudioCapture>::make(AUDIO_SOURCE_REMOTE_SUBMIX, mSampleRate, mFormat, mChannelMask);
104 CHECK_OK(capture->create(), "record creation failed")
105 CHECK_OK(capture->setRecordDuration(mCaptureDuration), "set record duration failed")
106 CHECK_OK(capture->enableRecordDump(), "enable record dump failed")
107 auto cbCapture = sp<OnAudioDeviceUpdateNotifier>::make();
108 CHECK_OK(capture->getAudioRecordHandle()->addAudioDeviceCallback(cbCapture),
109 "addAudioDeviceCallback failed")
110 CHECK_OK(capture->start(), "start recording failed")
111 CHECK_OK(capture->audioProcess(), "recording process failed")
112 CHECK_OK(cbCapture->waitForAudioDeviceCb(), "audio device callback notification timed out");
113 if (port.id != capture->getAudioRecordHandle()->getRoutedDeviceId()) {
114 CHECK_OK(BAD_VALUE, "Capture NOT routed on expected port")
115 }
116 CHECK_OK(getPortByAttributes(AUDIO_PORT_ROLE_SINK, AUDIO_PORT_TYPE_DEVICE,
117 AUDIO_DEVICE_OUT_REMOTE_SUBMIX, "0", port),
118 "Could not find port")
119 CHECK_OK(capture->stop(), "record stop failed")
120 mDumpFileName = capture->getRecordDumpFileName();
121}
122
123struct PlaybackEnv {
124 // input args
125 uint32_t mSampleRate{kSamplingFrequency};
126 audio_format_t mFormat{kFormat};
127 audio_channel_mask_t mChannelMask{AUDIO_CHANNEL_OUT_MONO};
128 audio_session_t mSessionId{AUDIO_SESSION_NONE};
129 std::string mRes;
130 // output val
131 status_t mStatus{OK};
132 std::string mMsg;
133
134 void play();
135};
136
137void PlaybackEnv::play() {
138 const auto ap =
139 sp<AudioPlayback>::make(mSampleRate, mFormat, mChannelMask, AUDIO_OUTPUT_FLAG_NONE,
140 mSessionId, AudioTrack::TRANSFER_OBTAIN);
141 CHECK_OK(ap->loadResource(mRes.c_str()), "Unable to open Resource")
142 const auto cbPlayback = sp<OnAudioDeviceUpdateNotifier>::make();
143 CHECK_OK(ap->create(), "track creation failed")
144 ap->getAudioTrackHandle()->setVolume(1.0f);
145 CHECK_OK(ap->getAudioTrackHandle()->addAudioDeviceCallback(cbPlayback),
146 "addAudioDeviceCallback failed")
147 CHECK_OK(ap->start(), "audio track start failed")
148 CHECK_OK(cbPlayback->waitForAudioDeviceCb(), "audio device callback notification timed out")
149 CHECK_OK(ap->onProcess(), "playback process failed")
150 ap->stop();
151}
152
153void generateMultiTone(const std::vector<int>& toneFrequencies, float samplingFrequency,
154 float duration, float amplitude, float* buffer, int numSamples) {
155 int totalFrameCount = (samplingFrequency * duration);
156 int limit = std::min(totalFrameCount, numSamples);
157
158 for (auto i = 0; i < limit; i++) {
159 buffer[i] = 0;
160 for (auto j = 0; j < toneFrequencies.size(); j++) {
161 buffer[i] += sin(2 * M_PI * toneFrequencies[j] * i / samplingFrequency);
162 }
163 buffer[i] *= (amplitude / toneFrequencies.size());
164 }
165}
166
167sp<AudioEffect> createEffect(const effect_uuid_t* type,
168 audio_session_t sessionId = AUDIO_SESSION_OUTPUT_MIX) {
169 std::string packageName{gPackageName};
170 AttributionSourceState attributionSource;
171 attributionSource.packageName = packageName;
172 attributionSource.uid = VALUE_OR_FATAL(legacy2aidl_uid_t_int32_t(getuid()));
173 attributionSource.pid = VALUE_OR_FATAL(legacy2aidl_pid_t_int32_t(getpid()));
174 attributionSource.token = sp<BBinder>::make();
175 sp<AudioEffect> effect = sp<AudioEffect>::make(attributionSource);
176 effect->set(type, nullptr, 0, nullptr, sessionId, AUDIO_IO_HANDLE_NONE, {}, false, false);
177 return effect;
178}
179
180void computeFilterGainsAtTones(float captureDuration, int nPointFft, std::vector<int>& binOffsets,
181 float* inputMag, float* gaindB, const char* res,
182 audio_session_t sessionId) {
183 int totalFrameCount = captureDuration * kSamplingFrequency;
184 auto output = pffft::AlignedVector<float>(totalFrameCount);
185 auto fftOutput = pffft::AlignedVector<float>(nPointFft);
186 PlaybackEnv argsP;
187 argsP.mRes = std::string{res};
188 argsP.mSessionId = sessionId;
189 CaptureEnv argsR;
190 argsR.mCaptureDuration = captureDuration;
191 std::thread playbackThread(&PlaybackEnv::play, &argsP);
192 std::thread captureThread(&CaptureEnv::capture, &argsR);
193 captureThread.join();
194 playbackThread.join();
195 ASSERT_EQ(OK, argsR.mStatus) << argsR.mMsg;
196 ASSERT_EQ(OK, argsP.mStatus) << argsP.mMsg;
197 ASSERT_FALSE(argsR.mDumpFileName.empty()) << "recorded not written to file";
198 std::ifstream fin(argsR.mDumpFileName, std::ios::in | std::ios::binary);
199 fin.read((char*)output.data(), totalFrameCount * sizeof(output[0]));
200 fin.close();
201 PFFFT_Setup* handle = pffft_new_setup(nPointFft, PFFFT_REAL);
202 // ignore first few samples. This is to not analyse until audio track is re-routed to remote
203 // submix source, also for the effect filter response to reach steady-state (priming / pruning
204 // samples).
205 int rerouteOffset = kPrimeDurationInSec * kSamplingFrequency;
206 pffft_transform_ordered(handle, output.data() + rerouteOffset, fftOutput.data(), nullptr,
207 PFFFT_FORWARD);
208 pffft_destroy_setup(handle);
209 for (auto i = 0; i < binOffsets.size(); i++) {
210 auto k = binOffsets[i];
211 auto outputMag = sqrt((fftOutput[k * 2] * fftOutput[k * 2]) +
212 (fftOutput[k * 2 + 1] * fftOutput[k * 2 + 1]));
213 gaindB[i] = 20 * log10(outputMag / inputMag[i]);
214 }
215}
216
217std::tuple<int, int> roundToFreqCenteredToFftBin(float binWidth, float freq) {
218 int bin_index = std::round(freq / binWidth);
219 int cfreq = std::round(bin_index * binWidth);
220 return std::make_tuple(bin_index, cfreq);
221}
222
223TEST(AudioEffectTest, CheckEqualizerEffect) {
224 audio_session_t sessionId =
225 (audio_session_t)AudioSystem::newAudioUniqueId(AUDIO_UNIQUE_ID_USE_SESSION);
226 sp<AudioEffect> equalizer = createEffect(SL_IID_EQUALIZER, sessionId);
227 ASSERT_EQ(OK, equalizer->initCheck());
228 ASSERT_EQ(NO_ERROR, equalizer->setEnabled(true));
229 if ((equalizer->descriptor().flags & EFFECT_FLAG_HW_ACC_MASK) != 0) {
230 GTEST_SKIP() << "effect processed output inaccessible, skipping test";
231 }
232#define MAX_PARAMS 64
233 uint32_t buf32[sizeof(effect_param_t) / sizeof(uint32_t) + MAX_PARAMS];
234 effect_param_t* eqParam = (effect_param_t*)(&buf32);
235
236 // get num of presets
237 eqParam->psize = sizeof(uint32_t);
238 eqParam->vsize = sizeof(uint16_t);
239 *(int32_t*)eqParam->data = EQ_PARAM_GET_NUM_OF_PRESETS;
240 EXPECT_EQ(0, equalizer->getParameter(eqParam));
241 EXPECT_EQ(0, eqParam->status);
242 int numPresets = *((uint16_t*)((int32_t*)eqParam->data + 1));
243
244 // get num of bands
245 eqParam->psize = sizeof(uint32_t);
246 eqParam->vsize = sizeof(uint16_t);
247 *(int32_t*)eqParam->data = EQ_PARAM_NUM_BANDS;
248 EXPECT_EQ(0, equalizer->getParameter(eqParam));
249 EXPECT_EQ(0, eqParam->status);
250 int numBands = *((uint16_t*)((int32_t*)eqParam->data + 1));
251
252 const int totalFrameCount = kSamplingFrequency * kPlayBackDurationSec;
253
254 // get band center frequencies
255 std::vector<int> centerFrequencies;
256 std::vector<int> binOffsets;
257 for (auto i = 0; i < numBands; i++) {
258 eqParam->psize = sizeof(uint32_t) * 2;
259 eqParam->vsize = sizeof(uint32_t);
260 *(int32_t*)eqParam->data = EQ_PARAM_CENTER_FREQ;
261 *((uint16_t*)((int32_t*)eqParam->data + 1)) = i;
262 EXPECT_EQ(0, equalizer->getParameter(eqParam));
263 EXPECT_EQ(0, eqParam->status);
264 float cfreq = *((int32_t*)eqParam->data + 2) / 1000; // milli hz
265 // pick frequency close to bin center frequency
266 auto [bin_index, bin_freq] = roundToFreqCenteredToFftBin(kBinWidth, cfreq);
267 centerFrequencies.push_back(bin_freq);
268 binOffsets.push_back(bin_index);
269 }
270
271 // input for effect module
272 auto input = pffft::AlignedVector<float>(totalFrameCount);
273 generateMultiTone(centerFrequencies, kSamplingFrequency, kPlayBackDurationSec, kDefAmplitude,
274 input.data(), totalFrameCount);
275 auto fftInput = pffft::AlignedVector<float>(kNPointFFT);
276 PFFFT_Setup* handle = pffft_new_setup(kNPointFFT, PFFFT_REAL);
277 pffft_transform_ordered(handle, input.data(), fftInput.data(), nullptr, PFFFT_FORWARD);
278 pffft_destroy_setup(handle);
279 float inputMag[numBands];
280 for (auto i = 0; i < numBands; i++) {
281 auto k = binOffsets[i];
282 inputMag[i] = sqrt((fftInput[k * 2] * fftInput[k * 2]) +
283 (fftInput[k * 2 + 1] * fftInput[k * 2 + 1]));
284 }
285 TemporaryFile tf("/data/local/tmp");
286 close(tf.release());
287 std::ofstream fout(tf.path, std::ios::out | std::ios::binary);
288 fout.write((char*)input.data(), input.size() * sizeof(input[0]));
289 fout.close();
290
291 float expGaindB[numBands], actGaindB[numBands];
292
293 std::string msg = "";
294 int numPresetsOk = 0;
295 for (auto preset = 0; preset < numPresets; preset++) {
296 // set preset
297 eqParam->psize = sizeof(uint32_t);
298 eqParam->vsize = sizeof(uint32_t);
299 *(int32_t*)eqParam->data = EQ_PARAM_CUR_PRESET;
300 *((uint16_t*)((int32_t*)eqParam->data + 1)) = preset;
301 EXPECT_EQ(0, equalizer->setParameter(eqParam));
302 EXPECT_EQ(0, eqParam->status);
303 // get preset gains
304 eqParam->psize = sizeof(uint32_t);
305 eqParam->vsize = (numBands + 1) * sizeof(uint32_t);
306 *(int32_t*)eqParam->data = EQ_PARAM_PROPERTIES;
307 EXPECT_EQ(0, equalizer->getParameter(eqParam));
308 EXPECT_EQ(0, eqParam->status);
309 t_equalizer_settings* settings =
310 reinterpret_cast<t_equalizer_settings*>((int32_t*)eqParam->data + 1);
311 EXPECT_EQ(preset, settings->curPreset);
312 EXPECT_EQ(numBands, settings->numBands);
313 for (auto i = 0; i < numBands; i++) {
314 expGaindB[i] = ((int16_t)settings->bandLevels[i]) / 100.0f; // gain in milli bels
315 }
316 memset(actGaindB, 0, sizeof(actGaindB));
317 ASSERT_NO_FATAL_FAILURE(computeFilterGainsAtTones(kCaptureDurationSec, kNPointFFT,
318 binOffsets, inputMag, actGaindB, tf.path,
319 sessionId));
320 bool isOk = true;
321 for (auto i = 0; i < numBands - 1; i++) {
322 auto diffA = expGaindB[i] - expGaindB[i + 1];
323 auto diffB = actGaindB[i] - actGaindB[i + 1];
324 if (diffA == 0 && fabs(diffA - diffB) > 1.0f) {
325 msg += (android::base::StringPrintf(
326 "For eq preset : %d, between bands %d and %d, expected relative gain is : "
327 "%f, got relative gain is : %f, error : %f \n",
328 preset, i, i + 1, diffA, diffB, diffA - diffB));
329 isOk = false;
330 } else if (diffA * diffB < 0) {
331 msg += (android::base::StringPrintf(
332 "For eq preset : %d, between bands %d and %d, expected relative gain and "
333 "seen relative gain are of opposite signs \n. Expected relative gain is : "
334 "%f, seen relative gain is : %f \n",
335 preset, i, i + 1, diffA, diffB));
336 isOk = false;
337 }
338 }
339 if (isOk) numPresetsOk++;
340 }
341 EXPECT_EQ(numPresetsOk, numPresets) << msg;
342}
343
344TEST(AudioEffectTest, CheckBassBoostEffect) {
345 audio_session_t sessionId =
346 (audio_session_t)AudioSystem::newAudioUniqueId(AUDIO_UNIQUE_ID_USE_SESSION);
347 sp<AudioEffect> bassboost = createEffect(SL_IID_BASSBOOST, sessionId);
348 ASSERT_EQ(OK, bassboost->initCheck());
349 ASSERT_EQ(NO_ERROR, bassboost->setEnabled(true));
350 if ((bassboost->descriptor().flags & EFFECT_FLAG_HW_ACC_MASK) != 0) {
351 GTEST_SKIP() << "effect processed output inaccessible, skipping test";
352 }
353 int32_t buf32[sizeof(effect_param_t) / sizeof(int32_t) + MAX_PARAMS];
354 effect_param_t* bbParam = (effect_param_t*)(&buf32);
355
356 bbParam->psize = sizeof(int32_t);
357 bbParam->vsize = sizeof(int32_t);
358 *(int32_t*)bbParam->data = BASSBOOST_PARAM_STRENGTH_SUPPORTED;
359 EXPECT_EQ(0, bassboost->getParameter(bbParam));
360 EXPECT_EQ(0, bbParam->status);
361 bool strengthSupported = *((int32_t*)bbParam->data + 1);
362
363 const int totalFrameCount = kSamplingFrequency * kPlayBackDurationSec;
364
365 // selecting bass frequency, speech tone (for relative gain)
366 std::vector<int> testFrequencies{100, 1200};
367 std::vector<int> binOffsets;
368 for (auto i = 0; i < testFrequencies.size(); i++) {
369 // pick frequency close to bin center frequency
370 auto [bin_index, bin_freq] = roundToFreqCenteredToFftBin(kBinWidth, testFrequencies[i]);
371 testFrequencies[i] = bin_freq;
372 binOffsets.push_back(bin_index);
373 }
374
375 // input for effect module
376 auto input = pffft::AlignedVector<float>(totalFrameCount);
377 generateMultiTone(testFrequencies, kSamplingFrequency, kPlayBackDurationSec, kDefAmplitude,
378 input.data(), totalFrameCount);
379 auto fftInput = pffft::AlignedVector<float>(kNPointFFT);
380 PFFFT_Setup* handle = pffft_new_setup(kNPointFFT, PFFFT_REAL);
381 pffft_transform_ordered(handle, input.data(), fftInput.data(), nullptr, PFFFT_FORWARD);
382 pffft_destroy_setup(handle);
383 float inputMag[testFrequencies.size()];
384 for (auto i = 0; i < testFrequencies.size(); i++) {
385 auto k = binOffsets[i];
386 inputMag[i] = sqrt((fftInput[k * 2] * fftInput[k * 2]) +
387 (fftInput[k * 2 + 1] * fftInput[k * 2 + 1]));
388 }
389 TemporaryFile tf("/data/local/tmp");
390 close(tf.release());
391 std::ofstream fout(tf.path, std::ios::out | std::ios::binary);
392 fout.write((char*)input.data(), input.size() * sizeof(input[0]));
393 fout.close();
394
395 float gainWithOutFilter[testFrequencies.size()];
396 memset(gainWithOutFilter, 0, sizeof(gainWithOutFilter));
397 ASSERT_NO_FATAL_FAILURE(computeFilterGainsAtTones(kCaptureDurationSec, kNPointFFT, binOffsets,
398 inputMag, gainWithOutFilter, tf.path,
399 AUDIO_SESSION_OUTPUT_MIX));
400 float diffA = gainWithOutFilter[0] - gainWithOutFilter[1];
401 float prevGain = -100.f;
402 for (auto strength = 150; strength < 1000; strength += strengthSupported ? 150 : 1000) {
403 // configure filter strength
404 if (strengthSupported) {
405 bbParam->psize = sizeof(int32_t);
406 bbParam->vsize = sizeof(int16_t);
407 *(int32_t*)bbParam->data = BASSBOOST_PARAM_STRENGTH;
408 *((int16_t*)((int32_t*)bbParam->data + 1)) = strength;
409 EXPECT_EQ(0, bassboost->setParameter(bbParam));
410 EXPECT_EQ(0, bbParam->status);
411 }
412 float gainWithFilter[testFrequencies.size()];
413 memset(gainWithFilter, 0, sizeof(gainWithFilter));
414 ASSERT_NO_FATAL_FAILURE(computeFilterGainsAtTones(kCaptureDurationSec, kNPointFFT,
415 binOffsets, inputMag, gainWithFilter,
416 tf.path, sessionId));
417 float diffB = gainWithFilter[0] - gainWithFilter[1];
418 EXPECT_GT(diffB, diffA) << "bassboost effect not seen";
419 EXPECT_GE(diffB, prevGain) << "increase in boost strength causing fall in gain";
420 prevGain = diffB;
421 }
422}
Mikhail Naganovf84a5582024-02-15 11:26:33 -0800423
424int main(int argc, char** argv) {
425 android::ProcessState::self()->startThreadPool();
426 ::testing::InitGoogleTest(&argc, argv);
427 ::testing::UnitTest::GetInstance()->listeners().Append(new TestExecutionTracer());
428 return RUN_ALL_TESTS();
429}