Skip to content

Commit 38e8d0c

Browse files
authored
OboeTester: Add chirp and multi Data Paths tests (#2326)
* OboeTester: Add chirp and multi Data Paths tests * address comments * address comments
1 parent 910b422 commit 38e8d0c

9 files changed

Lines changed: 542 additions & 87 deletions

File tree

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 => 110
9+
# Increment this number when adding files to OboeTester => 111
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/cpp/analyzer/BaseSineAnalyzer.h

Lines changed: 91 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,25 @@
3434
class BaseSineAnalyzer : public LoopbackProcessor {
3535
public:
3636

37+
enum SignalType {
38+
Sine,
39+
Chirp,
40+
MultiTone
41+
};
42+
3743
BaseSineAnalyzer()
3844
: LoopbackProcessor()
3945
, mInfiniteRecording(64 * 1024) {}
4046

4147
virtual bool isOutputEnabled() { return true; }
4248

49+
void setSignalType(int signalType) {
50+
mSignalType = static_cast<SignalType>(signalType);
51+
if (mSignalType < SignalType::Sine || mSignalType > SignalType::MultiTone) {
52+
ALOGD("%s(), invalid signal type %d\n", __func__, mSignalType);
53+
}
54+
}
55+
4356
void setMagnitude(double magnitude) {
4457
mMagnitude = magnitude;
4558
mScaledTolerance = mMagnitude * getTolerance();
@@ -96,6 +109,15 @@ class BaseSineAnalyzer : public LoopbackProcessor {
96109
}
97110
}
98111

112+
void incrementMultiTonePhases() {
113+
for (size_t i = 0; i < mMultiTonePhases.size(); i++) {
114+
mMultiTonePhases[i] += mMultiTonePhaseIncrements[i];
115+
if (mMultiTonePhases[i] > M_PI) {
116+
mMultiTonePhases[i] -= (2.0 * M_PI);
117+
}
118+
}
119+
}
120+
99121

100122
/**
101123
* @param frameData upon return, contains the reference sine wave
@@ -105,11 +127,44 @@ class BaseSineAnalyzer : public LoopbackProcessor {
105127
float output = 0.0f;
106128
// Output sine wave so we can measure it.
107129
if (isOutputEnabled()) {
108-
float sinOut = sinf(mOutputPhase);
109-
incrementOutputPhase();
110-
output = (sinOut * mOutputAmplitude)
111-
+ (mWhiteNoise.nextRandomDouble() * getNoiseAmplitude());
112-
// ALOGD("sin(%f) = %f, %f\n", mOutputPhase, sinOut, kPhaseIncrement);
130+
switch (mSignalType) {
131+
case Chirp: {
132+
if (mFrameCounter < getSampleRate() * kChirpDurationSeconds) {
133+
float sinOut = sinf(mOutputPhase);
134+
// Simple linear chirp from kChirpStartFrequency to mChirpEndFrequencyActual
135+
// in kChirpDurationSeconds seconds.
136+
double freq = kChirpStartFrequency
137+
+ (mChirpEndFrequencyActual - kChirpStartFrequency)
138+
* mFrameCounter
139+
/ (getSampleRate() * kChirpDurationSeconds);
140+
mPhaseIncrement = 2.0 * M_PI * freq / getSampleRate();
141+
incrementOutputPhase();
142+
output = sinOut * mOutputAmplitude;
143+
mFrameCounter++;
144+
} else {
145+
output = 0.0f;
146+
}
147+
break;
148+
}
149+
case MultiTone: {
150+
double sum = 0.0;
151+
for (double phase : mMultiTonePhases) {
152+
sum += sin(phase);
153+
}
154+
incrementMultiTonePhases();
155+
output = (sum / mMultiTonePhases.size()) * mOutputAmplitude;
156+
break;
157+
}
158+
case Sine:
159+
default: {
160+
float sinOut = sinf(mOutputPhase);
161+
incrementOutputPhase();
162+
output = (sinOut * mOutputAmplitude)
163+
+ (mWhiteNoise.nextRandomDouble() * getNoiseAmplitude());
164+
// ALOGD("sin(%f) = %f, %f\n", mOutputPhase, sinOut, kPhaseIncrement);
165+
break;
166+
}
167+
}
113168
}
114169
for (int i = 0; i < channelCount; i++) {
115170
frameData[i] = (i == getOutputChannel()) ? output : 0.0f;
@@ -192,6 +247,7 @@ class BaseSineAnalyzer : public LoopbackProcessor {
192247
LoopbackProcessor::reset();
193248
resetAccumulator();
194249
mMagnitude = 0.0;
250+
mFrameCounter = 0;
195251
}
196252

197253
void prepareToTest() override {
@@ -201,13 +257,34 @@ class BaseSineAnalyzer : public LoopbackProcessor {
201257
mOutputPhase = 0.0f;
202258
mInverseSinePeriod = 1.0 / mSinePeriod;
203259
mPhaseIncrement = 2.0 * M_PI * mInverseSinePeriod;
260+
261+
mMultiTonePhases.clear();
262+
mMultiTonePhaseIncrements.clear();
263+
for (double freq : sMultiToneFrequencies) {
264+
mMultiTonePhases.push_back(0.0);
265+
mMultiTonePhaseIncrements.push_back(2.0 * M_PI * freq / getSampleRate());
266+
}
267+
268+
// Adjust chirp frequency to be no higher than Nyquist.
269+
mChirpEndFrequencyActual = std::min((double)kChirpEndFrequency, getSampleRate() / 2.0);
204270
}
205271

206272
protected:
207273
// Use a frequency that will not align with the common burst sizes.
208274
// If it aligns then buffer reordering bugs could be masked.
209275
static constexpr int32_t kTargetGlitchFrequency = 857; // Match CTS Verifier
210276

277+
// Chirp constants
278+
static constexpr double kChirpStartFrequency = 20.0;
279+
static constexpr double kChirpEndFrequency = 15000.0;
280+
static constexpr double kChirpDurationSeconds = 4.0;
281+
282+
// Multi-tone constants
283+
static constexpr double sMultiToneFrequencies[] = {401.0, 601.0, 1009.0, 1409.0, 2203.0};
284+
285+
SignalType mSignalType = Sine;
286+
int32_t mFrameCounter = 0;
287+
211288
int32_t mSinePeriod = 1; // this will be set before use
212289
double mInverseSinePeriod = 1.0;
213290
double mPhaseIncrement = 0.0;
@@ -216,6 +293,15 @@ class BaseSineAnalyzer : public LoopbackProcessor {
216293
// in a callback and the output frame count may advance ahead of the input, or visa versa.
217294
double mInputPhase = 0.0;
218295
double mOutputPhase = 0.0;
296+
297+
// For chirp
298+
double mChirpEndFrequencyActual = kChirpEndFrequency; // Nyquist adjusted
299+
300+
// For multi-tone
301+
std::vector<double> mMultiTonePhases;
302+
std::vector<double> mMultiTonePhaseIncrements;
303+
304+
219305
double mOutputAmplitude = 0.90;
220306
// This is the phase offset between the mInputPhase sine wave and the recorded
221307
// signal at the tuned frequency.
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
/*
2+
* Copyright (C) 2025 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 "DataPathAnalyzer.h"
18+
#include <sstream>
19+
#include <iomanip>
20+
#include "fft.h"
21+
22+
double DataPathAnalyzer::calculatePhaseError(double p1, double p2) {
23+
double diff = p1 - p2;
24+
// Wrap around the circle.
25+
while (diff > M_PI) {
26+
diff -= (2 * M_PI);
27+
}
28+
while (diff < -M_PI) {
29+
diff += (2 * M_PI);
30+
}
31+
return diff;
32+
}
33+
34+
BaseSineAnalyzer::result_code DataPathAnalyzer::processInputFrame(const float *frameData, int channelCount) {
35+
switch (mSignalType) {
36+
case Chirp: {
37+
if (mFftBuffer.size() != mFftBufferSize) {
38+
mFftBuffer.resize(mFftBufferSize);
39+
}
40+
if (mFftBufferIndex == 0) {
41+
mFftBufferStartFrame = mFrameCounter;
42+
}
43+
float sample = frameData[getInputChannel()];
44+
mFftBuffer[mFftBufferIndex++] = sample;
45+
if (mFftBufferIndex >= mFftBufferSize) {
46+
// Perform Spectrogram Analysis
47+
std::stringstream report;
48+
report << "Chirp analysis (peak frequency per window):\n";
49+
50+
std::vector<double> peakFreqs;
51+
for (int i = 0; i + mSpectrogramWindowSize <= mFftBufferSize; i += mSpectrogramHopSize) {
52+
CVector fftInput(mSpectrogramWindowSize);
53+
for (int j = 0; j < mSpectrogramWindowSize; j++) {
54+
fftInput[j] = Complex(mFftBuffer[i + j], 0);
55+
}
56+
fft(fftInput);
57+
58+
double maxMag = 0;
59+
int peakBin = 0;
60+
long frameInChirp = mFftBufferStartFrame + i + mSpectrogramWindowSize / 2;
61+
double maxFreq = kChirpEndFrequency;
62+
if (maxFreq > getSampleRate() / 2.0) {
63+
maxFreq = getSampleRate() / 2.0;
64+
}
65+
double expectedFreq = kChirpStartFrequency + (maxFreq - kChirpStartFrequency) * frameInChirp / (getSampleRate() * kChirpDurationSeconds);
66+
int expectedBin = (int)(expectedFreq * mSpectrogramWindowSize / getSampleRate());
67+
int searchRadius = 50; // search in a window of 100 bins
68+
int startBin = std::max(1, expectedBin - searchRadius);
69+
int endBin = std::min(mSpectrogramWindowSize / 2, expectedBin + searchRadius);
70+
71+
for (int k = startBin; k < endBin; k++) {
72+
double mag = std::abs(fftInput[k]);
73+
if (mag > maxMag) {
74+
maxMag = mag;
75+
peakBin = k;
76+
}
77+
}
78+
double peakFreq = (double)peakBin * getSampleRate() / mSpectrogramWindowSize;
79+
peakFreqs.push_back(peakFreq);
80+
report << std::fixed << std::setprecision(0) << peakFreq << " Hz\n";
81+
}
82+
83+
// Check if frequencies are monotonically increasing
84+
bool passed = true;
85+
for (size_t i = 1; i < peakFreqs.size(); i++) {
86+
if (peakFreqs[i] < peakFreqs[i-1]) {
87+
passed = false;
88+
break;
89+
}
90+
}
91+
92+
if (passed) {
93+
mAnalysisResult = 0; // Pass
94+
report << "PASS: Frequencies are monotonically increasing.\n";
95+
} else {
96+
mAnalysisResult = 1; // Fail
97+
report << "FAIL: Frequencies are not monotonically increasing.\n";
98+
}
99+
100+
mFrequencyResponse = report.str();
101+
mFftBufferIndex = 0;
102+
}
103+
break;
104+
}
105+
case MultiTone: {
106+
if (mFftBuffer.size() != mFftBufferSize) {
107+
mFftBuffer.resize(mFftBufferSize);
108+
}
109+
float sample = frameData[getInputChannel()];
110+
mFftBuffer[mFftBufferIndex++] = sample;
111+
if (mFftBufferIndex >= mFftBufferSize) {
112+
// Perform FFT
113+
CVector fftInput(mFftBufferSize);
114+
for (int i = 0; i < mFftBufferSize; i++) {
115+
fftInput[i] = Complex(mFftBuffer[i], 0);
116+
}
117+
fft(fftInput);
118+
119+
// Analyze FFT output
120+
double signalPower = 0;
121+
double noisePower = 0;
122+
123+
const int numTones = sizeof(sMultiToneFrequencies) / sizeof(sMultiToneFrequencies[0]);
124+
int bins[numTones];
125+
for (int i = 0; i < numTones; i++) {
126+
bins[i] = (int)(sMultiToneFrequencies[i] * mFftBufferSize / getSampleRate());
127+
}
128+
129+
for (int i = 1; i < mFftBufferSize / 2; i++) {
130+
double power = std::norm(fftInput[i]);
131+
bool isSignal = false;
132+
for (int j = 0; j < numTones; j++) {
133+
if (i >= bins[j] - 1 && i <= bins[j] + 1) {
134+
isSignal = true;
135+
break;
136+
}
137+
}
138+
if (isSignal) {
139+
signalPower += power;
140+
} else {
141+
noisePower += power;
142+
}
143+
}
144+
145+
std::stringstream report;
146+
report << "Multi-tone analysis:\n";
147+
if (noisePower > 0) {
148+
double sinad = 10 * log10(signalPower / noisePower);
149+
report << "SINAD = " << std::fixed << std::setprecision(2) << sinad << " dB\n";
150+
if (sinad < mMinSinad) {
151+
mAnalysisResult = 1; // Fail
152+
report << "FAIL: SINAD is below threshold of " << mMinSinad << " dB\n";
153+
} else {
154+
mAnalysisResult = 0; // Pass
155+
}
156+
} else {
157+
report << "SINAD = inf\n";
158+
mAnalysisResult = 0; // Pass
159+
}
160+
mDistortionReport = report.str();
161+
mFftBufferIndex = 0;
162+
}
163+
break;
164+
}
165+
case Sine:
166+
default: {
167+
float sample = frameData[getInputChannel()];
168+
mInfiniteRecording.write(sample);
169+
170+
if (transformSample(sample)) {
171+
// Analyze magnitude and phase on every period.
172+
if (mPhaseOffset != kPhaseInvalid) {
173+
double diff = fabs(calculatePhaseError(mPhaseOffset, mPreviousPhaseOffset));
174+
if (diff < mPhaseTolerance) {
175+
mMaxMagnitude = std::max(mMagnitude, mMaxMagnitude);
176+
}
177+
mPreviousPhaseOffset = mPhaseOffset;
178+
}
179+
}
180+
break;
181+
}
182+
}
183+
return RESULT_OK;
184+
}
185+
186+
std::string DataPathAnalyzer::analyze() {
187+
std::stringstream report;
188+
report << "DataPathAnalyzer ------------------\n";
189+
switch (mSignalType) {
190+
case Sine:
191+
report << "LOOPBACK_RESULT_TAG " << "sine.magnitude = " << std::setw(8)
192+
<< mMagnitude << "\n";
193+
report << "LOOPBACK_RESULT_TAG " << "frames.accumulated = " << std::setw(8)
194+
<< mFramesAccumulated << "\n";
195+
report << "LOOPBACK_RESULT_TAG " << "sine.period = " << std::setw(8)
196+
<< mSinePeriod << "\n";
197+
break;
198+
case Chirp:
199+
report << "Chirp analysis not implemented yet.\n";
200+
break;
201+
case MultiTone:
202+
report << "Multi-tone analysis not implemented yet.\n";
203+
break;
204+
}
205+
return report.str();
206+
}
207+
208+
void DataPathAnalyzer::reset() {
209+
BaseSineAnalyzer::reset();
210+
mPreviousPhaseOffset = 999.0; // Arbitrary high offset to prevent early lock.
211+
mMaxMagnitude = 0.0;
212+
}
213+
214+
double DataPathAnalyzer::getMaxMagnitude() {
215+
return mMaxMagnitude;
216+
}
217+
218+
std::string DataPathAnalyzer::getFrequencyResponse() {
219+
return mFrequencyResponse;
220+
}
221+
222+
std::string DataPathAnalyzer::getDistortionReport() {
223+
return mDistortionReport;
224+
}
225+
226+
int DataPathAnalyzer::getAnalysisResult() {
227+
return mAnalysisResult;
228+
}
229+

0 commit comments

Comments
 (0)