Skip to content
Draft
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
58 changes: 46 additions & 12 deletions src/ui/Logic/VideoPlayers/Ffmpeg/Audio/AudioQueueAudioSink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ private struct AudioTimeStamp
private double _sampleBase;
private double _lastSampleTime;
private long _bytesWritten;
private bool _clockBaselineValid = true;
private int _clockReadFailures;

public void Open(int sampleRate, int channels)
{
Expand Down Expand Up @@ -176,6 +178,8 @@ public void Open(int sampleRate, int channels)
_sampleBase = 0;
_lastSampleTime = 0;
_bytesWritten = 0;
_clockBaselineValid = true;
_clockReadFailures = 0;
}
}

Expand Down Expand Up @@ -214,16 +218,35 @@ public double PlayedSeconds
{
get
{
lock (_lock)
TryGetPlayedSeconds(out var playedSeconds);
return playedSeconds;
}
}

public bool TryGetPlayedSeconds(out double playedSeconds)
{
lock (_lock)
{
if (_queue == IntPtr.Zero || _bytesPerSecond == 0)
{
if (_queue == IntPtr.Zero || _bytesPerSecond == 0)
{
return 0;
}
playedSeconds = 0;
return false;
}

var played = Math.Max(0, CurrentSampleTime() - _sampleBase) * _blockAlign;
return Math.Min(played, _bytesWritten) / _bytesPerSecond;
if (!_clockBaselineValid)
{
// A failed post-reset sample-time read leaves no trustworthy delta for this
// serial. Report zero played time and let the player leave the device clock;
// a later Reset can establish a fresh baseline.
playedSeconds = 0;
return false;
}

var readSucceeded = TryCurrentSampleTime(out var sampleTime);
_clockReadFailures = AudioSinkClockHealth.NextFailureCount(_clockReadFailures, readSucceeded);
var played = Math.Max(0, sampleTime - _sampleBase) * _blockAlign;
playedSeconds = Math.Min(played, _bytesWritten) / _bytesPerSecond;
return AudioSinkClockHealth.IsHealthy(baselineValid: true, _clockReadFailures);
}
}

Expand All @@ -240,18 +263,28 @@ private static double SampleBaseAfterUnderrun(double currentSampleTime, long byt
/// <summary>Queue clock in sample frames; the last known value when the queue is not running.</summary>
private double CurrentSampleTime()
{
var readSucceeded = TryCurrentSampleTime(out var sampleTime);
_clockReadFailures = AudioSinkClockHealth.NextFailureCount(_clockReadFailures, readSucceeded);
return sampleTime;
}

private bool TryCurrentSampleTime(out double sampleTime)
{
sampleTime = _lastSampleTime;
if (!_started)
{
return _lastSampleTime;
return true;
}

var result = AudioQueueGetCurrentTime(_queue, IntPtr.Zero, out var time, IntPtr.Zero);
if (result == 0 && !double.IsNaN(time.mSampleTime))
if (result != 0 || !double.IsFinite(time.mSampleTime))
{
_lastSampleTime = time.mSampleTime;
return false;
}

return _lastSampleTime;
_lastSampleTime = time.mSampleTime;
sampleTime = _lastSampleTime;
return true;
}

public bool Write(ReadOnlySpan<byte> pcm, int serial)
Expand Down Expand Up @@ -375,7 +408,8 @@ public void Reset(int serial)
_inFlight = 0;
_nextBuffer = 0;
_bytesWritten = 0;
_sampleBase = CurrentSampleTime();
_clockReadFailures = 0;
_clockBaselineValid = TryCurrentSampleTime(out _sampleBase);
_startFailed = false;
Volatile.Write(ref _serial, AudioSinkResetFence.SerialAfterReset(serial, succeeded: true));
_resetting = false;
Expand Down
25 changes: 25 additions & 0 deletions src/ui/Logic/VideoPlayers/Ffmpeg/Audio/IAudioSink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ public interface IAudioSink : IDisposable
/// <summary>Seconds of audio played since the last <see cref="Reset"/>.</summary>
double PlayedSeconds { get; }

/// <summary>
/// Returns the best known played position and whether the native clock read is currently
/// trustworthy. A false result still supplies the last known position so the player can
/// switch clocks without a discontinuity.
/// </summary>
bool TryGetPlayedSeconds(out double playedSeconds);

/// <summary>
/// Drop everything queued, make <paramref name="serial"/> the only serial accepted by
/// <see cref="Write"/>, and restart the played counter at zero (seek, stop).
Expand All @@ -43,3 +50,21 @@ internal static int SerialAfterReset(int requestedSerial, bool succeeded)
return succeeded ? requestedSerial : RejectedSerial;
}
}


internal static class AudioSinkClockHealth
{
internal const int ConsecutiveFailuresBeforeUnhealthy = 2;

internal static int NextFailureCount(int currentFailures, bool readSucceeded)
{
return readSucceeded
? 0
: Math.Min(ConsecutiveFailuresBeforeUnhealthy, currentFailures + 1);
}

internal static bool IsHealthy(bool baselineValid, int consecutiveFailures)
{
return baselineValid && consecutiveFailures < ConsecutiveFailuresBeforeUnhealthy;
}
}
17 changes: 16 additions & 1 deletion src/ui/Logic/VideoPlayers/Ffmpeg/Audio/SilentAudioSink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,22 @@ public void Open(int sampleRate, int channels)
Reset(0);
}

