Catch and retry when DirectSound crashes (squashed PR #2166, fixes #1212)

* Handling for BufferLost
Fixes issue #1212 where adding/removing headphones would lead to a crash by restoring the sound device when it's detected and being defensive with exception handling.

* Internalize the handling of buffer lost and make it a bit more efficient.
Remove interface function for SoundLost and move the logic to soley live inside DirectSoundOutput. Additionally I discovered I did not need to tear down the entire device to restore sound, typically Restore() and Play() handle it. Still need to wrap every place that can throw an exception in a try/catch block and wait for WriteSamples to handle it.

* Update DirectSoundSoundOutput.cs
Logic is hard.

* Retry limiter added to recovery
Added a self reducing retry counter that will try to start sound 5 -> 4 -> 3 -> 2 -> 1 times each time it tries to recover until it succeeds, at which point the counter returns to 5. This allows for quicker attempts at recovery without the risk of an infinite loop or terrible performance from sleeping 10 ms.
This commit is contained in:
diddily 2020-06-25 21:56:27 -04:00 committed by GitHub
parent 312f029b0b
commit 46e744cd33
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 150 additions and 64 deletions

View File

@ -18,10 +18,12 @@ namespace BizHawk.Client.EmuHawk
private int _filledBufferSizeBytes;
private long _lastWriteTime;
private int _lastWriteCursor;
private int _retryCounter;
public DirectSoundSoundOutput(Sound sound, IntPtr mainWindowHandle, string soundDevice)
{
_sound = sound;
_retryCounter = 5;
var deviceInfo = DirectSound.GetDevices().FirstOrDefault(d => d.Description == soundDevice);
_device = deviceInfo != null ? new DirectSound(deviceInfo.DriverGuid) : new DirectSound();
@ -49,26 +51,22 @@ namespace BizHawk.Client.EmuHawk
public int MaxSamplesDeficit { get; private set; }
public void ApplyVolumeSettings(double volume)
private bool IsPlaying => _deviceBuffer != null && (_deviceBuffer.Status & BufferStatus.BufferLost) == 0 && (_deviceBuffer.Status & BufferStatus.Playing) == BufferStatus.Playing;
private void StartPlaying()
{
// I'm not sure if this is "technically" correct but it works okay
int range = (int)Volume.Maximum - (int)Volume.Minimum;
_deviceBuffer.Volume = (int)(Math.Pow(volume, 0.1) * range) + (int)Volume.Minimum;
}
public void StartSound()
_actualWriteOffsetBytes = -1;
_filledBufferSizeBytes = 0;
_lastWriteTime = 0;
_lastWriteCursor = 0;
int attempts = _retryCounter;
while (!IsPlaying && attempts > 0)
{
attempts--;
try
{
if (_deviceBuffer == null)
{
BufferSizeSamples = Sound.MillisecondsToSamples(GlobalWin.Config.SoundBufferSizeMs);
// 35 to 65 milliseconds depending on how big the buffer is. This is a trade-off
// between more frequent but less severe glitches (i.e. catching underruns before
// they happen and filling the buffer with silence) or less frequent but more
// severe glitches. At least on my Windows 8 machines, the distance between the
// play and write cursors can be up to 30 milliseconds, so that would be the
// absolute minimum we could use here.
int minBufferFullnessMs = Math.Min(35 + ((GlobalWin.Config.SoundBufferSizeMs - 60) / 2), 65);
MaxSamplesDeficit = BufferSizeSamples - Sound.MillisecondsToSamples(minBufferFullnessMs);
var format = new WaveFormat
{
SamplesPerSecond = Sound.SampleRate,
@ -91,28 +89,89 @@ namespace BizHawk.Client.EmuHawk
};
_deviceBuffer = new SecondarySoundBuffer(_device, desc);
_actualWriteOffsetBytes = -1;
_filledBufferSizeBytes = 0;
_lastWriteTime = 0;
_lastWriteCursor = 0;
}
_deviceBuffer.Play(0, PlayFlags.Looping);
}
catch (DirectSoundException)
{
if (_deviceBuffer != null)
{
_deviceBuffer.Restore();
}
if (attempts > 0)
{
System.Threading.Thread.Sleep(10);
}
}
}
if (IsPlaying)
{
_retryCounter = 5;
}
else if (_retryCounter > 1)
{
_retryCounter--;
}
}
public void ApplyVolumeSettings(double volume)
{
if (IsPlaying)
{
try
{
// I'm not sure if this is "technically" correct but it works okay
int range = (int)Volume.Maximum - (int)Volume.Minimum;
_deviceBuffer.Volume = (int)(Math.Pow(volume, 0.1) * range) + (int)Volume.Minimum;
}
catch (DirectSoundException)
{
}
}
}
public void StartSound()
{
BufferSizeSamples = Sound.MillisecondsToSamples(GlobalWin.Config.SoundBufferSizeMs);
// 35 to 65 milliseconds depending on how big the buffer is. This is a trade-off
// between more frequent but less severe glitches (i.e. catching underruns before
// they happen and filling the buffer with silence) or less frequent but more
// severe glitches. At least on my Windows 8 machines, the distance between the
// play and write cursors can be up to 30 milliseconds, so that would be the
// absolute minimum we could use here.
int minBufferFullnessMs = Math.Min(35 + ((GlobalWin.Config.SoundBufferSizeMs - 60) / 2), 65);
MaxSamplesDeficit = BufferSizeSamples - Sound.MillisecondsToSamples(minBufferFullnessMs);
StartPlaying();
}
public void StopSound()
{
if (IsPlaying)
{
try
{
_deviceBuffer.Stop();
}
catch (DirectSoundException)
{
}
}
_deviceBuffer.Dispose();
_deviceBuffer = null;
BufferSizeSamples = 0;
}
public int CalculateSamplesNeeded()
{
if (_deviceBuffer.Status == BufferStatus.BufferLost) return 0;
int samplesNeeded = 0;
if (IsPlaying)
{
try
{
long currentWriteTime = Stopwatch.GetTimestamp();
int playCursor = _deviceBuffer.CurrentPlayPosition;
int writeCursor = _deviceBuffer.CurrentWritePosition;
@ -132,13 +191,19 @@ namespace BizHawk.Client.EmuHawk
_actualWriteOffsetBytes = writeCursor;
_filledBufferSizeBytes = 0;
}
int samplesNeeded = CircularDistance(_actualWriteOffsetBytes, playCursor, BufferSizeBytes) / Sound.BlockAlign;
samplesNeeded = CircularDistance(_actualWriteOffsetBytes, playCursor, BufferSizeBytes) / Sound.BlockAlign;
if (isInitializing || detectedUnderrun)
{
_sound.HandleInitializationOrUnderrun(detectedUnderrun, ref samplesNeeded);
}
_lastWriteTime = currentWriteTime;
_lastWriteCursor = writeCursor;
}
catch (DirectSoundException)
{
samplesNeeded = 0;
}
}
return samplesNeeded;
}
@ -148,11 +213,32 @@ namespace BizHawk.Client.EmuHawk
}
public void WriteSamples(short[] samples, int sampleOffset, int sampleCount)
{
// For lack of a better place, this function will be the one that attempts to restart playing
// after a sound buffer is lost.
if (IsPlaying)
{
if (sampleCount == 0) return;
try
{
_deviceBuffer.Write(samples, sampleOffset * Sound.ChannelCount, sampleCount * Sound.ChannelCount, _actualWriteOffsetBytes, LockFlags.None);
_actualWriteOffsetBytes = (_actualWriteOffsetBytes + (sampleCount * Sound.BlockAlign)) % BufferSizeBytes;
_filledBufferSizeBytes += sampleCount * Sound.BlockAlign;
}
catch (DirectSoundException)
{
_deviceBuffer.Restore();
StartPlaying();
}
}
else
{
if (_deviceBuffer != null)
{
_deviceBuffer.Restore();
}
StartPlaying();
}
}
}
}