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,11 +51,85 @@ namespace BizHawk.Client.EmuHawk
public int MaxSamplesDeficit { get; private set; }
private bool IsPlaying => _deviceBuffer != null && (_deviceBuffer.Status & BufferStatus.BufferLost) == 0 && (_deviceBuffer.Status & BufferStatus.Playing) == BufferStatus.Playing;
private void StartPlaying()
{
_actualWriteOffsetBytes = -1;
_filledBufferSizeBytes = 0;
_lastWriteTime = 0;
_lastWriteCursor = 0;
int attempts = _retryCounter;
while (!IsPlaying && attempts > 0)
{
attempts--;
try
{
if (_deviceBuffer == null)
{
var format = new WaveFormat
{
SamplesPerSecond = Sound.SampleRate,
BitsPerSample = Sound.BytesPerSample * 8,
Channels = Sound.ChannelCount,
FormatTag = WaveFormatTag.Pcm,
BlockAlignment = Sound.BlockAlign,
AverageBytesPerSecond = Sound.SampleRate * Sound.BlockAlign
};
var desc = new SoundBufferDescription
{
Format = format,
Flags =
BufferFlags.GlobalFocus |
BufferFlags.Software |
BufferFlags.GetCurrentPosition2 |
BufferFlags.ControlVolume,
SizeInBytes = BufferSizeBytes
};
_deviceBuffer = new SecondarySoundBuffer(_device, desc);
}
_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)
{
// 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;
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()
@ -69,76 +145,65 @@ namespace BizHawk.Client.EmuHawk
int minBufferFullnessMs = Math.Min(35 + ((GlobalWin.Config.SoundBufferSizeMs - 60) / 2), 65);
MaxSamplesDeficit = BufferSizeSamples - Sound.MillisecondsToSamples(minBufferFullnessMs);
var format = new WaveFormat
{
SamplesPerSecond = Sound.SampleRate,
BitsPerSample = Sound.BytesPerSample * 8,
Channels = Sound.ChannelCount,
FormatTag = WaveFormatTag.Pcm,
BlockAlignment = Sound.BlockAlign,
AverageBytesPerSecond = Sound.SampleRate * Sound.BlockAlign
};
var desc = new SoundBufferDescription
{
Format = format,
Flags =
BufferFlags.GlobalFocus |
BufferFlags.Software |
BufferFlags.GetCurrentPosition2 |
BufferFlags.ControlVolume,
SizeInBytes = BufferSizeBytes
};
_deviceBuffer = new SecondarySoundBuffer(_device, desc);
_actualWriteOffsetBytes = -1;
_filledBufferSizeBytes = 0;
_lastWriteTime = 0;
_lastWriteCursor = 0;
_deviceBuffer.Play(0, PlayFlags.Looping);
StartPlaying();
}
public void StopSound()
{
_deviceBuffer.Stop();
if (IsPlaying)
{
try
{
_deviceBuffer.Stop();
}
catch (DirectSoundException)
{
}
}
_deviceBuffer.Dispose();
_deviceBuffer = null;
BufferSizeSamples = 0;
}
public int CalculateSamplesNeeded()
{
if (_deviceBuffer.Status == BufferStatus.BufferLost) return 0;
long currentWriteTime = Stopwatch.GetTimestamp();
int playCursor = _deviceBuffer.CurrentPlayPosition;
int writeCursor = _deviceBuffer.CurrentWritePosition;
bool isInitializing = _actualWriteOffsetBytes == -1;
bool detectedUnderrun = false;
if (!isInitializing)
int samplesNeeded = 0;
if (IsPlaying)
{
double elapsedSeconds = (currentWriteTime - _lastWriteTime) / (double)Stopwatch.Frequency;
double bufferSizeSeconds = (double)BufferSizeSamples / Sound.SampleRate;
int cursorDelta = CircularDistance(_lastWriteCursor, writeCursor, BufferSizeBytes);
cursorDelta += BufferSizeBytes * (int)Math.Round((elapsedSeconds - (cursorDelta / (double)(Sound.SampleRate * Sound.BlockAlign))) / bufferSizeSeconds);
_filledBufferSizeBytes -= cursorDelta;
detectedUnderrun = _filledBufferSizeBytes < 0;
try
{
long currentWriteTime = Stopwatch.GetTimestamp();
int playCursor = _deviceBuffer.CurrentPlayPosition;
int writeCursor = _deviceBuffer.CurrentWritePosition;
bool isInitializing = _actualWriteOffsetBytes == -1;
bool detectedUnderrun = false;
if (!isInitializing)
{
double elapsedSeconds = (currentWriteTime - _lastWriteTime) / (double)Stopwatch.Frequency;
double bufferSizeSeconds = (double)BufferSizeSamples / Sound.SampleRate;
int cursorDelta = CircularDistance(_lastWriteCursor, writeCursor, BufferSizeBytes);
cursorDelta += BufferSizeBytes * (int)Math.Round((elapsedSeconds - (cursorDelta / (double)(Sound.SampleRate * Sound.BlockAlign))) / bufferSizeSeconds);
_filledBufferSizeBytes -= cursorDelta;
detectedUnderrun = _filledBufferSizeBytes < 0;
}
if (isInitializing || detectedUnderrun)
{
_actualWriteOffsetBytes = writeCursor;
_filledBufferSizeBytes = 0;
}
samplesNeeded = CircularDistance(_actualWriteOffsetBytes, playCursor, BufferSizeBytes) / Sound.BlockAlign;
if (isInitializing || detectedUnderrun)
{
_sound.HandleInitializationOrUnderrun(detectedUnderrun, ref samplesNeeded);
}
_lastWriteTime = currentWriteTime;
_lastWriteCursor = writeCursor;
}
catch (DirectSoundException)
{
samplesNeeded = 0;
}
}
if (isInitializing || detectedUnderrun)
{
_actualWriteOffsetBytes = writeCursor;
_filledBufferSizeBytes = 0;
}
int samplesNeeded = CircularDistance(_actualWriteOffsetBytes, playCursor, BufferSizeBytes) / Sound.BlockAlign;
if (isInitializing || detectedUnderrun)
{
_sound.HandleInitializationOrUnderrun(detectedUnderrun, ref samplesNeeded);
}
_lastWriteTime = currentWriteTime;
_lastWriteCursor = writeCursor;
return samplesNeeded;
}
@ -149,10 +214,31 @@ namespace BizHawk.Client.EmuHawk
public void WriteSamples(short[] samples, int sampleOffset, int sampleCount)
{
if (sampleCount == 0) return;
_deviceBuffer.Write(samples, sampleOffset * Sound.ChannelCount, sampleCount * Sound.ChannelCount, _actualWriteOffsetBytes, LockFlags.None);
_actualWriteOffsetBytes = (_actualWriteOffsetBytes + (sampleCount * Sound.BlockAlign)) % BufferSizeBytes;
_filledBufferSizeBytes += sampleCount * Sound.BlockAlign;
// 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();
}
}
}
}