public double PlayedSeconds => Math.Min(_clock.Elapsed.TotalSeconds, Interlocked.Read(ref _bytesWritten) / (double)_bytesPerSecond);
public double PlayedSeconds
{
get
{
TryGetPlayedSeconds(out var playedSeconds);
return playedSeconds;
}
}

public bool TryGetPlayedSeconds(out double playedSeconds)
{
playedSeconds = Math.Min(
_clock.Elapsed.TotalSeconds,
Interlocked.Read(ref _bytesWritten) / (double)_bytesPerSecond);
return true;
}

public bool Write(ReadOnlySpan<byte> pcm, int serial)
{
Expand Down
58 changes: 42 additions & 16 deletions src/ui/Logic/VideoPlayers/Ffmpeg/Audio/WaveOutAudioSink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ private struct MmTime
private uint _lastPositionType;
private uint _lastPositionCounter;
private long _positionWrapBaseBytes;
private bool _clockBaselineValid = true;
private int _clockReadFailures;

public void Open(int sampleRate, int channels)
{
Expand Down Expand Up @@ -232,6 +234,8 @@ public void Open(int sampleRate, int channels)
_lastPositionType = 0;
_lastPositionCounter = 0;
_positionWrapBaseBytes = 0;
_clockBaselineValid = true;
_clockReadFailures = 0;
_serial = 0;
_paused = false;
}
Expand All @@ -241,34 +245,53 @@ public double PlayedSeconds
{
get
{
lock (_lock)
TryGetPlayedSeconds(out var playedSeconds);
return playedSeconds;
}
}

public bool TryGetPlayedSeconds(out double playedSeconds)
{
lock (_lock)
{
if (_device == IntPtr.Zero || _bytesPerSecond == 0)
{
if (_device == IntPtr.Zero || _bytesPerSecond == 0)
{
return 0;
}
playedSeconds = 0;
return false;
}

var raw = GetRawPositionBytes();
return Math.Max(0, raw - _positionBase) / (double)_bytesPerSecond;
if (!_clockBaselineValid)
{
// Without a trustworthy post-reset baseline the driver's absolute counter cannot
// be converted into a safe delta. Fall back from the audio master clock at the
// anchor itself; a later Reset can establish a new baseline and recover.
playedSeconds = 0;
return false;
}

var readSucceeded = TryGetRawPositionBytes(out var raw);
_clockReadFailures = AudioSinkClockHealth.NextFailureCount(_clockReadFailures, readSucceeded);
playedSeconds = Math.Max(0, raw - _positionBase) / (double)_bytesPerSecond;
return AudioSinkClockHealth.IsHealthy(baselineValid: true, _clockReadFailures);
}
}

private long GetRawPositionBytes()
private bool TryGetRawPositionBytes(out long position)
{
position = _lastRawPosition;
// Samples are Microsoft's preferred waveform position format. Drivers may still answer
// in another supported MMTIME format, so normalize the returned type rather than assuming
// the request was honoured.
var time = new MmTime { wType = WaveOutPosition.TimeSamples };
if (waveOutGetPosition(_device, ref time, (uint)sizeof(MmTime)) != MmSysErrNoError)
{
return _lastRawPosition;
return false;
}

var converted = WaveOutPosition.CounterToBytes(time.wType, time.u, _blockAlign, _bytesPerSecond);
if (!converted.HasValue)
{
return _lastRawPosition;
return false;
}

var wrapBytes = WaveOutPosition.CounterWrapBytes(time.wType, _blockAlign, _bytesPerSecond);
Expand All @@ -290,17 +313,18 @@ private long GetRawPositionBytes()
wrapBytes);
}

var position = _positionWrapBaseBytes + converted.Value;
if (position < _lastRawPosition)
var absolutePosition = _positionWrapBaseBytes + converted.Value;
if (absolutePosition < _lastRawPosition)
{
// Millisecond conversion can round a format switch slightly backwards.
position = _lastRawPosition;
absolutePosition = _lastRawPosition;
}

_lastPositionType = time.wType;
_lastPositionCounter = time.u;
_lastRawPosition = position;
return position;
_lastRawPosition = absolutePosition;
position = absolutePosition;
return true;
}

public bool Write(ReadOnlySpan<byte> pcm, int serial)
Expand Down Expand Up @@ -376,7 +400,9 @@ public void Reset(int serial)
_lastPositionType = 0;
_lastPositionCounter = 0;
_positionWrapBaseBytes = 0;
_positionBase = GetRawPositionBytes();
_clockReadFailures = 0;
_clockBaselineValid = TryGetRawPositionBytes(out var positionBase);
_positionBase = positionBase;
_nextBuffer = 0;
for (var i = 0; i < BufferCount; i++)
{
Expand Down
Loading
Loading