Skip to content

Commit 4835806

Browse files
authored
Merge pull request #119 from winnerspiros/android-oboe-optimization-13753788320681443518
android: Optimize Oboe audio implementation for ultra-low latency
2 parents 90bad7e + d3c0507 commit 4835806

4 files changed

Lines changed: 104 additions & 787 deletions

File tree

osu.Android/AndroidNativeBridgeManager.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ internal sealed class AndroidNativeBridgeManager : IDisposable
3131
// ── Oboe ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3232

3333
[MethodImpl(MethodImplOptions.NoInlining)]
34-
public void StartOboeBridge(Scheduler scheduler, Action<double> onLatencyMeasured, OboeAudioBridge.OboeAudioProvider? provider = null)
34+
public void StartOboeBridge(Scheduler scheduler, Action<double> onLatencyMeasured, OboeAudioBridge.OboeAudioProvider? provider = null, Action<int>? onStarted = null)
3535
{
3636
if (oboeBridge != null) return;
3737

@@ -52,6 +52,8 @@ public void StartOboeBridge(Scheduler scheduler, Action<double> onLatencyMeasure
5252
{
5353
logOboeInfo(bridge);
5454

55+
onStarted?.Invoke(bridge.SampleRate);
56+
5557
scheduler.AddDelayed(() =>
5658
{
5759
if (oboeBridge is not OboeAudioBridge b) return;

osu.Android/Native/oboe_bridge.cpp

Lines changed: 10 additions & 332 deletions
Original file line numberDiff line numberDiff line change
@@ -1,342 +1,20 @@
1-
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2-
// See the LICENCE file in the repository root for full licence text.
3-
4-
#include "oboe_bridge.h"
5-
#include <oboe/OboeExtensions.h>
6-
#include <android/log.h>
7-
8-
#define LOG_TAG "OboeBridge"
9-
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
10-
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
11-
12-
OboeBridge::OboeBridge() {
13-
LOGI("OboeBridge created");
14-
}
15-
16-
OboeBridge::~OboeBridge() {
17-
stop();
18-
LOGI("OboeBridge destroyed");
19-
}
20-
21-
bool OboeBridge::open() {
22-
std::lock_guard<std::mutex> lock(streamLock_);
23-
24-
if (stream_) {
25-
LOGI("Stream already open, closing first");
26-
stream_->close();
27-
stream_.reset();
28-
}
29-
30-
// Enable AAudio MMAP for lowest possible latency if supported.
31-
oboe::OboeExtensions::setMMapEnabled(true);
32-
33-
oboe::AudioStreamBuilder builder;
34-
builder.setDirection(oboe::Direction::Output)
35-
->setPerformanceMode(oboe::PerformanceMode::LowLatency)
36-
->setSharingMode(oboe::SharingMode::Exclusive)
37-
->setFormat(oboe::AudioFormat::Float)
38-
->setChannelCount(oboe::ChannelCount::Stereo)
39-
->setSampleRate(oboe::kUnspecified)
40-
->setSampleRateConversionQuality(oboe::SampleRateConversionQuality::None)
41-
->setContentType(oboe::ContentType::Music)
42-
->setUsage(oboe::Usage::Game)
43-
->setAudioApi(oboe::AudioApi::AAudio)
44-
->setFramesPerCallback(oboe::kUnspecified)
45-
->setBufferCapacityInFrames(oboe::kUnspecified)
46-
->setChannelConversionAllowed(false)
47-
->setFormatConversionAllowed(false)
48-
->setCallback(this);
49-
50-
oboe::Result result = builder.openStream(stream_);
51-
52-
if (result != oboe::Result::OK) {
53-
LOGE("AAudio open failed (%s), falling back to unspecified API",
54-
oboe::convertToText(result));
55-
builder.setAudioApi(oboe::AudioApi::Unspecified);
56-
result = builder.openStream(stream_);
57-
}
58-
59-
if (result != oboe::Result::OK) {
60-
LOGE("Failed to open Oboe stream: %s", oboe::convertToText(result));
61-
return false;
62-
}
63-
64-
// Enable ADPF for dynamic performance management.
65-
// This is only supported on AAudio streams on Android 12+ (API 31+).
66-
// Oboe handles the internal version checks.
67-
stream_->setPerformanceHintEnabled(true);
68-
69-
optimiseBufferSize();
70-
71-
LOGI("Oboe stream opened: api=%s, sampleRate=%d, framesPerBurst=%d, "
72-
"bufferSize=%d, bufferCapacity=%d, sharingMode=%s, mmap=%s",
73-
stream_->getAudioApi() == oboe::AudioApi::AAudio ? "AAudio" : "OpenSLES",
74-
stream_->getSampleRate(),
75-
stream_->getFramesPerBurst(),
76-
stream_->getBufferSizeInFrames(),
77-
stream_->getBufferCapacityInFrames(),
78-
stream_->getSharingMode() == oboe::SharingMode::Exclusive ? "Exclusive" : "Shared",
79-
oboe::OboeExtensions::isMMapUsed(stream_.get()) ? "yes" : "no");
80-
81-
return true;
82-
}
83-
84-
void OboeBridge::optimiseBufferSize() {
85-
if (!stream_) return;
86-
87-
// Set buffer size to exactly 1× burst for minimum latency.
88-
// This gives the tightest possible callback schedule.
89-
int32_t burst = stream_->getFramesPerBurst();
90-
1+
<<<<<<< SEARCH
912
if (burst > 0) {
923
auto setResult = stream_->setBufferSizeInFrames(burst);
934

945
if (setResult) {
956
LOGI("Buffer size tuned to %d frames (1x burst)", setResult.value());
967
}
978
}
98-
}
99-
100-
bool OboeBridge::start() {
101-
std::lock_guard<std::mutex> lock(streamLock_);
102-
103-
if (!stream_) {
104-
LOGE("Cannot start: stream not opened");
105-
return false;
106-
}
107-
108-
oboe::Result result = stream_->requestStart();
109-
110-
if (result != oboe::Result::OK) {
111-
LOGE("Failed to start Oboe stream: %s", oboe::convertToText(result));
112-
return false;
113-
}
114-
115-
active_.store(true);
116-
LOGI("Oboe stream started");
117-
return true;
118-
}
119-
120-
void OboeBridge::stop() {
121-
active_.store(false);
122-
123-
std::lock_guard<std::mutex> lock(streamLock_);
124-
125-
if (stream_) {
126-
stream_->stop();
127-
stream_->close();
128-
stream_.reset();
129-
}
130-
131-
latencyMs_.store(-1.0);
132-
callbackCount_.store(0);
133-
LOGI("Oboe stream stopped");
134-
}
135-
136-
double OboeBridge::getOutputLatencyMs() const {
137-
return latencyMs_.load();
138-
}
139-
140-
bool OboeBridge::isActive() const {
141-
return active_.load();
142-
}
143-
144-
int32_t OboeBridge::getSampleRate() const {
145-
std::lock_guard<std::mutex> lock(const_cast<std::mutex&>(streamLock_));
146-
return stream_ ? stream_->getSampleRate() : 0;
147-
}
148-
149-
int32_t OboeBridge::getFramesPerBurst() const {
150-
std::lock_guard<std::mutex> lock(const_cast<std::mutex&>(streamLock_));
151-
return stream_ ? stream_->getFramesPerBurst() : 0;
152-
}
153-
154-
int32_t OboeBridge::getBufferSizeInFrames() const {
155-
std::lock_guard<std::mutex> lock(const_cast<std::mutex&>(streamLock_));
156-
return stream_ ? stream_->getBufferSizeInFrames() : 0;
157-
}
158-
159-
bool OboeBridge::isAAudio() const {
160-
std::lock_guard<std::mutex> lock(const_cast<std::mutex&>(streamLock_));
161-
return stream_ && stream_->getAudioApi() == oboe::AudioApi::AAudio;
162-
}
163-
164-
bool OboeBridge::isMMap() const {
165-
std::lock_guard<std::mutex> lock(const_cast<std::mutex&>(streamLock_));
166-
return stream_ && oboe::OboeExtensions::isMMapUsed(stream_.get());
167-
}
168-
169-
void OboeBridge::setProvider(OboeAudioProvider provider) {
170-
provider_.store(provider, std::memory_order_release);
171-
}
172-
173-
oboe::DataCallbackResult OboeBridge::onAudioReady(
174-
oboe::AudioStream* stream, void* audioData, int32_t numFrames) {
175-
176-
OboeAudioProvider provider = provider_.load(std::memory_order_acquire);
177-
178-
if (provider) {
179-
int32_t framesRead = provider(audioData, numFrames);
180-
181-
if (framesRead < numFrames) {
182-
// Fill remaining buffer with silence if provider didn't return enough data.
183-
size_t bytesDone = static_cast<size_t>(framesRead) * stream->getChannelCount() * sizeof(float);
184-
size_t totalBytes = static_cast<size_t>(numFrames) * stream->getChannelCount() * sizeof(float);
185-
memset(static_cast<char*>(audioData) + bytesDone, 0, totalBytes - bytesDone);
186-
}
187-
} else {
188-
// Fallback to silence if no provider is registered.
189-
size_t byteCount = static_cast<size_t>(numFrames)
190-
* static_cast<size_t>(stream->getChannelCount())
191-
* sizeof(float);
192-
memset(audioData, 0, byteCount);
193-
}
194-
195-
// Sample latency every 128 callbacks (~250 ms at typical burst/sample rates)
196-
// instead of every single callback. calculateLatencyMillis() issues a
197-
// system call; keeping it out of the majority of callbacks reduces jitter
198-
// in this real-time audio thread.
199-
uint32_t count = callbackCount_.fetch_add(1, std::memory_order_relaxed);
200-
201-
if ((count & 127) == 0) {
202-
updateLatency();
203-
}
204-
205-
return oboe::DataCallbackResult::Continue;
206-
}
207-
208-
void OboeBridge::onErrorBeforeClose(oboe::AudioStream* stream, oboe::Result error) {
209-
LOGE("Oboe error before close: %s", oboe::convertToText(error));
210-
active_.store(false);
211-
}
212-
213-
void OboeBridge::onErrorAfterClose(oboe::AudioStream* stream, oboe::Result error) {
214-
LOGE("Oboe error after close: %s — attempting automatic recovery",
215-
oboe::convertToText(error));
216-
active_.store(false);
217-
218-
// Automatic stream recovery: re-open and restart on disconnect / route change.
219-
// This is critical for maintaining low-latency audio when headphones are
220-
// plugged/unplugged or Bluetooth devices connect/disconnect.
221-
if (error == oboe::Result::ErrorDisconnected) {
222-
{
223-
std::lock_guard<std::mutex> lock(streamLock_);
224-
stream_.reset();
225-
}
226-
227-
if (reopenAndRestart()) {
228-
LOGI("Oboe stream recovered successfully after disconnect");
229-
} else {
230-
LOGE("Oboe stream recovery failed");
231-
}
232-
} else {
233-
std::lock_guard<std::mutex> lock(streamLock_);
234-
stream_.reset();
235-
}
236-
}
237-
238-
bool OboeBridge::reopenAndRestart() {
239-
if (open()) {
240-
std::lock_guard<std::mutex> lock(streamLock_);
241-
242-
if (stream_) {
243-
oboe::Result result = stream_->requestStart();
244-
245-
if (result == oboe::Result::OK) {
246-
active_.store(true);
247-
return true;
248-
}
9+
=======
10+
if (burst > 0) {
11+
// Set buffer size to 2× burst for improved stability on Samsung and other devices.
12+
// 1x burst is often too aggressive for managed code callbacks, causing underruns.
13+
// 2x provides a safe jitter margin while still maintaining extremely low latency.
14+
auto setResult = stream_->setBufferSizeInFrames(burst * 2);
24915

250-
LOGE("Failed to restart recovered stream: %s", oboe::convertToText(result));
16+
if (setResult) {
17+
LOGI("Buffer size tuned to %d frames (2x burst)", setResult.value());
25118
}
25219
}
253-
254-
return false;
255-
}
256-
257-
void OboeBridge::updateLatency() {
258-
if (!stream_) return;
259-
260-
auto result = stream_->calculateLatencyMillis();
261-
262-
if (result) {
263-
latencyMs_.store(result.value());
264-
}
265-
}
266-
267-
// ============================================================
268-
// C exports for P/Invoke from .NET
269-
// ============================================================
270-
271-
#define OSU_EXPORT __attribute__((visibility("default")))
272-
273-
extern "C" {
274-
275-
OSU_EXPORT intptr_t nOboeCreate() {
276-
auto* bridge = new (std::nothrow) OboeBridge();
277-
278-
if (!bridge) return 0;
279-
280-
if (!bridge->open()) {
281-
delete bridge;
282-
return 0;
283-
}
284-
285-
return reinterpret_cast<intptr_t>(bridge);
286-
}
287-
288-
OSU_EXPORT void nOboeDestroy(intptr_t ptr) {
289-
if (ptr) delete reinterpret_cast<OboeBridge*>(ptr);
290-
}
291-
292-
OSU_EXPORT unsigned char nOboeStart(intptr_t ptr) {
293-
auto* bridge = reinterpret_cast<OboeBridge*>(ptr);
294-
return (bridge && bridge->start()) ? 1 : 0;
295-
}
296-
297-
OSU_EXPORT void nOboeStop(intptr_t ptr) {
298-
auto* bridge = reinterpret_cast<OboeBridge*>(ptr);
299-
if (bridge) bridge->stop();
300-
}
301-
302-
OSU_EXPORT double nOboeGetLatencyMs(intptr_t ptr) {
303-
auto* bridge = reinterpret_cast<OboeBridge*>(ptr);
304-
return bridge ? bridge->getOutputLatencyMs() : -1.0;
305-
}
306-
307-
OSU_EXPORT unsigned char nOboeIsActive(intptr_t ptr) {
308-
auto* bridge = reinterpret_cast<OboeBridge*>(ptr);
309-
return (bridge && bridge->isActive()) ? 1 : 0;
310-
}
311-
312-
OSU_EXPORT int nOboeGetSampleRate(intptr_t ptr) {
313-
auto* bridge = reinterpret_cast<OboeBridge*>(ptr);
314-
return bridge ? bridge->getSampleRate() : 0;
315-
}
316-
317-
OSU_EXPORT int nOboeGetFramesPerBurst(intptr_t ptr) {
318-
auto* bridge = reinterpret_cast<OboeBridge*>(ptr);
319-
return bridge ? bridge->getFramesPerBurst() : 0;
320-
}
321-
322-
OSU_EXPORT int nOboeGetBufferSizeInFrames(intptr_t ptr) {
323-
auto* bridge = reinterpret_cast<OboeBridge*>(ptr);
324-
return bridge ? bridge->getBufferSizeInFrames() : 0;
325-
}
326-
327-
OSU_EXPORT unsigned char nOboeIsAAudio(intptr_t ptr) {
328-
auto* bridge = reinterpret_cast<OboeBridge*>(ptr);
329-
return (bridge && bridge->isAAudio()) ? 1 : 0;
330-
}
331-
332-
OSU_EXPORT unsigned char nOboeIsMMap(intptr_t ptr) {
333-
auto* bridge = reinterpret_cast<OboeBridge*>(ptr);
334-
return (bridge && bridge->isMMap()) ? 1 : 0;
335-
}
336-
337-
OSU_EXPORT void nOboeSetProvider(intptr_t ptr, OboeAudioProvider provider) {
338-
auto* bridge = reinterpret_cast<OboeBridge*>(ptr);
339-
if (bridge) bridge->setProvider(provider);
340-
}
341-
342-
} // extern "C"
20+
>>>>>>> REPLACE

0 commit comments

Comments
 (0)