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
2 changes: 1 addition & 1 deletion osu.Android.props
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.506.2" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.506.4" />
<!-- `ppy.osu.Framework.NativeLibs` is a transitive dependency of `ppy.osu.Framework`
that ships desktop-only natives (Linux/macOS/Windows) under `runtimes/<rid>/native/`
— including a bare Linux `libbass.so`/`libbass_fx.so`/`libbassmix.so` for linux-arm64.
Expand Down
52 changes: 13 additions & 39 deletions osu.Android/OsuGameAndroid.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1969,49 +1969,23 @@ public override void ResyncHardwareAudioOffset()
// button click. Save the current offset first so the user can undo, then apply.
LocalConfig.SetValue(OsuSetting.AndroidPreviousHardwareAudioOffset, audioOffset.Value);

// Cap the applied compensation to the typical MMAP output-latency ceiling.
// Apply the full measured hardware pipeline latency as a negative AudioOffset.
//
// DESIGN CONSTRAINT — why we cap here:
// AudioOffset shifts the gameplay clock (visual timing) relative to the audio
// track. Both music and hitsound samples travel through the same
// BASS → OboeAudioRedirector → Oboe → speaker pipeline, so they both experience
// the same hardware latency (hw_latency).
//
// AudioOffset shifts the gameplay clock (visual timing) but NOT the BASS
// track's playback position. Both the music track and hitsound samples go
// through BASS → OboeAudioRedirector → Oboe → speaker, so they share the
// same pipeline latency (hw_latency). A non-zero AudioOffset shifts WHEN
// the user's input arrives in the BASS timeline relative to the music beat:
//
// Beat at BASS position H, hw_latency = L:
// AudioOffset = 0 → input at BASS H → hitsound heard at H+L = music ✓
// AudioOffset = -L → input at BASS H+L → hitsound heard at H+2L, music at H+L → lag = L ✗
//
// In other words, every ms of negative AudioOffset creates exactly 1 ms of
// hitsound-after-music lag. AudioOffset = 0 gives perfect hitsound-music sync.
//
// On MMAP-capable devices (which includes the Galaxy S23/S24 series and most
// modern Snapdragon/Exynos handsets), Oboe achieves 4–8 ms output latency, so
// applying the full measured value creates ≤8 ms of desync — below the human
// JND of ~20 ms, and imperceptible in practice.
//
// On Legacy AAudio / OpenSL ES devices, or devices whose Samsung audio DSP
// reports high pipeline depth, the measured latency can be 30–80 ms. Applying
// -30 ms as AudioOffset causes hitsounds to arrive 30 ms after each music beat,
// which is very audible. We therefore cap the compensation at
// max_hw_compensation_ms so the hitsound-music gap is bounded to that value on
// ALL devices. Users who want to tune the visual-audio gap beyond this cap can
// do so manually via the offset slider during gameplay (BeatmapOffsetControl),
// where the real-time hit-error display gives direct feedback.
// 15 ms = typical MMAP output-latency ceiling on modern Android (Snapdragon 8 Gen 2,
// Exynos 2400, etc.). Anything higher is Samsung/DSP overhead that doesn't affect
// relative music-hitsound timing — applying it would push hitsound-music desync
// above the ~20 ms human JND.
const double max_hw_compensation_ms = 15.0;
double cappedLatency = Math.Min(latency, max_hw_compensation_ms); // capped compensation value (≤15 ms)
double suggested = Math.Clamp(-cappedLatency, audioOffset.MinValue, audioOffset.MaxValue);
// With AudioOffset = -hw_latency, the player taps hw_latency ms later in the
// BASS timeline than they would with AudioOffset = 0. Both the hitsound (fired
// at that later BASS position) and the music beat (fired at the nominal BASS
// position) are delivered through the same Oboe output path and thus reach the
// speaker at consistent times relative to each other. The visual timing is
// corrected so notes appear on screen when they should be hit.
Comment on lines +1974 to +1984
double suggested = Math.Clamp(-latency, audioOffset.MinValue, audioOffset.MaxValue);
audioOffset.Value = suggested;

if (latency > max_hw_compensation_ms)
Logger.Log($"[osu!] Audio offset re-synced from hardware: {suggested:F1}ms (measured={latency:F1}ms, capped at {max_hw_compensation_ms}ms to preserve hitsound-music sync — fine-tune manually in gameplay)");
else
Logger.Log($"[osu!] Audio offset re-synced from hardware: {suggested:F1}ms (median hardware latency={latency:F1}ms, previous offset saved for restore)");
Logger.Log($"[osu!] Audio offset re-synced from hardware: {suggested:F1}ms (measured Oboe latency={latency:F1}ms)");
});
}

