Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion osu.Android/AndroidNativeBridgeManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ internal sealed class AndroidNativeBridgeManager : IDisposable
// ── Oboe ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

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

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

onStarted?.Invoke(bridge.SampleRate);

scheduler.AddDelayed(() =>
{
if (oboeBridge is not OboeAudioBridge b) return;
Expand Down
342 changes: 10 additions & 332 deletions osu.Android/Native/oboe_bridge.cpp
Original file line number Diff line number Diff line change
@@ -1,342 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

#include "oboe_bridge.h"
#include <oboe/OboeExtensions.h>
#include <android/log.h>

#define LOG_TAG "OboeBridge"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

OboeBridge::OboeBridge() {
LOGI("OboeBridge created");
}

OboeBridge::~OboeBridge() {
stop();
LOGI("OboeBridge destroyed");
}

bool OboeBridge::open() {
std::lock_guard<std::mutex> lock(streamLock_);

if (stream_) {
LOGI("Stream already open, closing first");
stream_->close();
stream_.reset();
}

// Enable AAudio MMAP for lowest possible latency if supported.
oboe::OboeExtensions::setMMapEnabled(true);

oboe::AudioStreamBuilder builder;
builder.setDirection(oboe::Direction::Output)
->setPerformanceMode(oboe::PerformanceMode::LowLatency)
->setSharingMode(oboe::SharingMode::Exclusive)
->setFormat(oboe::AudioFormat::Float)
->setChannelCount(oboe::ChannelCount::Stereo)
->setSampleRate(oboe::kUnspecified)
->setSampleRateConversionQuality(oboe::SampleRateConversionQuality::None)
->setContentType(oboe::ContentType::Music)
->setUsage(oboe::Usage::Game)
->setAudioApi(oboe::AudioApi::AAudio)
->setFramesPerCallback(oboe::kUnspecified)
->setBufferCapacityInFrames(oboe::kUnspecified)
->setChannelConversionAllowed(false)
->setFormatConversionAllowed(false)
->setCallback(this);

oboe::Result result = builder.openStream(stream_);

if (result != oboe::Result::OK) {
LOGE("AAudio open failed (%s), falling back to unspecified API",
oboe::convertToText(result));
builder.setAudioApi(oboe::AudioApi::Unspecified);
result = builder.openStream(stream_);
}

if (result != oboe::Result::OK) {
LOGE("Failed to open Oboe stream: %s", oboe::convertToText(result));
return false;
}

// Enable ADPF for dynamic performance management.
// This is only supported on AAudio streams on Android 12+ (API 31+).
// Oboe handles the internal version checks.
stream_->setPerformanceHintEnabled(true);

optimiseBufferSize();

LOGI("Oboe stream opened: api=%s, sampleRate=%d, framesPerBurst=%d, "
"bufferSize=%d, bufferCapacity=%d, sharingMode=%s, mmap=%s",
stream_->getAudioApi() == oboe::AudioApi::AAudio ? "AAudio" : "OpenSLES",
stream_->getSampleRate(),
stream_->getFramesPerBurst(),
stream_->getBufferSizeInFrames(),
stream_->getBufferCapacityInFrames(),
stream_->getSharingMode() == oboe::SharingMode::Exclusive ? "Exclusive" : "Shared",
oboe::OboeExtensions::isMMapUsed(stream_.get()) ? "yes" : "no");

return true;
}

void OboeBridge::optimiseBufferSize() {
if (!stream_) return;

// Set buffer size to exactly 1× burst for minimum latency.
// This gives the tightest possible callback schedule.
int32_t burst = stream_->getFramesPerBurst();

<<<<<<< SEARCH
if (burst > 0) {
auto setResult = stream_->setBufferSizeInFrames(burst);

if (setResult) {
LOGI("Buffer size tuned to %d frames (1x burst)", setResult.value());
}
}
}

bool OboeBridge::start() {
std::lock_guard<std::mutex> lock(streamLock_);

if (!stream_) {
LOGE("Cannot start: stream not opened");
return false;
}

oboe::Result result = stream_->requestStart();

if (result != oboe::Result::OK) {
LOGE("Failed to start Oboe stream: %s", oboe::convertToText(result));
return false;
}

active_.store(true);
LOGI("Oboe stream started");
return true;
}

void OboeBridge::stop() {
active_.store(false);

std::lock_guard<std::mutex> lock(streamLock_);

if (stream_) {
stream_->stop();
stream_->close();
stream_.reset();
}

latencyMs_.store(-1.0);
callbackCount_.store(0);
LOGI("Oboe stream stopped");
}

double OboeBridge::getOutputLatencyMs() const {
return latencyMs_.load();
}

bool OboeBridge::isActive() const {
return active_.load();
}

int32_t OboeBridge::getSampleRate() const {
std::lock_guard<std::mutex> lock(const_cast<std::mutex&>(streamLock_));
return stream_ ? stream_->getSampleRate() : 0;
}

int32_t OboeBridge::getFramesPerBurst() const {
std::lock_guard<std::mutex> lock(const_cast<std::mutex&>(streamLock_));
return stream_ ? stream_->getFramesPerBurst() : 0;
}

int32_t OboeBridge::getBufferSizeInFrames() const {
std::lock_guard<std::mutex> lock(const_cast<std::mutex&>(streamLock_));
return stream_ ? stream_->getBufferSizeInFrames() : 0;
}

bool OboeBridge::isAAudio() const {
std::lock_guard<std::mutex> lock(const_cast<std::mutex&>(streamLock_));
return stream_ && stream_->getAudioApi() == oboe::AudioApi::AAudio;
}

bool OboeBridge::isMMap() const {
std::lock_guard<std::mutex> lock(const_cast<std::mutex&>(streamLock_));
return stream_ && oboe::OboeExtensions::isMMapUsed(stream_.get());
}

void OboeBridge::setProvider(OboeAudioProvider provider) {
provider_.store(provider, std::memory_order_release);
}

oboe::DataCallbackResult OboeBridge::onAudioReady(
oboe::AudioStream* stream, void* audioData, int32_t numFrames) {

OboeAudioProvider provider = provider_.load(std::memory_order_acquire);

if (provider) {
int32_t framesRead = provider(audioData, numFrames);

if (framesRead < numFrames) {
// Fill remaining buffer with silence if provider didn't return enough data.
size_t bytesDone = static_cast<size_t>(framesRead) * stream->getChannelCount() * sizeof(float);
size_t totalBytes = static_cast<size_t>(numFrames) * stream->getChannelCount() * sizeof(float);
memset(static_cast<char*>(audioData) + bytesDone, 0, totalBytes - bytesDone);
}
} else {
// Fallback to silence if no provider is registered.
size_t byteCount = static_cast<size_t>(numFrames)
* static_cast<size_t>(stream->getChannelCount())
* sizeof(float);
memset(audioData, 0, byteCount);
}

// Sample latency every 128 callbacks (~250 ms at typical burst/sample rates)
// instead of every single callback. calculateLatencyMillis() issues a
// system call; keeping it out of the majority of callbacks reduces jitter
// in this real-time audio thread.
uint32_t count = callbackCount_.fetch_add(1, std::memory_order_relaxed);

if ((count & 127) == 0) {
updateLatency();
}

return oboe::DataCallbackResult::Continue;
}

void OboeBridge::onErrorBeforeClose(oboe::AudioStream* stream, oboe::Result error) {
LOGE("Oboe error before close: %s", oboe::convertToText(error));
active_.store(false);
}

void OboeBridge::onErrorAfterClose(oboe::AudioStream* stream, oboe::Result error) {
LOGE("Oboe error after close: %s — attempting automatic recovery",
oboe::convertToText(error));
active_.store(false);

// Automatic stream recovery: re-open and restart on disconnect / route change.
// This is critical for maintaining low-latency audio when headphones are
// plugged/unplugged or Bluetooth devices connect/disconnect.
if (error == oboe::Result::ErrorDisconnected) {
{
std::lock_guard<std::mutex> lock(streamLock_);
stream_.reset();
}

if (reopenAndRestart()) {
LOGI("Oboe stream recovered successfully after disconnect");
} else {
LOGE("Oboe stream recovery failed");
}
} else {
std::lock_guard<std::mutex> lock(streamLock_);
stream_.reset();
}
}

bool OboeBridge::reopenAndRestart() {
if (open()) {
std::lock_guard<std::mutex> lock(streamLock_);

if (stream_) {
oboe::Result result = stream_->requestStart();

if (result == oboe::Result::OK) {
active_.store(true);
return true;
}
=======
if (burst > 0) {
// Set buffer size to 2× burst for improved stability on Samsung and other devices.
// 1x burst is often too aggressive for managed code callbacks, causing underruns.
// 2x provides a safe jitter margin while still maintaining extremely low latency.
auto setResult = stream_->setBufferSizeInFrames(burst * 2);

LOGE("Failed to restart recovered stream: %s", oboe::convertToText(result));
if (setResult) {
LOGI("Buffer size tuned to %d frames (2x burst)", setResult.value());
}
}

return false;
}

void OboeBridge::updateLatency() {
if (!stream_) return;

auto result = stream_->calculateLatencyMillis();

if (result) {
latencyMs_.store(result.value());
}
}

// ============================================================
// C exports for P/Invoke from .NET
// ============================================================

#define OSU_EXPORT __attribute__((visibility("default")))

extern "C" {

OSU_EXPORT intptr_t nOboeCreate() {
auto* bridge = new (std::nothrow) OboeBridge();

if (!bridge) return 0;

if (!bridge->open()) {
delete bridge;
return 0;
}

return reinterpret_cast<intptr_t>(bridge);
}

OSU_EXPORT void nOboeDestroy(intptr_t ptr) {
if (ptr) delete reinterpret_cast<OboeBridge*>(ptr);
}

OSU_EXPORT unsigned char nOboeStart(intptr_t ptr) {
auto* bridge = reinterpret_cast<OboeBridge*>(ptr);
return (bridge && bridge->start()) ? 1 : 0;
}

OSU_EXPORT void nOboeStop(intptr_t ptr) {
auto* bridge = reinterpret_cast<OboeBridge*>(ptr);
if (bridge) bridge->stop();
}

OSU_EXPORT double nOboeGetLatencyMs(intptr_t ptr) {
auto* bridge = reinterpret_cast<OboeBridge*>(ptr);
return bridge ? bridge->getOutputLatencyMs() : -1.0;
}

OSU_EXPORT unsigned char nOboeIsActive(intptr_t ptr) {
auto* bridge = reinterpret_cast<OboeBridge*>(ptr);
return (bridge && bridge->isActive()) ? 1 : 0;
}

OSU_EXPORT int nOboeGetSampleRate(intptr_t ptr) {
auto* bridge = reinterpret_cast<OboeBridge*>(ptr);
return bridge ? bridge->getSampleRate() : 0;
}

OSU_EXPORT int nOboeGetFramesPerBurst(intptr_t ptr) {
auto* bridge = reinterpret_cast<OboeBridge*>(ptr);
return bridge ? bridge->getFramesPerBurst() : 0;
}

OSU_EXPORT int nOboeGetBufferSizeInFrames(intptr_t ptr) {
auto* bridge = reinterpret_cast<OboeBridge*>(ptr);
return bridge ? bridge->getBufferSizeInFrames() : 0;
}

OSU_EXPORT unsigned char nOboeIsAAudio(intptr_t ptr) {
auto* bridge = reinterpret_cast<OboeBridge*>(ptr);
return (bridge && bridge->isAAudio()) ? 1 : 0;
}

OSU_EXPORT unsigned char nOboeIsMMap(intptr_t ptr) {
auto* bridge = reinterpret_cast<OboeBridge*>(ptr);
return (bridge && bridge->isMMap()) ? 1 : 0;
}

OSU_EXPORT void nOboeSetProvider(intptr_t ptr, OboeAudioProvider provider) {
auto* bridge = reinterpret_cast<OboeBridge*>(ptr);
if (bridge) bridge->setProvider(provider);
}

} // extern "C"
>>>>>>> REPLACE
Loading
Loading