Skip to content

Commit 42a5a3f

Browse files
Merge pull request #2378 from google/hieuhh/frequency_dual_test
Porting the frequency tests from CtsVerifier to OboeTester
2 parents afffcce + 24aad78 commit 42a5a3f

29 files changed

Lines changed: 3157 additions & 18 deletions

apps/OboeTester/app/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3")
66

77
link_directories(${CMAKE_CURRENT_LIST_DIR}/..)
88

9-
# Increment this number when adding files to OboeTester => 112
9+
# Increment this number when adding files to OboeTester => 113
1010
# The change in this file will help Android Studio resync
1111
# and generate new build files that reference the new code.
1212
file(GLOB_RECURSE app_native_sources src/main/cpp/*)

apps/OboeTester/app/src/main/AndroidManifest.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,14 @@
135135
android:label="@string/title_rapid_cycle"
136136
android:exported="true"
137137
android:screenOrientation="portrait" />
138+
<activity android:name=".FrequencyActivity"
139+
android:label="Frequency Test"
140+
android:exported="true"
141+
android:screenOrientation="portrait"/>
142+
<activity android:name=".DualFrequencyActivity"
143+
android:label="Dual Frequency Test"
144+
android:exported="true"
145+
android:screenOrientation="portrait"/>
138146
<activity
139147
android:name=".AudioWorkloadTestActivity"
140148
android:label="@string/title_audio_workload_test"

apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -933,6 +933,29 @@ void ActivityDataPath::finishOpen(bool isInput, std::shared_ptr<oboe::AudioStrea
933933
}
934934
}
935935

936+
// ======================================================================= ActivityFrequency
937+
void ActivityFrequency::configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) {
938+
ActivityFullDuplex::configureBuilder(isInput, builder);
939+
940+
if (mFullDuplexAnalyzer.get() == nullptr) {
941+
mFullDuplexAnalyzer = std::make_unique<FullDuplexAnalyzer>(&mFrequencyAnalyzer);
942+
}
943+
if (!isInput) {
944+
// Output uses a callback, input is polled by the analyzer.
945+
builder.setCallback(oboeCallbackProxy.get());
946+
oboeCallbackProxy->setDataCallback(mFullDuplexAnalyzer.get());
947+
}
948+
}
949+
950+
void ActivityFrequency::finishOpen(bool isInput, std::shared_ptr<oboe::AudioStream> &oboeStream) {
951+
if (isInput) {
952+
mFullDuplexAnalyzer->setSharedInputStream(oboeStream);
953+
mFullDuplexAnalyzer->setRecording(mRecording.get());
954+
} else {
955+
mFullDuplexAnalyzer->setSharedOutputStream(oboeStream);
956+
}
957+
}
958+
936959
// =================================================================== ActivityTestDisconnect
937960
ActivityTestDisconnect::ActivityTestDisconnect() {
938961
mRoutingCallback = std::make_shared<TestRoutingCallback>(this);

apps/OboeTester/app/src/main/cpp/NativeAudioContext.h

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
#include "FullDuplexEcho.h"
5454
#include "analyzer/GlitchAnalyzer.h"
5555
#include "analyzer/DataPathAnalyzer.h"
56+
#include "analyzer/FrequencyAnalyzer.h"
5657
#include "InputStreamCallbackAnalyzer.h"
5758
#include "MultiChannelRecording.h"
5859
#include "NoisePulseGenerator.h"
@@ -308,6 +309,8 @@ class ActivityContext {
308309

309310
virtual void setSignalType(int signalType) {}
310311

312+
virtual void setBalance(float balance) {}
313+
311314
virtual void setAmplitude(float amplitude) {}
312315

313316
virtual void setDuck(bool isDucked) {}
@@ -867,6 +870,47 @@ class ActivityTestDisconnect : public ActivityContext {
867870
std::shared_ptr<oboe::flowgraph::SinkFloat> mSinkFloat;
868871
};
869872

873+
/**
874+
* Test frequency
875+
*/
876+
class ActivityFrequency : public ActivityFullDuplex {
877+
public:
878+
ActivityFrequency() {
879+
mFullDuplexAnalyzer = std::make_unique<FullDuplexAnalyzer>(&mFrequencyAnalyzer);
880+
}
881+
virtual ~ActivityFrequency() = default;
882+
883+
oboe::Result startStreams() override {
884+
mFrequencyAnalyzer.reset();
885+
return mFullDuplexAnalyzer->start();
886+
}
887+
888+
void configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) override;
889+
890+
void setSignalType(int signalType) override {
891+
mFrequencyAnalyzer.setSignalType(signalType);
892+
}
893+
894+
void setBalance(float balance) override {
895+
mFrequencyAnalyzer.setBalance(balance);
896+
}
897+
898+
FrequencyAnalyzer *getFrequencyAnalyzer() {
899+
return &mFrequencyAnalyzer;
900+
}
901+
902+
FullDuplexAnalyzer *getFullDuplexAnalyzer() override {
903+
return mFullDuplexAnalyzer.get();
904+
}
905+
906+
protected:
907+
void finishOpen(bool isInput, std::shared_ptr<oboe::AudioStream> &oboeStream) override;
908+
909+
private:
910+
std::unique_ptr<FullDuplexAnalyzer> mFullDuplexAnalyzer;
911+
FrequencyAnalyzer mFrequencyAnalyzer;
912+
};
913+
870914
/**
871915
* Global context for native tests.
872916
* Switch between various ActivityContexts.
@@ -910,6 +954,12 @@ class NativeAudioContext {
910954
case ActivityType::DataPath:
911955
currentActivity = &mActivityDataPath;
912956
break;
957+
case ActivityType::Frequency:
958+
currentActivity = &mActivityFrequency;
959+
break;
960+
case ActivityType::DualFrequency:
961+
currentActivity = &mActivityFrequency;
962+
break;
913963
}
914964
}
915965

@@ -926,6 +976,7 @@ class NativeAudioContext {
926976
ActivityGlitches mActivityGlitches;
927977
ActivityDataPath mActivityDataPath;
928978
ActivityTestDisconnect mActivityTestDisconnect;
979+
ActivityFrequency mActivityFrequency;
929980

930981
private:
931982

@@ -941,6 +992,8 @@ class NativeAudioContext {
941992
Glitches = 6,
942993
TestDisconnect = 7,
943994
DataPath = 8,
995+
Frequency = 10,
996+
DualFrequency = 11,
944997
};
945998

946999
ActivityType mActivityType = ActivityType::Undefined;
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright (C) 2026 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+
17+
#ifndef OBOETESTER_AVERAGEBUFFER_H
18+
#define OBOETESTER_AVERAGEBUFFER_H
19+
20+
#include <vector>
21+
#include <algorithm>
22+
23+
class AverageBuffer {
24+
public:
25+
AverageBuffer() = default;
26+
27+
void resize(size_t size) {
28+
mAccumulator.resize(size);
29+
clear();
30+
}
31+
32+
void clear() {
33+
std::fill(mAccumulator.begin(), mAccumulator.end(), 0.0);
34+
mCount = 0;
35+
}
36+
37+
void accumulate(const double* data, size_t size) {
38+
if (size > mAccumulator.size()) {
39+
mAccumulator.resize(size, 0.0);
40+
}
41+
for (size_t i = 0; i < size; ++i) {
42+
mAccumulator[i] += data[i];
43+
}
44+
mCount++;
45+
}
46+
47+
int getCount() const {
48+
return mCount;
49+
}
50+
51+
size_t size() const {
52+
return mAccumulator.size();
53+
}
54+
55+
double getAverageAt(size_t index) const {
56+
if (mCount > 0 && index < mAccumulator.size()) {
57+
return mAccumulator[index] / mCount;
58+
}
59+
return 0.0;
60+
}
61+
62+
private:
63+
std::vector<double> mAccumulator;
64+
int mCount = 0;
65+
};
66+
67+
#endif //OBOETESTER_AVERAGEBUFFER_H
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/*
2+
* Copyright (C) 2026 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+
17+
#include "FrequencyAnalyzer.h"
18+
#include <math.h>
19+
#include "fft.h"
20+
21+
FrequencyAnalyzer::FrequencyAnalyzer() : LoopbackProcessor() {
22+
mWindow.resize(WINDOW_SIZE);
23+
mIncoherentPower = 0.0;
24+
mWindowSum = 0.0;
25+
for (int i = 0; i < WINDOW_SIZE; i++) {
26+
mWindow[i] = 0.5 * (1.0 - cos(2.0 * M_PI * i / (WINDOW_SIZE - 1)));
27+
mIncoherentPower += mWindow[i] * mWindow[i];
28+
mWindowSum += mWindow[i];
29+
}
30+
mInputBuffer.resize(WINDOW_SIZE);
31+
mAverageBuffer.resize(WINDOW_SIZE / 2);
32+
33+
mInputBufferIndex = 0;
34+
mFramesAccumulated = 0;
35+
mOutputPhase = 0.0f;
36+
37+
std::lock_guard<std::mutex> lock(mFftBufferLock);
38+
mFftMagnitudeBuffer.clear();
39+
}
40+
41+
void FrequencyAnalyzer::reset() {
42+
LoopbackProcessor::reset();
43+
mInputBufferIndex = 0;
44+
mOutputPhase = 0.0f;
45+
46+
mAverageBuffer.clear();
47+
mFramesAccumulated = 0;
48+
49+
std::lock_guard<std::mutex> lock(mFftBufferLock);
50+
mFftMagnitudeBuffer.clear();
51+
}
52+
53+
void FrequencyAnalyzer::prepareToTest() {
54+
LoopbackProcessor::prepareToTest();
55+
mPhaseIncrement = 2.0 * M_PI * SINE_WAVE_FREQUENCY / getSampleRate();
56+
mMeasurementWindowFrames = MEASUREMENT_TIME_SEC * getSampleRate();
57+
}
58+
59+
LoopbackProcessor::result_code FrequencyAnalyzer::processInputFrame(const float* frameData,
60+
int channelCount) {
61+
float sample = frameData[getInputChannel()];
62+
mInputBuffer[mInputBufferIndex++] = sample;
63+
if (mInputBufferIndex >= WINDOW_SIZE) {
64+
// Perform FFT
65+
std::lock_guard<std::mutex> lock(mFftBufferLock);
66+
CVector fftInput(WINDOW_SIZE);
67+
for (int i = 0; i < WINDOW_SIZE; i++) {
68+
double windowed = mInputBuffer[i] * mWindow[i];
69+
fftInput[i] = Complex(windowed, 0);
70+
}
71+
fft(fftInput);
72+
73+
// Accumulate magnitude
74+
std::vector<double> currentMagnitudes(WINDOW_SIZE / 2);
75+
for (int i = 0; i < WINDOW_SIZE / 2; i++) {
76+
double mag;
77+
if (mSignalType == 1) { // Sine
78+
mag = 2.0 * std::abs(fftInput[i]) / mWindowSum;
79+
} else {
80+
mag = 4.0 * std::abs(fftInput[i]) / std::sqrt(mIncoherentPower);
81+
}
82+
if (mag < 1e-9) mag = 1e-9; // to prevent log(0)
83+
currentMagnitudes[i] = mag;
84+
}
85+
mAverageBuffer.accumulate(currentMagnitudes.data(),
86+
currentMagnitudes.size());
87+
mInputBufferIndex = 0;
88+
}
89+
90+
mFramesAccumulated++;
91+
if (mFramesAccumulated >= mMeasurementWindowFrames) {
92+
// End of measurement window! Compute the average and save it to mFftMagnitudeBuffer
93+
std::lock_guard<std::mutex> lock(mFftBufferLock);
94+
if (mFftMagnitudeBuffer.empty()) {
95+
// First measurement window
96+
mFftMagnitudeBuffer.resize(WINDOW_SIZE / 2);
97+
}
98+
for (int i = 0; i < WINDOW_SIZE / 2; i++) {
99+
double avgMag = mAverageBuffer.getAverageAt(i);
100+
double dbfs = 20.0 * std::log10(avgMag);
101+
mFftMagnitudeBuffer[i] = static_cast<float>(dbfs);
102+
}
103+
mAverageBuffer.clear();
104+
mFramesAccumulated = 0;
105+
}
106+
107+
return RESULT_OK;
108+
}
109+
110+
void FrequencyAnalyzer::setSignalType(int signalType) {
111+
mSignalType = signalType;
112+
}
113+
114+
int FrequencyAnalyzer::getWindowSize() {
115+
return WINDOW_SIZE;
116+
}
117+
118+
LoopbackProcessor::result_code FrequencyAnalyzer::processOutputFrame(float* frameData,
119+
int channelCount) {
120+
float output = 0.0f;
121+
if (mSignalType == 0) { // White noise
122+
output = mWhiteNoise.nextRandomDouble() * mAmplitude;
123+
} else if (mSignalType == 1) { // Sine
124+
output = sin(mOutputPhase) * mAmplitude;
125+
mOutputPhase += mPhaseIncrement;
126+
if (mOutputPhase >= M_PI * 2) mOutputPhase -= M_PI * 2;
127+
} else if (mSignalType == 2) { // Silence
128+
output = 0.0f;
129+
}
130+
if (channelCount == 2) {
131+
float leftGain = (mBalance <= 0.5f) ? 1.0f : 2.0f * (1.0f - mBalance);
132+
float rightGain = (mBalance >= 0.5f) ? 1.0f : 2.0f * mBalance;
133+
frameData[0] = output * leftGain;
134+
frameData[1] = output * rightGain;
135+
} else {
136+
for (int i = 0; i < channelCount; i++) {
137+
frameData[i] = output;
138+
}
139+
}
140+
return RESULT_OK;
141+
}
142+
143+
std::string FrequencyAnalyzer::analyze() {
144+
return "FrequencyAnalyzer: Analysis the frequency magnitude";
145+
}
146+
147+
bool FrequencyAnalyzer::isDone() {
148+
return false;
149+
}
150+
151+
int FrequencyAnalyzer::getFftMagnitude(float* buffer, int length) {
152+
std::lock_guard<std::mutex> lock(mFftBufferLock);
153+
if (mFftMagnitudeBuffer.empty()) {
154+
return 0;
155+
}
156+
int numToCopy = std::min(length, (int) (WINDOW_SIZE / 2));
157+
for (int i = 0; i < numToCopy; i++) {
158+
buffer[i] = mFftMagnitudeBuffer[i];
159+
}
160+
return numToCopy;
161+
}
162+
163+
int FrequencyAnalyzer::getFftFrequencies(float* buffer, int length) {
164+
int numToCopy = std::min(length, (int) (WINDOW_SIZE / 2));
165+
double sampleRate = getSampleRate();
166+
for (int i = 0; i < numToCopy; i++) {
167+
buffer[i] = (float) (i * sampleRate / WINDOW_SIZE);
168+
}
169+
return numToCopy;
170+
}

0 commit comments

Comments
 (0)