// Copyright (C) 2026 SharpEmu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later using System.Diagnostics; namespace SharpEmu.HLE.Host; /// /// How much guest audio the host device has actually played, in seconds. /// /// This is the only clock in the emulator that advances at the rate the player /// hears. Wall clock runs ahead of it whenever the guest cannot feed the device /// (the stream underruns and the missing time is never played), so anything /// that has to stay in step with the guest's audio — host-decoded video being /// the case that matters — has to follow this rather than . /// /// Reported per stream and kept as the furthest-along value: the guest's ports /// all carry one mix, and the leading port is the one whose position the /// listener perceives. /// public static class GuestAudioClock { private static long _playedMicroseconds; private static long _lastAdvanceTimestamp; /// Seconds of guest audio the device has played. Monotonic. public static double PlayedSeconds => Interlocked.Read(ref _playedMicroseconds) / 1_000_000.0; /// /// True while a stream has reported progress recently. False means no guest /// audio is playing, and callers must fall back to wall clock rather than /// stalling on a clock that will never advance. /// public static bool IsRunning { get { var last = Interlocked.Read(ref _lastAdvanceTimestamp); return last != 0 && Stopwatch.GetElapsedTime(last) < TimeSpan.FromMilliseconds(250); } } public static void Report(double playedSeconds) { if (double.IsNaN(playedSeconds) || playedSeconds < 0) { return; } var microseconds = (long)(playedSeconds * 1_000_000.0); var current = Interlocked.Read(ref _playedMicroseconds); while (microseconds > current) { var seen = Interlocked.CompareExchange( ref _playedMicroseconds, microseconds, current); if (seen == current) { Interlocked.Exchange(ref _lastAdvanceTimestamp, Stopwatch.GetTimestamp()); return; } current = seen; } } }