From 46e744cd334f086a8fd4ed8373438ef2d57a243f Mon Sep 17 00:00:00 2001 From: diddily Date: Thu, 25 Jun 2020 21:56:27 -0400 Subject: [PATCH] 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. --- .../Sound/Output/DirectSoundSoundOutput.cs | 214 ++++++++++++------ 1 file changed, 150 insertions(+), 64 deletions(-) diff --git a/src/BizHawk.Client.EmuHawk/Sound/Output/DirectSoundSoundOutput.cs b/src/BizHawk.Client.EmuHawk/Sound/Output/DirectSoundSoundOutput.cs index 4bf81c8955..24e07aa0c1 100644 --- a/src/BizHawk.Client.EmuHawk/Sound/Output/DirectSoundSoundOutput.cs +++ b/src/BizHawk.Client.EmuHawk/Sound/Output/DirectSoundSoundOutput.cs @@ -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(); + } } } }