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
25 changes: 24 additions & 1 deletion osu.Android/Native/oboe_bridge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ void OboeBridge::stop() {
stream_.reset();
}

affinitySet_.store(false);
latencyMs_.store(-1.0);
callbackCount_.store(0);
LOGI("Oboe stream stopped");
Expand Down Expand Up @@ -202,8 +203,30 @@ oboe::DataCallbackResult OboeBridge::onAudioReady(
// 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) {
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<int> 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;
Expand Down
1 change: 1 addition & 0 deletions osu.Android/Native/oboe_bridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class OboeBridge : public oboe::AudioStreamCallback {
std::shared_ptr<oboe::AudioStream> stream_;
std::mutex streamLock_;
std::atomic<bool> active_{false};
std::atomic<bool> affinitySet_{false};
std::atomic<double> latencyMs_{-1.0};
std::atomic<uint32_t> callbackCount_{0};
std::atomic<OboeAudioProvider> provider_{nullptr};
Expand Down
79 changes: 76 additions & 3 deletions osu.Android/OboeAudioRedirector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.Numerics;
using System.Reflection;
using System.Runtime.InteropServices;
using ManagedBass;
Expand All @@ -20,6 +21,7 @@ internal sealed class OboeAudioRedirector : IDisposable
{
private readonly AudioManager audioManager;
private readonly List<int> mixerHandles = new List<int>();
private bool devicesSilenced;
private readonly OboeAudioBridge.OboeAudioProvider providerDelegate;

private float[]? mixBuffer;
Expand All @@ -40,9 +42,65 @@ public void RefreshMixers()
addMixer(audioManager.TrackMixer);
addMixer(audioManager.SampleMixer);

silenceDefaultAudio();

Debug.WriteLine($"[osu!] Oboe redirector initialized with {mixerHandles.Count} BASS mixers");
}

private void silenceDefaultAudio()
{
if (devicesSilenced) return;

try
{
// Initialize BASS "No Sound" device (0) if not already.
// This device allows BASS to process audio streams without outputting to hardware.
if (!Bass.Init(0) && Bass.LastError != Errors.Already)
{
Debug.WriteLine($"[osu!] Failed to initialize BASS No Sound device: {Bass.LastError}");
return;
}

// Move all redirected mixers to the silent device.
// This "unplugs" them from the system hardware while keeping them active so we can pull data.
foreach (int handle in mixerHandles)
{
if (!Bass.ChannelSetDevice(handle, 0))
Debug.WriteLine($"[osu!] Failed to move mixer {handle} to silent device: {Bass.LastError}");
}

devicesSilenced = true;
Debug.WriteLine($"[osu!] BASS mixers moved to silent device 0 (Oboe active)");
}
catch (Exception e)
{
Debug.WriteLine($"[osu!] Failed to silence default audio: {e.Message}");
}
}

private void restoreDefaultAudio()
{
if (!devicesSilenced) return;

try
{
// Move mixers back to the default device (usually 1 on Android).
foreach (int handle in mixerHandles)
{
// On Android, Device 1 is typically the default output.
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;
Expand All @@ -55,7 +113,7 @@ private void addMixer(AudioMixer mixer)

if (field != null)
{
int handle = (int)field.GetValue(mixer);
int handle = field.GetValue(mixer) is int h ? h : 0;
if (handle != 0) mixerHandles.Add(handle);
}
else
Expand All @@ -64,7 +122,7 @@ private void addMixer(AudioMixer mixer)
var prop = mixer.GetType().GetProperty("Handle", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
if (prop != null)
{
int handle = (int)prop.GetValue(mixer);
int handle = prop.GetValue(mixer) is int h ? h : 0;
if (handle != 0) mixerHandles.Add(handle);
}
}
Expand Down Expand Up @@ -100,7 +158,21 @@ private int provideAudio(IntPtr audioData, int numFrames)
anyRead = true;
int samplesRead = bytesRead / 4;

for (int i = 0; i < samplesRead; i++)
int i = 0;

if (Vector.IsHardwareAccelerated)
{
int vectorSize = Vector<float>.Count;

for (; i <= samplesRead - vectorSize; i += vectorSize)
{
var vMix = new Vector<float>(mixBuffer, i);
var vChan = new Vector<float>(channelBuffer, i);
(vMix + vChan).CopyTo(mixBuffer, i);
}
}

for (; i < samplesRead; i++)
{
mixBuffer[i] += channelBuffer[i];
}
Expand All @@ -114,6 +186,7 @@ private int provideAudio(IntPtr audioData, int numFrames)

public void Dispose()
{
restoreDefaultAudio();
mixerHandles.Clear();
}
}
Expand Down
4 changes: 4 additions & 0 deletions osu.Android/OsuGameAndroid.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,11 @@ protected override void LoadComplete()
}, audioRedirector?.Provider);
}
else if (nativeBridges != null)
{
stopOboeBridge();
audioRedirector?.Dispose();
audioRedirector = new OboeAudioRedirector(Audio);
}
}
catch (Exception ex)
{
Expand Down
Loading