diff --git a/oboe_bridge_base.cpp b/oboe_bridge_base.cpp deleted file mode 100644 index 3c49bc205004..000000000000 --- a/oboe_bridge_base.cpp +++ /dev/null @@ -1,351 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -#include "oboe_bridge.h" -#include -#include -#include -#include - -#define LOG_TAG "osu!native" -#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 lock(streamLock_); - - // Request MMAP mode globally before opening the stream. - // MMAP provides a hardware-level DMA path that bypasses the kernel audio - // copy, shaving ~1-2 ms off the round-trip latency on supported devices. - oboe::OboeExtensions::setMMapEnabled(true); - - oboe::AudioStreamBuilder builder; - builder.setDirection(oboe::Direction::Output) - ->setPerformanceMode(oboe::PerformanceMode::LowLatency) - ->setSharingMode(oboe::SharingMode::Exclusive) - ->setFormat(oboe::AudioFormat::Float) - // Stereo output for high-quality game audio. - // Most Android devices use stereo as their native "Fast Path" configuration. - ->setChannelCount(oboe::ChannelCount::Stereo) - // Let Oboe pick the device's native sample rate. - // Hardcoding (e.g. 48000) would force Android's SRC resampler when the - // device native rate differs, adding measurable latency. - ->setSampleRate(oboe::kUnspecified) - // Explicitly forbid all internal conversions so that no resampler, - // channel mixer, or format converter sits in the audio path. - ->setChannelConversionAllowed(false) - ->setFormatConversionAllowed(false) - ->setSampleRateConversionQuality(oboe::SampleRateConversionQuality::None) - // Semantic hints help Android route through the optimal audio path. - ->setContentType(oboe::ContentType::Music) - ->setUsage(oboe::Usage::Game) - ->setCallback(this) - // Prefer AAudio for lowest latency (available on Android 8.1+). - // Falls back to OpenSL ES automatically on older devices. - ->setAudioApi(oboe::AudioApi::AAudio) - // Request minimum buffer for lowest latency. - // Oboe will clamp to the smallest safe value. - ->setFramesPerCallback(oboe::kUnspecified) - ->setBufferCapacityInFrames(oboe::kUnspecified); - - oboe::Result result = builder.openStream(stream_); - - if (result != oboe::Result::OK) { - // AAudio might not be available; retry without API preference. - LOGI("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; - } - - 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(); - - 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 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 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 lock(const_cast(streamLock_)); - return stream_ ? stream_->getSampleRate() : 0; -} - -int32_t OboeBridge::getFramesPerBurst() const { - std::lock_guard lock(const_cast(streamLock_)); - return stream_ ? stream_->getFramesPerBurst() : 0; -} - -int32_t OboeBridge::getBufferSizeInFrames() const { - std::lock_guard lock(const_cast(streamLock_)); - return stream_ ? stream_->getBufferSizeInFrames() : 0; -} - -bool OboeBridge::isAAudio() const { - std::lock_guard lock(const_cast(streamLock_)); - return stream_ && stream_->getAudioApi() == oboe::AudioApi::AAudio; -} - -bool OboeBridge::isMMap() const { - std::lock_guard lock(const_cast(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(framesRead) * stream->getChannelCount() * sizeof(float); - size_t totalBytes = static_cast(numFrames) * stream->getChannelCount() * sizeof(float); - memset(static_cast(audioData) + bytesDone, 0, totalBytes - bytesDone); - } - } else { - // Fallback to silence if no provider is registered. - size_t byteCount = static_cast(numFrames) - * static_cast(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. - if ((callbackCount_.fetch_add(1, std::memory_order_relaxed) & 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 lock(streamLock_); - stream_.reset(); - } - - if (reopenAndRestart()) { - LOGI("Oboe stream recovered successfully after disconnect"); - } else { - LOGE("Oboe stream recovery failed"); - } - } else { - std::lock_guard lock(streamLock_); - stream_.reset(); - } -} - -bool OboeBridge::reopenAndRestart() { - if (open()) { - std::lock_guard lock(streamLock_); - - if (stream_) { - oboe::Result result = stream_->requestStart(); - - if (result == oboe::Result::OK) { - active_.store(true); - return true; - } - - LOGE("Failed to restart recovered stream: %s", oboe::convertToText(result)); - } - } - - 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 -// ============================================================ - -// Use intptr_t for pointer handles so the size matches C# IntPtr on both -// 32-bit (4 bytes) and 64-bit (8 bytes) platforms. The previous use of -// C++ `long` was 4 bytes on 32-bit ARM/x86 but C# `long` is always -// 8 bytes, causing a calling-convention mismatch and crash. - -#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(bridge); -} - -OSU_EXPORT void nOboeDestroy(intptr_t ptr) { - if (ptr) delete reinterpret_cast(ptr); -} - -OSU_EXPORT unsigned char nOboeStart(intptr_t ptr) { - auto* bridge = reinterpret_cast(ptr); - return (bridge && bridge->start()) ? 1 : 0; -} - -OSU_EXPORT void nOboeStop(intptr_t ptr) { - auto* bridge = reinterpret_cast(ptr); - if (bridge) bridge->stop(); -} - -OSU_EXPORT double nOboeGetLatencyMs(intptr_t ptr) { - auto* bridge = reinterpret_cast(ptr); - return bridge ? bridge->getOutputLatencyMs() : -1.0; -} - -OSU_EXPORT unsigned char nOboeIsActive(intptr_t ptr) { - auto* bridge = reinterpret_cast(ptr); - return (bridge && bridge->isActive()) ? 1 : 0; -} - -OSU_EXPORT int nOboeGetSampleRate(intptr_t ptr) { - auto* bridge = reinterpret_cast(ptr); - return bridge ? bridge->getSampleRate() : 0; -} - -OSU_EXPORT int nOboeGetFramesPerBurst(intptr_t ptr) { - auto* bridge = reinterpret_cast(ptr); - return bridge ? bridge->getFramesPerBurst() : 0; -} - -OSU_EXPORT int nOboeGetBufferSizeInFrames(intptr_t ptr) { - auto* bridge = reinterpret_cast(ptr); - return bridge ? bridge->getBufferSizeInFrames() : 0; -} - -OSU_EXPORT unsigned char nOboeIsAAudio(intptr_t ptr) { - auto* bridge = reinterpret_cast(ptr); - return (bridge && bridge->isAAudio()) ? 1 : 0; -} - -OSU_EXPORT unsigned char nOboeIsMMap(intptr_t ptr) { - auto* bridge = reinterpret_cast(ptr); - return (bridge && bridge->isMMap()) ? 1 : 0; -} - -OSU_EXPORT void nOboeSetProvider(intptr_t ptr, OboeAudioProvider provider) { - auto* bridge = reinterpret_cast(ptr); - if (bridge) bridge->setProvider(provider); -} - -} // extern "C" diff --git a/oboe_bridge_fix_branch.cpp b/oboe_bridge_fix_branch.cpp deleted file mode 100644 index 6e1c4f23dbee..000000000000 --- a/oboe_bridge_fix_branch.cpp +++ /dev/null @@ -1,374 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -#include "oboe_bridge.h" -#include -#include -#include -#include - -#define LOG_TAG "osu!native" -#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 lock(streamLock_); - - // Request MMAP mode globally before opening the stream. - // MMAP provides a hardware-level DMA path that bypasses the kernel audio - // copy, shaving ~1-2 ms off the round-trip latency on supported devices. - oboe::OboeExtensions::setMMapEnabled(true); - - oboe::AudioStreamBuilder builder; - builder.setDirection(oboe::Direction::Output) - ->setPerformanceMode(oboe::PerformanceMode::LowLatency) - ->setSharingMode(oboe::SharingMode::Exclusive) - ->setFormat(oboe::AudioFormat::Float) - // Stereo output for high-quality game audio. - // Most Android devices use stereo as their native "Fast Path" configuration. - ->setChannelCount(oboe::ChannelCount::Stereo) - // Let Oboe pick the device's native sample rate. - // Hardcoding (e.g. 48000) would force Android's SRC resampler when the - // device native rate differs, adding measurable latency. - ->setSampleRate(oboe::kUnspecified) - // Explicitly forbid all internal conversions so that no resampler, - // channel mixer, or format converter sits in the audio path. - ->setChannelConversionAllowed(false) - ->setFormatConversionAllowed(false) - ->setSampleRateConversionQuality(oboe::SampleRateConversionQuality::None) - // Semantic hints help Android route through the optimal audio path. - ->setContentType(oboe::ContentType::Music) - ->setUsage(oboe::Usage::Game) - ->setCallback(this) - // Prefer AAudio for lowest latency (available on Android 8.1+). - // Falls back to OpenSL ES automatically on older devices. - ->setAudioApi(oboe::AudioApi::AAudio) - // Request minimum buffer for lowest latency. - // Oboe will clamp to the smallest safe value. - ->setFramesPerCallback(oboe::kUnspecified) - ->setBufferCapacityInFrames(oboe::kUnspecified); - - oboe::Result result = builder.openStream(stream_); - - if (result != oboe::Result::OK) { - // AAudio might not be available; retry without API preference. - LOGI("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; - } - - 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(); - - 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 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 lock(streamLock_); - - if (stream_) { - stream_->stop(); - stream_->close(); - stream_.reset(); - } - - affinitySet_.store(false); - 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 lock(const_cast(streamLock_)); - return stream_ ? stream_->getSampleRate() : 0; -} - -int32_t OboeBridge::getFramesPerBurst() const { - std::lock_guard lock(const_cast(streamLock_)); - return stream_ ? stream_->getFramesPerBurst() : 0; -} - -int32_t OboeBridge::getBufferSizeInFrames() const { - std::lock_guard lock(const_cast(streamLock_)); - return stream_ ? stream_->getBufferSizeInFrames() : 0; -} - -bool OboeBridge::isAAudio() const { - std::lock_guard lock(const_cast(streamLock_)); - return stream_ && stream_->getAudioApi() == oboe::AudioApi::AAudio; -} - -bool OboeBridge::isMMap() const { - std::lock_guard lock(const_cast(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(framesRead) * stream->getChannelCount() * sizeof(float); - size_t totalBytes = static_cast(numFrames) * stream->getChannelCount() * sizeof(float); - memset(static_cast(audioData) + bytesDone, 0, totalBytes - bytesDone); - } - } else { - // Fallback to silence if no provider is registered. - size_t byteCount = static_cast(numFrames) - * static_cast(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(); - - // Attempt to set CPU affinity to high-performance cores on the first few callbacks. - // Doing this inside the callback ensures we are targeting the actual audio thread - // created by Oboe/AAudio. - if (!affinitySet_.load(std::memory_order_relaxed)) { - std::vector exclusiveCores = oboe::Process::getExclusiveCores(); - - if (!exclusiveCores.empty()) { - oboe::Result result = oboe::Process::setThreadAffinity( - oboe::Process::getThreadId(), - exclusiveCores); - - if (result == oboe::Result::OK) { - LOGI("Oboe audio thread pinned to exclusive cores"); - } else { - LOGI("Failed to pin Oboe audio thread: %s", oboe::convertToText(result)); - } - } - affinitySet_.store(true); - } - } - - 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 lock(streamLock_); - stream_.reset(); - } - - if (reopenAndRestart()) { - LOGI("Oboe stream recovered successfully after disconnect"); - } else { - LOGE("Oboe stream recovery failed"); - } - } else { - std::lock_guard lock(streamLock_); - stream_.reset(); - } -} - -bool OboeBridge::reopenAndRestart() { - if (open()) { - std::lock_guard lock(streamLock_); - - if (stream_) { - oboe::Result result = stream_->requestStart(); - - if (result == oboe::Result::OK) { - active_.store(true); - return true; - } - - LOGE("Failed to restart recovered stream: %s", oboe::convertToText(result)); - } - } - - 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 -// ============================================================ - -// Use intptr_t for pointer handles so the size matches C# IntPtr on both -// 32-bit (4 bytes) and 64-bit (8 bytes) platforms. The previous use of -// C++ `long` was 4 bytes on 32-bit ARM/x86 but C# `long` is always -// 8 bytes, causing a calling-convention mismatch and crash. - -#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(bridge); -} - -OSU_EXPORT void nOboeDestroy(intptr_t ptr) { - if (ptr) delete reinterpret_cast(ptr); -} - -OSU_EXPORT unsigned char nOboeStart(intptr_t ptr) { - auto* bridge = reinterpret_cast(ptr); - return (bridge && bridge->start()) ? 1 : 0; -} - -OSU_EXPORT void nOboeStop(intptr_t ptr) { - auto* bridge = reinterpret_cast(ptr); - if (bridge) bridge->stop(); -} - -OSU_EXPORT double nOboeGetLatencyMs(intptr_t ptr) { - auto* bridge = reinterpret_cast(ptr); - return bridge ? bridge->getOutputLatencyMs() : -1.0; -} - -OSU_EXPORT unsigned char nOboeIsActive(intptr_t ptr) { - auto* bridge = reinterpret_cast(ptr); - return (bridge && bridge->isActive()) ? 1 : 0; -} - -OSU_EXPORT int nOboeGetSampleRate(intptr_t ptr) { - auto* bridge = reinterpret_cast(ptr); - return bridge ? bridge->getSampleRate() : 0; -} - -OSU_EXPORT int nOboeGetFramesPerBurst(intptr_t ptr) { - auto* bridge = reinterpret_cast(ptr); - return bridge ? bridge->getFramesPerBurst() : 0; -} - -OSU_EXPORT int nOboeGetBufferSizeInFrames(intptr_t ptr) { - auto* bridge = reinterpret_cast(ptr); - return bridge ? bridge->getBufferSizeInFrames() : 0; -} - -OSU_EXPORT unsigned char nOboeIsAAudio(intptr_t ptr) { - auto* bridge = reinterpret_cast(ptr); - return (bridge && bridge->isAAudio()) ? 1 : 0; -} - -OSU_EXPORT unsigned char nOboeIsMMap(intptr_t ptr) { - auto* bridge = reinterpret_cast(ptr); - return (bridge && bridge->isMMap()) ? 1 : 0; -} - -OSU_EXPORT void nOboeSetProvider(intptr_t ptr, OboeAudioProvider provider) { - auto* bridge = reinterpret_cast(ptr); - if (bridge) bridge->setProvider(provider); -} - -} // extern "C" diff --git a/oboe_bridge_fix_branch.h b/oboe_bridge_fix_branch.h deleted file mode 100644 index a1f42da4c68f..000000000000 --- a/oboe_bridge_fix_branch.h +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -#pragma once - -#include -#include -#include -#include - -/// Callback function type for providing PCM audio data to the Oboe stream. -/// Returns the number of frames actually written to the buffer. -typedef int32_t (*OboeAudioProvider)(void* audioData, int32_t numFrames); - -/// Low-latency audio bridge using Google's Oboe library. -/// Optimised for rhythm-game audio-visual synchronization with: -/// - AAudio preferred (lowest latency path on Android 8.1+) -/// - MMAP enabled (hardware-level DMA, bypasses kernel copy) -/// - Exclusive sharing mode (bypass system mixer) -/// - Mono output (minimum buffer for latency-measurement stream) -/// - Buffer size tuned to 1× burst for minimum latency -/// - All format/rate/channel conversions disabled (zero resampler overhead) -/// - Latency sampled every 128 callbacks (avoids syscall overhead in hot path) -/// - Automatic stream recovery on disconnect / route change -class OboeBridge : public oboe::AudioStreamCallback { -public: - OboeBridge(); - ~OboeBridge(); - - bool open(); - bool start(); - void stop(); - - /// Returns the measured output latency in milliseconds, or -1 if unavailable. - double getOutputLatencyMs() const; - - /// Returns true if the stream is currently active. - bool isActive() const; - - /// Returns the negotiated sample rate of the open stream (e.g. 48000). - int32_t getSampleRate() const; - - /// Returns the optimal burst size in frames (one callback quantum). - int32_t getFramesPerBurst() const; - - /// Returns the current buffer size in frames (ideally == framesPerBurst for lowest latency). - int32_t getBufferSizeInFrames() const; - - /// Returns true if the stream is using AAudio (vs OpenSL ES fallback). - bool isAAudio() const; - - /// Returns true if the stream is using the hardware MMAP path (lowest possible latency). - /// MMAP provides direct memory-mapped access to audio hardware buffers. - bool isMMap() const; - - /// Sets the provider function that will be called to fill the audio buffer. - void setProvider(OboeAudioProvider provider); - - // oboe::AudioStreamCallback - oboe::DataCallbackResult onAudioReady( - oboe::AudioStream* stream, void* audioData, int32_t numFrames) override; - - void onErrorBeforeClose(oboe::AudioStream* stream, oboe::Result error) override; - void onErrorAfterClose(oboe::AudioStream* stream, oboe::Result error) override; - -private: - std::shared_ptr stream_; - std::mutex streamLock_; - std::atomic active_{false}; - std::atomic affinitySet_{false}; - std::atomic latencyMs_{-1.0}; - std::atomic callbackCount_{0}; - std::atomic provider_{nullptr}; - - void updateLatency(); - void optimiseBufferSize(); - bool reopenAndRestart(); -}; diff --git a/oboe_bridge_perf.patch b/oboe_bridge_perf.patch deleted file mode 100644 index 91828d65694b..000000000000 --- a/oboe_bridge_perf.patch +++ /dev/null @@ -1,13 +0,0 @@ ---- osu.Android/Native/oboe_bridge.cpp -+++ osu.Android/Native/oboe_bridge.cpp -@@ -212,8 +212,8 @@ - // S23 Ultra (Snapdragon 8 Gen 2) layout: 1 Prime + 2 Gold + 2 Gold + 3 Silver. - // Indices are typically: 0-2 (Silver), 3-4 (Gold), 5-6 (Gold), 7 (Prime). -- // We want to target the Prime (7) and Gold (3-6) cores. -+ // We want to target the Prime (7) and Gold (3-6) cores. - if (num_cores >= 8) { -- for (int i = 4; i < num_cores; ++i) { -+ for (int i = 3; i < num_cores; ++i) { - CPU_SET(i, &cpuset); - } - } else { diff --git a/oboe_redirector_final.patch b/oboe_redirector_final.patch deleted file mode 100644 index 54b263d9152f..000000000000 --- a/oboe_redirector_final.patch +++ /dev/null @@ -1,40 +0,0 @@ ---- osu.Android/OboeAudioRedirector.cs -+++ osu.Android/OboeAudioRedirector.cs -@@ -45,15 +45,11 @@ -- if (mixerHandles.Count > 0) -- { -- silenceDefaultAudio(); -- setupMasterMixer(); -- } -- else -- { -- Debug.WriteLine("[osu!] Oboe redirector: ABORTED redirection - no mixer handles found via reflection. Audio will use default path."); -- restoreDefaultAudio(); -- } -+ // User requested Oboe: we MUST use Oboe. -+ // Silence the default device immediately to prevent duplicated audio. -+ silenceDefaultAudio(); -+ setupMasterMixer(); - - ActiveMasterMixer = masterMixer; - -- Debug.WriteLine($"[osu!] Oboe redirector initialized: rate={sampleRate}Hz, mixers={mixerHandles.Count}, master={masterMixer}"); -+ if (mixerHandles.Count == 0) -+ Debug.WriteLine("[osu!] Oboe redirector: CRITICAL WARNING - no mixer handles found via reflection. Audio WILL BE SILENT until fixed."); -+ else -+ Debug.WriteLine($"[osu!] Oboe redirector initialized: rate={sampleRate}Hz, mixers={mixerHandles.Count}, master={masterMixer}"); - } - -@@ -102,6 +98,12 @@ - return; - } - -+ // Disable BASS-internal buffering for the master mixer. -+ // This ensures BASS renders as fast as possible when we call ChannelGetData. -+ // This is key for "lowest possible latency" as requested. -+ if (!Bass.ChannelSetAttribute(masterMixer, ChannelAttribute.Buffer, 0)) -+ Debug.WriteLine($"[osu!] Failed to disable BASS buffering on master mixer: {Bass.LastError}"); -+ - foreach (int handle in mixerHandles) - { - // Add redirected mixers as sources to our master mixer. diff --git a/oboe_redirector_patch.diff b/oboe_redirector_patch.diff deleted file mode 100644 index e39d300e5b0e..000000000000 --- a/oboe_redirector_patch.diff +++ /dev/null @@ -1,118 +0,0 @@ ---- osu.Android/OboeAudioRedirector.cs -+++ osu.Android/OboeAudioRedirector.cs -@@ -34,11 +34,19 @@ - mixerHandles.Clear(); - addMixer(audioManager.TrackMixer); - addMixer(audioManager.SampleMixer); - -- silenceDefaultAudio(); -- setupMasterMixer(); -+ if (mixerHandles.Count > 0) -+ { -+ silenceDefaultAudio(); -+ setupMasterMixer(); -+ } -+ else -+ { -+ Debug.WriteLine("[osu!] Oboe redirector: ABORTED redirection - no mixer handles found. Reverting to default audio path."); -+ restoreDefaultAudio(); -+ } - - ActiveMasterMixer = masterMixer; - -- Debug.WriteLine($"[osu!] Oboe redirector initialized: rate={sampleRate}Hz, mixers={mixerHandles.Count}"); -+ Debug.WriteLine($"[osu!] Oboe redirector initialized: rate={sampleRate}Hz, mixers={mixerHandles.Count}, master={masterMixer}"); - } - -@@ -140,11 +148,15 @@ - - try - { -- object? handleObj = mixer.GetType().GetField("mixerHandle", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)?.GetValue(mixer) -- ?? mixer.GetType().GetField("Handle", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)?.GetValue(mixer) -- ?? mixer.GetType().GetProperty("Handle", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)?.GetValue(mixer); -- -- if (handleObj == null) return; -+ int handle = findHandle(mixer); -+ -+ if (handle != 0) -+ { -+ if (!mixerHandles.Contains(handle)) -+ { -+ mixerHandles.Add(handle); -+ Debug.WriteLine($"[osu!] Oboe redirector: added mixer handle {handle} for {mixer.GetType().Name}"); -+ } -+ } -+ else -+ { -+ Debug.WriteLine($"[osu!] Oboe redirector: WARNING - could not find BASS handle for {mixer.GetType().Name} via reflection. Audio may be muted!"); -+ } -+ } -+ catch (Exception e) -+ { -+ Debug.WriteLine($"[osu!] Oboe redirector: failed to get mixer handle via reflection: {e.Message}"); -+ } -+ } -+ -+ private int findHandle(object obj) -+ { -+ Type? type = obj.GetType(); -+ -+ while (type != null && type != typeof(object)) -+ { -+ // Try common explicit names first (fast path) -+ foreach (string name in new[] { "mixerHandle", "handle", "Handle", "mixer_handle", "_handle", "m_handle" }) -+ { -+ var field = type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); -+ if (field != null) -+ { -+ int h = convertHandle(field.GetValue(obj)); -+ if (h != 0) return h; -+ } -+ -+ var prop = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); -+ if (prop != null) -+ { -+ int h = convertHandle(prop.GetValue(obj)); -+ if (h != 0) return h; -+ } -+ } -+ -+ // Scan all fields for anything that looks like a handle as a last resort -+ foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) -+ { -+ if (field.Name.Contains("handle", StringComparison.OrdinalIgnoreCase)) -+ { -+ int h = convertHandle(field.GetValue(obj)); -+ if (h != 0) return h; -+ } -+ } -+ -+ type = type.BaseType; -+ } -+ -+ return 0; -+ } - -- int handle = 0; -- if (handleObj is int ih) handle = ih; -- else if (handleObj is long lh) handle = (int)lh; -- else if (handleObj is IntPtr ph) handle = (int)ph.ToInt64(); -- -- if (handle != 0 && !mixerHandles.Contains(handle)) -- mixerHandles.Add(handle); -- } -- catch (Exception e) -- { -- Debug.WriteLine($"[osu!] Failed to get mixer handle via reflection: {e.Message}"); -- } -+ private int convertHandle(object? val) -+ { -+ if (val == null) return 0; -+ if (val is int ih) return ih; -+ if (val is long lh) return (int)lh; -+ if (val is IntPtr ph) return (int)ph.ToInt64(); -+ return 0; - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] diff --git a/osu.Android/OboeAudioRedirector.cs b/osu.Android/OboeAudioRedirector.cs index 2290bdf6c62c..145da4f5d9e3 100644 --- a/osu.Android/OboeAudioRedirector.cs +++ b/osu.Android/OboeAudioRedirector.cs @@ -3,26 +3,28 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using ManagedBass; using ManagedBass.Mix; -using osu.Android.Native; using osu.Framework.Audio; using osu.Framework.Audio.Mixing; -using System.Runtime.CompilerServices; -using Debug = System.Diagnostics.Debug; namespace osu.Android { /// - /// Redirects audio from the framework's BASS mixers into the Oboe bridge. - /// Optimized for zero-copy delivery and hardware sample rate synchronization. + /// A bridge between BASS and Oboe that redirects mixed PCM audio from BASS mixers + /// into an Oboe/AAudio stream for low-latency output on Android. /// - internal sealed class OboeAudioRedirector : IDisposable + public class OboeAudioRedirector : IDisposable { private readonly AudioManager audioManager; private readonly List mixerHandles = new List(); + private readonly Dictionary originalParents = new Dictionary(); + private int masterMixer; private bool devicesSilenced; private int sampleRate = 44100; // Default, will be updated from bridge. @@ -36,11 +38,26 @@ public OboeAudioRedirector(AudioManager audioManager) public void RefreshMixers(int hardwareSampleRate) { + // Ensure we are in a clean state before re-initialising. + // This restores any previous hijacks if Oboe is being toggled or refreshed. + restoreDefaultAudio(); + sampleRate = hardwareSampleRate > 0 ? hardwareSampleRate : 44100; mixerHandles.Clear(); - addMixer(audioManager.TrackMixer); - addMixer(audioManager.SampleMixer); + + // Try to find the root mixer of the framework. + // By capturing the root, we get UI sounds, music, and SFX in one go, + // and we bypass the framework's final output stages for even lower latency. + addRootMixer(audioManager.TrackMixer); + addRootMixer(audioManager.SampleMixer); + + // If we failed to find a shared root, fallback to individual mixers. + if (mixerHandles.Count == 0) + { + addMixer(audioManager.TrackMixer); + addMixer(audioManager.SampleMixer); + } // User requested Low-Latency Oboe: we MUST use Oboe. // Silence the default device immediately to prevent duplicated audio. @@ -84,6 +101,17 @@ private void setupMasterMixer() foreach (int handle in mixerHandles) { + // BASS only allows a channel to have one parent mixer at a time. + // The framework's mixers are already attached to a master mixer, so we MUST hijack them. + int parent = BassMix.ChannelGetMixer(handle); + + if (parent != 0) + { + originalParents[handle] = parent; + if (!BassMix.MixerRemoveChannel(handle)) + Debug.WriteLine($"[osu!] Failed to hijack mixer {handle} from parent {parent}: {Bass.LastError}"); + } + // Add redirected mixers as sources to our master mixer. // We remove BASS_MIXER_BUFFER to eliminate internal BASS buffering latency, // relying entirely on the Oboe callback timing for rock-solid sync. @@ -135,15 +163,6 @@ private void silenceDefaultAudio() private void restoreDefaultAudio() { ActiveMasterMixer = 0; - if (!devicesSilenced) - { - if (masterMixer != 0) - { - Bass.StreamFree(masterMixer); - masterMixer = 0; - } - return; - } try { @@ -155,12 +174,25 @@ private void restoreDefaultAudio() foreach (int handle in mixerHandles) { + // Unplug from our Oboe master mixer. + BassMix.MixerRemoveChannel(handle); + + // Restore to framework's original parent mixer if we hijacked it. + if (originalParents.TryGetValue(handle, out int parent)) + { + if (BassMix.MixerAddChannel(parent, handle, BassFlags.MixerChanNoRampin)) + Debug.WriteLine($"[osu!] Restored mixer {handle} to framework parent {parent}"); + else + Debug.WriteLine($"[osu!] Failed to restore mixer {handle} to framework parent {parent}: {Bass.LastError}"); + } + if (!Bass.ChannelSetDevice(handle, 1)) Debug.WriteLine($"[osu!] Failed to restore mixer {handle} to default device: {Bass.LastError}"); } + originalParents.Clear(); devicesSilenced = false; - Debug.WriteLine($"[osu!] BASS mixers restored to default device 1"); + Debug.WriteLine($"[osu!] BASS mixers restored to default device 1 and framework parents"); } catch (Exception e) { @@ -168,6 +200,28 @@ private void restoreDefaultAudio() } } + private void addRootMixer(AudioMixer? mixer) + { + if (mixer == null) return; + + int handle = findHandle(mixer); + if (handle == 0) return; + + // Walk up the mixer tree using BASS calls directly to find the absolute root. + // This is safer than reflection because it queries the actual BASS engine state. + int current = handle; + int parent; + + while ((parent = BassMix.ChannelGetMixer(current)) != 0) + current = parent; + + if (!mixerHandles.Contains(current)) + { + mixerHandles.Add(current); + Debug.WriteLine($"[osu!] Oboe redirector: discovered root mixer {current} from source {handle}"); + } + } + private void addMixer(AudioMixer? mixer) { if (mixer == null) return; diff --git a/osu.Android/OboeAudioRedirector.cs.orig b/osu.Android/OboeAudioRedirector.cs.orig deleted file mode 100644 index 07dfa3c17b95..000000000000 --- a/osu.Android/OboeAudioRedirector.cs.orig +++ /dev/null @@ -1,205 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Runtime.InteropServices; -using ManagedBass; -using ManagedBass.Mix; -using osu.Android.Native; -using osu.Framework.Audio; -using osu.Framework.Audio.Mixing; -using System.Runtime.CompilerServices; -using Debug = System.Diagnostics.Debug; - -namespace osu.Android -{ - /// - /// Redirects audio from the framework's BASS mixers into the Oboe bridge. - /// Optimized for zero-copy delivery and hardware sample rate synchronization. - /// - internal sealed class OboeAudioRedirector : IDisposable - { - private readonly AudioManager audioManager; - private readonly List mixerHandles = new List(); - private int masterMixer; - private bool devicesSilenced; - private int sampleRate = 44100; // Default, will be updated from bridge. - - public OboeAudioRedirector(AudioManager audioManager) - { - this.audioManager = audioManager; - } - - public unsafe IntPtr Provider => (IntPtr)(delegate* unmanaged[Cdecl])&provideAudio; - - public void RefreshMixers(int hardwareSampleRate) - { - sampleRate = hardwareSampleRate > 0 ? hardwareSampleRate : 44100; - - mixerHandles.Clear(); - addMixer(audioManager.TrackMixer); - addMixer(audioManager.SampleMixer); - - silenceDefaultAudio(); - setupMasterMixer(); - - ActiveMasterMixer = masterMixer; - - Debug.WriteLine($"[osu!] Oboe redirector initialized: rate={sampleRate}Hz, mixers={mixerHandles.Count}"); - } - - private void setupMasterMixer() - { - if (masterMixer != 0) - { - Bass.StreamFree(masterMixer); - masterMixer = 0; - } - - if (mixerHandles.Count == 0 || !devicesSilenced) return; - - // Create a BASS master mixer that matches the Oboe hardware format (Stereo Float). - // We use BASS_MIXER_NONSTOP to ensure the mixer doesn't stall if sources are empty. - // BASS_STREAM_DECODE means we pull data manually via ChannelGetData. - masterMixer = BassMix.CreateMixerStream(sampleRate, 2, BassFlags.Float | BassFlags.Decode | BassFlags.MixerNonStop); - - if (masterMixer == 0) - { - Debug.WriteLine($"[osu!] Failed to create BASS master mixer: {Bass.LastError}"); - return; - } - - foreach (int handle in mixerHandles) - { - // Add redirected mixers as sources to our master mixer. - // We remove BASS_MIXER_BUFFER to eliminate internal BASS buffering latency, - // relying entirely on the Oboe callback timing for rock-solid sync. - if (!BassMix.MixerAddChannel(masterMixer, handle, BassFlags.MixerChanNoRampin)) - { - Debug.WriteLine($"[osu!] Failed to add mixer {handle} to master mixer: {Bass.LastError}"); - } - } - - // Move the master mixer to the silent device too. - Bass.ChannelSetDevice(masterMixer, 0); - } - - private void silenceDefaultAudio() - { - try - { - // Initialize BASS "No Sound" device (0) with the hardware sample rate. - // This minimizes resampling overhead within BASS. - if (!Bass.Init(0, sampleRate) && Bass.LastError != Errors.Already) - { - Debug.WriteLine($"[osu!] Failed to initialize BASS No Sound device: {Bass.LastError}"); - return; - } - - bool allSuccess = true; - - // Move all redirected mixers to the silent device. - foreach (int handle in mixerHandles) - { - if (!Bass.ChannelSetDevice(handle, 0)) - { - Debug.WriteLine($"[osu!] Failed to move mixer {handle} to silent device: {Bass.LastError}"); - allSuccess = false; - } - } - - devicesSilenced = allSuccess; - - if (allSuccess && mixerHandles.Count > 0) - Debug.WriteLine($"[osu!] BASS mixers ({mixerHandles.Count}) moved to silent device 0 (Oboe active)"); - } - catch (Exception e) - { - Debug.WriteLine($"[osu!] Failed to silence default audio: {e.Message}"); - } - } - - private void restoreDefaultAudio() - { - ActiveMasterMixer = 0; - if (!devicesSilenced) return; - - try - { - if (masterMixer != 0) - { - Bass.StreamFree(masterMixer); - masterMixer = 0; - } - - foreach (int handle in mixerHandles) - { - if (!Bass.ChannelSetDevice(handle, 1)) - Debug.WriteLine($"[osu!] Failed to restore mixer {handle} to default device: {Bass.LastError}"); - } - - devicesSilenced = false; - Debug.WriteLine($"[osu!] BASS mixers restored to default device 1"); - } - catch (Exception e) - { - Debug.WriteLine($"[osu!] Failed to restore default audio: {e.Message}"); - } - } - - private void addMixer(AudioMixer? mixer) - { - if (mixer == null) return; - - try - { - object? handleObj = mixer.GetType().GetField("mixerHandle", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)?.GetValue(mixer) - ?? mixer.GetType().GetField("Handle", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)?.GetValue(mixer) - ?? mixer.GetType().GetProperty("Handle", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)?.GetValue(mixer); - - if (handleObj == null) return; - - int handle = 0; - if (handleObj is int ih) handle = ih; - else if (handleObj is long lh) handle = (int)lh; - else if (handleObj is IntPtr ph) handle = (int)ph.ToInt64(); - - if (handle != 0 && !mixerHandles.Contains(handle)) - mixerHandles.Add(handle); - } - catch (Exception e) - { - Debug.WriteLine($"[osu!] Failed to get mixer handle via reflection: {e.Message}"); - } - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static int provideAudio(IntPtr audioData, int numFrames) - { - // We use a static method with [UnmanagedCallersOnly] to eliminate delegate marshalling overhead. - // Since this is static, we need a way to find the active mixer. - int mixer = ActiveMasterMixer; - - if (mixer == 0) return 0; - - // Zero-copy: Tell BASS to render directly into the memory provided by Oboe. - // BASS_DATA_FLOAT is implied by the mixer stream flags. - int bytesToRead = numFrames * 8; // 2 channels * 4 bytes/sample - int bytesRead = Bass.ChannelGetData(mixer, audioData, bytesToRead); - - if (bytesRead <= 0) return 0; - - return bytesRead / 8; - } - - internal static int ActiveMasterMixer; - - public void Dispose() - { - restoreDefaultAudio(); - mixerHandles.Clear(); - } - } -} diff --git a/osu.Android/OboeAudioRedirector.cs.rej b/osu.Android/OboeAudioRedirector.cs.rej deleted file mode 100644 index 3a7b65df3d38..000000000000 --- a/osu.Android/OboeAudioRedirector.cs.rej +++ /dev/null @@ -1,25 +0,0 @@ ---- OboeAudioRedirector.cs -+++ OboeAudioRedirector.cs -@@ -34,11 +34,19 @@ - mixerHandles.Clear(); - addMixer(audioManager.TrackMixer); - addMixer(audioManager.SampleMixer); - -- silenceDefaultAudio(); -- setupMasterMixer(); -+ if (mixerHandles.Count > 0) -+ { -+ silenceDefaultAudio(); -+ setupMasterMixer(); -+ } -+ else -+ { -+ Debug.WriteLine("[osu!] Oboe redirector: ABORTED redirection - no mixer handles found. Reverting to default audio path."); -+ restoreDefaultAudio(); -+ } - - ActiveMasterMixer = masterMixer; - -- Debug.WriteLine($"[osu!] Oboe redirector initialized: rate={sampleRate}Hz, mixers={mixerHandles.Count}"); -+ Debug.WriteLine($"[osu!] Oboe redirector initialized: rate={sampleRate}Hz, mixers={mixerHandles.Count}, master={masterMixer}"); - }