Skip to content

Commit 124977d

Browse files
authored
OboeTester: Add AudioWorkloadTestActivity and AudioWorkloadTestRunnerActivity (#2199)
1 parent becd17e commit 124977d

21 files changed

Lines changed: 2153 additions & 68 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 => 106
9+
# Increment this number when adding files to OboeTester => 107
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: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,16 @@
128128
android:label="@string/title_rapid_cycle"
129129
android:exported="true"
130130
android:screenOrientation="portrait" />
131+
<activity
132+
android:name=".AudioWorkloadTestActivity"
133+
android:label="@string/title_audio_workload_test"
134+
android:exported="true"
135+
android:screenOrientation="portrait" />
136+
<activity
137+
android:name=".AudioWorkloadTestRunnerActivity"
138+
android:label="@string/title_audio_workload_test_runner"
139+
android:exported="true"
140+
android:screenOrientation="portrait" />
131141

132142
<service
133143
android:name=".MidiTapTester"

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

Lines changed: 1 addition & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#include "oboe/Oboe.h"
2525
#include "synth/Synthesizer.h"
2626
#include "synth/SynthTools.h"
27+
#include "cpu/SynthWorkload.h"
2728
#include "OboeTesterStreamCallback.h"
2829

2930
class DoubleStatistics {
@@ -69,71 +70,6 @@ class DoubleStatistics {
6970
std::atomic<double> maximum { 0 };
7071
};
7172

72-
/**
73-
* Manage the synthesizer workload that burdens the CPU.
74-
* Adjust the number of voices according to the requested workload.
75-
* Trigger noteOn and noteOff messages.
76-
*/
77-
class SynthWorkload {
78-
public:
79-
SynthWorkload() {
80-
mSynth.setup(marksynth::kSynthmarkSampleRate, marksynth::kSynthmarkMaxVoices);
81-
}
82-
83-
void onCallback(double workload) {
84-
// If workload changes then restart notes.
85-
if (workload != mPreviousWorkload) {
86-
mSynth.allNotesOff();
87-
mAreNotesOn = false;
88-
mCountdown = 0; // trigger notes on
89-
mPreviousWorkload = workload;
90-
}
91-
if (mCountdown <= 0) {
92-
if (mAreNotesOn) {
93-
mSynth.allNotesOff();
94-
mAreNotesOn = false;
95-
mCountdown = mOffFrames;
96-
} else {
97-
mSynth.notesOn((int)mPreviousWorkload);
98-
mAreNotesOn = true;
99-
mCountdown = mOnFrames;
100-
}
101-
}
102-
}
103-
104-
/**
105-
* Render the notes into a stereo buffer.
106-
* Passing a nullptr will cause the calculated results to be discarded.
107-
* The workload should be the same.
108-
* @param buffer a real stereo buffer or nullptr
109-
* @param numFrames
110-
*/
111-
void renderStereo(float *buffer, int numFrames) {
112-
if (buffer == nullptr) {
113-
int framesLeft = numFrames;
114-
while (framesLeft > 0) {
115-
int framesThisTime = std::min(kDummyBufferSizeInFrames, framesLeft);
116-
// Do the work then throw it away.
117-
mSynth.renderStereo(&mDummyStereoBuffer[0], framesThisTime);
118-
framesLeft -= framesThisTime;
119-
}
120-
} else {
121-
mSynth.renderStereo(buffer, numFrames);
122-
}
123-
mCountdown -= numFrames;
124-
}
125-
126-
private:
127-
marksynth::Synthesizer mSynth;
128-
static constexpr int kDummyBufferSizeInFrames = 32;
129-
float mDummyStereoBuffer[kDummyBufferSizeInFrames * 2];
130-
double mPreviousWorkload = 1.0;
131-
bool mAreNotesOn = false;
132-
int mCountdown = 0;
133-
int mOnFrames = (int) (0.2 * 48000);
134-
int mOffFrames = (int) (0.3 * 48000);
135-
};
136-
13773
class OboeStreamCallbackProxy : public OboeTesterStreamCallback {
13874
public:
13975

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
/*
2+
* Copyright 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 "AudioWorkloadTest.h"
18+
19+
int32_t AudioWorkloadTest::open() {
20+
oboe::AudioStreamBuilder builder;
21+
builder.setDirection(oboe::Direction::Output);
22+
builder.setPerformanceMode(oboe::PerformanceMode::LowLatency);
23+
builder.setSharingMode(oboe::SharingMode::Exclusive);
24+
builder.setFormat(oboe::AudioFormat::Float);
25+
builder.setChannelCount(2);
26+
builder.setDataCallback(this);
27+
28+
oboe::Result result = builder.openStream(mStream);
29+
if (result != oboe::Result::OK) {
30+
std::cerr << "Error opening stream: " << oboe::convertToText(result) << std::endl;
31+
return static_cast<int32_t>(result);
32+
}
33+
34+
mFramesPerBurst = mStream->getFramesPerBurst();
35+
mSampleRate = mStream->getSampleRate();
36+
mPreviousXRunCount = 0;
37+
mXRunCount = 0;
38+
mPhaseIncrement = 2.0f * (float) M_PI * 440.0f / mSampleRate; // 440 Hz sine wave
39+
40+
return 0;
41+
}
42+
43+
int32_t AudioWorkloadTest::getFramesPerBurst() const {
44+
return mFramesPerBurst;
45+
}
46+
47+
int32_t AudioWorkloadTest::getSampleRate() const {
48+
return mSampleRate;
49+
}
50+
51+
int32_t AudioWorkloadTest::getBufferSizeInFrames() const {
52+
return mBufferSizeInFrames;
53+
}
54+
55+
int32_t AudioWorkloadTest::start(int32_t targetDurationMs, int32_t numBursts, int32_t numVoices,
56+
int32_t alternateNumVoices, int32_t alternatingPeriodMs, bool adpfEnabled,
57+
bool hearWorkload) {
58+
mTargetDurationMs = targetDurationMs;
59+
mNumBursts = numBursts;
60+
mNumVoices = numVoices;
61+
mAlternateNumVoices = alternateNumVoices;
62+
mAlternatingPeriodMs = alternatingPeriodMs;
63+
mStartTimeMs = 0;
64+
mCallbackStatistics.clear();
65+
mCallbackCount = 0;
66+
mPreviousXRunCount = mXRunCount.load();
67+
mXRunCount = 0;
68+
mRunning = true;
69+
mHearWorkload = hearWorkload;
70+
mStream->setPerformanceHintEnabled(adpfEnabled);
71+
mStream->setBufferSizeInFrames(mNumBursts * mFramesPerBurst);
72+
mBufferSizeInFrames = mStream->getBufferSizeInFrames();
73+
mSynthWorkload = SynthWorkload((int) 0.2 * mSampleRate, (int) 0.3 * mSampleRate);
74+
oboe::Result result = mStream->start();
75+
if (result != oboe::Result::OK) {
76+
std::cerr << "Error starting stream: " << oboe::convertToText(result) << std::endl;
77+
return static_cast<int32_t>(result);
78+
}
79+
80+
return 0;
81+
}
82+
83+
int32_t AudioWorkloadTest::setCpuAffinityForCallback(uint32_t mask) {
84+
cpu_set_t cpuset;
85+
CPU_ZERO(&cpuset);
86+
for (uint32_t i = 0; i < 32; ++i) {
87+
if ((mask >> i) & 1) {
88+
CPU_SET(i, &cpuset);
89+
}
90+
}
91+
92+
if (sched_setaffinity(pthread_self(), sizeof(cpu_set_t), &cpuset) != 0) {
93+
std::cerr << "Error setting CPU affinity." << std::endl;
94+
return -1;
95+
}
96+
return 0;
97+
}
98+
99+
int32_t AudioWorkloadTest::getCpuCount() {
100+
return sysconf(_SC_NPROCESSORS_CONF);
101+
}
102+
103+
int32_t AudioWorkloadTest::getXRunCount() const {
104+
return mXRunCount - mPreviousXRunCount;
105+
}
106+
107+
int32_t AudioWorkloadTest::getCallbackCount() const {
108+
return mCallbackCount;
109+
}
110+
111+
int64_t AudioWorkloadTest::getLastDurationNs() {
112+
return mLastDurationNs;
113+
}
114+
115+
bool AudioWorkloadTest::isRunning() {
116+
return mRunning;
117+
}
118+
119+
int32_t AudioWorkloadTest::stop() {
120+
if (mStream) {
121+
oboe::Result result = mStream->stop();
122+
if (result != oboe::Result::OK) {
123+
std::cerr << "Error stopping stream: " << oboe::convertToText(result) << std::endl;
124+
return static_cast<int32_t>(result);
125+
}
126+
}
127+
return 0;
128+
}
129+
130+
int32_t AudioWorkloadTest::close() {
131+
if (mStream) {
132+
oboe::Result result = mStream->close();
133+
mStream = nullptr;
134+
if (result != oboe::Result::OK) {
135+
std::cerr << "Error closing stream: " << oboe::convertToText(result) << std::endl;
136+
return static_cast<int32_t>(result);
137+
}
138+
}
139+
return 0;
140+
}
141+
142+
std::vector<AudioWorkloadTest::CallbackStatus> AudioWorkloadTest::getCallbackStatistics() {
143+
return mCallbackStatistics;
144+
}
145+
146+
oboe::DataCallbackResult AudioWorkloadTest::onAudioReady(oboe::AudioStream* audioStream,
147+
void* audioData, int32_t numFrames) {
148+
int64_t beginTimeNs = std::chrono::duration_cast<std::chrono::nanoseconds>(
149+
std::chrono::high_resolution_clock::now().time_since_epoch()).count();
150+
151+
int currentVoices = mNumVoices;
152+
if (mAlternatingPeriodMs > 0) {
153+
int64_t timeMs = std::chrono::duration_cast<std::chrono::milliseconds>(
154+
std::chrono::high_resolution_clock::now().time_since_epoch()).count();
155+
if (mStartTimeMs == 0) {
156+
mStartTimeMs = timeMs;
157+
}
158+
if (((timeMs - mStartTimeMs) % (2 * mAlternatingPeriodMs)) >= mAlternatingPeriodMs) {
159+
currentVoices = mAlternateNumVoices;
160+
}
161+
}
162+
163+
auto floatData = static_cast<float *>(audioData);
164+
int channelCount = audioStream->getChannelCount();
165+
166+
// Fill buffer with a sine wave.
167+
for (int i = 0; i < numFrames; i++) {
168+
float value = sinf(mPhase) * 0.2f;
169+
for (int j = 0; j < channelCount; j++) {
170+
*floatData++ = value;
171+
}
172+
mPhase = mPhase + mPhaseIncrement;
173+
// Wrap the phase around in a circle.
174+
if (mPhase >= M_PI) mPhase = mPhase - 2.0f * M_PI;
175+
}
176+
177+
mSynthWorkload.onCallback(currentVoices);
178+
if (currentVoices > 0) {
179+
// Render synth workload into the buffer or discard the synth voices.
180+
float *buffer = (audioStream->getChannelCount() == 2 && mHearWorkload)
181+
? static_cast<float *>(audioData) : nullptr;
182+
mSynthWorkload.renderStereo(buffer, numFrames);
183+
}
184+
185+
int64_t finishTimeNs = std::chrono::duration_cast<std::chrono::nanoseconds>(
186+
std::chrono::high_resolution_clock::now().time_since_epoch()).count();
187+
188+
mXRunCount = audioStream->getXRunCount().value();
189+
190+
CallbackStatus status{};
191+
status.numVoices = currentVoices;
192+
status.beginTimeNs = beginTimeNs;
193+
status.finishTimeNs = finishTimeNs;
194+
status.xRunCount = mXRunCount - mPreviousXRunCount;
195+
status.cpuIndex = sched_getcpu();
196+
197+
mCallbackStatistics.push_back(status);
198+
mCallbackCount++;
199+
mLastDurationNs = finishTimeNs - beginTimeNs;
200+
201+
int64_t currentTimeMs = std::chrono::duration_cast<std::chrono::milliseconds>(
202+
std::chrono::high_resolution_clock::now().time_since_epoch()).count();
203+
204+
if (currentTimeMs - mStartTimeMs > mTargetDurationMs) {
205+
mRunning = false;
206+
stop();
207+
return oboe::DataCallbackResult::Stop;
208+
}
209+
210+
return oboe::DataCallbackResult::Continue;
211+
}

0 commit comments

Comments
 (0)