@@ -34,6 +34,7 @@ public partial class OsuGameAndroid : OsuGame
3434 private readonly Bindable < bool > performanceMode = new Bindable < bool > ( ) ;
3535 private readonly Bindable < bool > lowLatencyAudio = new Bindable < bool > ( ) ;
3636 private readonly Bindable < bool > vulkanProbeEnabled = new Bindable < bool > ( ) ;
37+ private readonly BindableDouble audioOffset = new BindableDouble ( ) ;
3738
3839 private OboeAudioBridge ? oboeBridge ;
3940 private VulkanProbe ? vulkanProbe ;
@@ -64,6 +65,7 @@ private void load(OsuConfigManager config)
6465 config . BindWith ( OsuSetting . AndroidPerformanceMode , performanceMode ) ;
6566 config . BindWith ( OsuSetting . AndroidLowLatencyAudio , lowLatencyAudio ) ;
6667 config . BindWith ( OsuSetting . AndroidVulkanProbe , vulkanProbeEnabled ) ;
68+ config . BindWith ( OsuSetting . AudioOffset , audioOffset ) ;
6769 }
6870
6971 protected override void LoadComplete ( )
@@ -114,7 +116,13 @@ private void startOboeBridge()
114116
115117 if ( started )
116118 {
119+ // Log basic stream info immediately.
117120 logOboeInfo ( ) ;
121+
122+ // Latency is measured asynchronously by the audio callback.
123+ // Schedule a check after a short warm-up period to get a stable reading
124+ // and apply the auto-suggested audio offset if appropriate.
125+ Scheduler . AddDelayed ( applyMeasuredLatencyOffset , 2000 ) ;
118126 }
119127 else
120128 {
@@ -173,6 +181,7 @@ private void logVulkanInfo()
173181 Debug . WriteLine ( $ "[osu!] Vulkan GPU: available={ vulkanProbe . IsAvailable } , "
174182 + $ "API={ major } .{ minor } .{ patch } , "
175183 + $ "swapchain={ vulkanProbe . SupportsSwapchain } , "
184+ + $ "mailbox={ vulkanProbe . SupportsMailboxPresentMode } , "
176185 + $ "VRAM={ vulkanProbe . DeviceLocalMemoryMB } MB, "
177186 + $ "queueFamilies={ vulkanProbe . QueueFamilyCount } , "
178187 + $ "dedicatedCompute={ vulkanProbe . HasDedicatedComputeQueue } , "
@@ -187,8 +196,34 @@ private void logOboeInfo()
187196 + $ "api={ ( oboeBridge . IsAAudio ? "AAudio" : "OpenSLES" ) } , "
188197 + $ "sampleRate={ oboeBridge . SampleRate } Hz, "
189198 + $ "burst={ oboeBridge . FramesPerBurst } frames, "
190- + $ "bufferSize={ oboeBridge . BufferSizeInFrames } frames, "
191- + $ "latency={ oboeBridge . GetOutputLatencyMs ( ) : F1} ms") ;
199+ + $ "bufferSize={ oboeBridge . BufferSizeInFrames } frames") ;
200+ }
201+
202+ /// <summary>
203+ /// Called after a warm-up delay to read the stable measured latency and apply it
204+ /// as an auto-suggested audio offset when the user hasn't set a manual value.
205+ /// </summary>
206+ private void applyMeasuredLatencyOffset ( )
207+ {
208+ if ( oboeBridge == null ) return ;
209+
210+ double latency = oboeBridge . GetOutputLatencyMs ( ) ;
211+
212+ Debug . WriteLine ( $ "[osu!] Oboe measured latency after warm-up: { latency : F1} ms") ;
213+
214+ if ( latency <= 0 )
215+ return ;
216+
217+ // Only auto-suggest when the user hasn't already configured a manual offset.
218+ // Use a small epsilon to safely compare against the default value of 0.
219+ if ( Math . Abs ( audioOffset . Value ) >= 0.01 )
220+ return ;
221+
222+ // The audio offset compensates for hardware output delay: if audio arrives
223+ // 20 ms late, we need to set the offset to -20 ms so osu! plays notes earlier.
224+ double suggested = Math . Clamp ( - latency , audioOffset . MinValue , audioOffset . MaxValue ) ;
225+ audioOffset . Value = suggested ;
226+ Debug . WriteLine ( $ "[osu!] Audio offset auto-suggested: { suggested : F1} ms (hardware latency={ latency : F1} ms)") ;
192227 }
193228
194229 /// <summary>
0 commit comments