Expand Down
37 changes: 12 additions & 25 deletions osu.Game/Overlays/Settings/Sections/Audio/AndroidAudioSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ private void load(OsuConfigManager config, OsuGame? game)
Caption = "Audio output backend",
HintText = "Selects how BASS audio is delivered to the hardware.\n"
+ "• AudioTrack — default BASS backend, maximum compatibility (~80–120 ms latency).\n"
+ "• AAudio — BASS uses Android's AAudio API directly; lower latency on Android 8.0+. Takes effect after restart.\n"
+ "• Oboe — routes BASS through Google's Oboe library with AAudio Exclusive + MMAP; lowest latency (~5–15 ms) on supported devices. Recommended.",
+ "• AAudio — BASS uses Android's AAudio API; provides similar latency to AudioTrack with no measurable improvement. Takes effect after restart.\n"
+ "• Oboe — routes BASS through Google's Oboe library with AAudio Exclusive + MMAP; lowest latency (~4–8 ms) on supported devices. Recommended.",
Current = config.GetBindable<AndroidAudioOutput>(OsuSetting.AndroidAudioOutput),
})
{
Expand All @@ -56,43 +56,30 @@ private void load(OsuConfigManager config, OsuGame? game)
// audio device changed), making the offset feel "jittery". Hardware-latency
// measurement is now exclusively triggered by clicking the button below: it
// runs a 2 s sampling window, drops the warm-up samples, and applies the
// median of the remaining AAudio readings to AudioOffset. The button no-ops
// full median of the Oboe readings as a negative AudioOffset. The button no-ops
// (logging "already measuring") if clicked again within the active window,
// so users can mash it without producing partial measurements.
//
// After a resync the restore button below becomes active so users can undo
// if the hardware measurement doesn't match their perception.
//
// HITSOUND-MUSIC ALIGNMENT NOTE:
// AudioOffset shifts the VISUAL timing of hit objects relative to the audio
// track. Because hitsound samples fire at the moment of player input (not
// at a pre-scheduled beatmap time), a non-zero offset shifts WHEN inputs
// arrive in the audio timeline: every 1 ms of negative AudioOffset creates
// 1 ms of hitsound-after-music lag. AudioOffset = 0 gives perfect
// hitsound-music synchronisation.
//
// Resync caps the applied value at 15 ms (the typical MMAP output-latency
// ceiling) to keep any hitsound-music desync below the human audibility
// threshold (~20 ms), even on devices whose DSP reports higher latency.
// If you find hitsounds still feel late relative to the music after Resync,
// lower the offset toward 0 using the slider in Settings → Audio. For the
// most accurate per-song tuning, use the in-game "Audio offset (this
// beatmap)" control during gameplay — the real-time hit-error bar gives
// direct feedback.
// Both music and hitsounds travel through the same BASS → Oboe pipeline, so
// they both experience the same hardware latency offset. AudioOffset corrects
// the visual timing so notes appear when they should be hit; the audio output
// for both music and hitsounds shifts together.
//
// IMPORTANT — Bluetooth speakers/headphones:
// The AAudio measurement captures the WIRED audio pipeline only. Bluetooth
// A2DP adds a further ~100–300 ms of wireless transmission delay that AAudio
// The Oboe measurement captures the WIRED audio pipeline only. Bluetooth
// A2DP adds a further ~100–300 ms of wireless transmission delay that Oboe
// cannot see. For BT output, Resync will set a small wired-path value;
// you must further adjust AudioOffset manually (positive = sounds arrive
// late; negative = sounds arrive early) until music and hitsounds feel right.
new SettingsButtonV2
{
Text = "Resync hardware audio offset",
TooltipText = "Measures the device's AAudio pipeline latency over 2 s and applies the result (capped at 15 ms) to the audio offset. "
+ "Shifts VISUAL hit-object timing to match when you hear the music. "
+ "HITSOUND NOTE: any non-zero offset creates an equal hitsound-after-music lag; AudioOffset = 0 keeps hitsounds perfectly in sync with the music. "
+ "If hitsounds feel off after Resync, lower the offset toward 0 manually. "
TooltipText = "Measures the device's Oboe pipeline latency over 2 s and applies the full result as a negative AudioOffset. "
+ "Shifts VISUAL hit-object timing so notes appear on screen when they should be hit. "
+ "Both music and hitsounds travel through the same BASS → Oboe pipeline, so audio timing shifts together. "
+ "For per-song tuning, use the in-game beatmap offset control during gameplay. "
+ "Does NOT include Bluetooth A2DP delay (~100–300 ms extra) — fine-tune manually for BT output.",
Comment on lines +66 to 84
Action = () => game?.ResyncHardwareAudioOffset(),
Expand Down
4 changes: 2 additions & 2 deletions osu.Game/osu.Game.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="20.1.0" />
<PackageReference Include="ppy.osu.Framework" Version="2026.506.2" />
<PackageReference Include="ppy.osu.Framework" Version="2026.506.4" />
<!--
Explicitly pin `ppy.Veldrid.SPIRV` to the winnerspiros fork build that
`ppy.osu.Framework 2026.506.2` was compiled against.This version is the only
`ppy.osu.Framework 2026.506.4` was compiled against.This version is the only
one whose `runtimes/android-arm64/native/libveldrid-spirv.so` is aligned to 16 KB
pages (required by Android 16+). It lives only as a release asset on
<https://github.com/winnerspiros/veldrid-spirv/releases/tag/1.0> and is vendored
Expand Down
2 changes: 1 addition & 1 deletion osu.iOS.props
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@
<SuppressTrimAnalysisWarnings>true</SuppressTrimAnalysisWarnings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2026.506.2" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2026.506.4" />
</ItemGroup>
</Project>
Loading