From 2e2b704898d64c4b8424e80297337b36370d9670 Mon Sep 17 00:00:00 2001 From: Thomas Mathieson Date: Sun, 18 Feb 2024 01:00:43 +0000 Subject: [PATCH] A few playback bugfixes: - Cue delays were the source of a few race conditions which have now been fixed, including a crash bug - Added a few try catches defensively to the audio playback manager to prevent crashes --- QPlayer/ViewModels/AudioPlaybackManager.cs | 56 ++++++++++++++++++---- QPlayer/ViewModels/CueViewModel.cs | 2 + QPlayer/ViewModels/SoundCueViewModel.cs | 5 ++ QPlayer/ViewModels/ViewModel.cs | 2 + 4 files changed, 57 insertions(+), 8 deletions(-) diff --git a/QPlayer/ViewModels/AudioPlaybackManager.cs b/QPlayer/ViewModels/AudioPlaybackManager.cs index 3e6a4a6..6016189 100644 --- a/QPlayer/ViewModels/AudioPlaybackManager.cs +++ b/QPlayer/ViewModels/AudioPlaybackManager.cs @@ -145,8 +145,15 @@ public void OpenOutputDevice(AudioOutputDriver driver, object key) this.driver = driver; device.PlaybackStopped += DevicePlaybackStopped; deviceClosedEvent.Reset(); - device.Init(mixer); - device.Play(); + try + { + device.Init(mixer); + device.Play(); + } catch(Exception ex) + { + MainViewModel.Log($"Failed to start device '{key}' with driver '{driver}'.\n" + ex, + MainViewModel.LogLevel.Error); + } /*var sig = new SignalGenerator(); PlaySound(sig);*/ MainViewModel.Log($"Opened sound device '{key}' with driver '{driver}'!", MainViewModel.LogLevel.Info); @@ -184,9 +191,18 @@ private ISampleProvider ConvertToMixerFormat(ISampleProvider input) /// a callback raised when the stream is removed from the mixer public void PlaySound(ISampleProvider provider, Action? onCompleted = null) { - var converted = ConvertToMixerFormat(provider); - activeChannels.Add(provider, (converted, onCompleted)); - mixer.AddMixerInput(converted); + try + { + if (activeChannels.ContainsKey(provider)) + return; + + var converted = ConvertToMixerFormat(provider); + activeChannels.Add(provider, (converted, onCompleted)); + mixer.AddMixerInput(converted); + } catch (Exception ex) + { + MainViewModel.Log("Error while trying to play sound! \n" + ex, MainViewModel.LogLevel.Error); + } } /// @@ -195,12 +211,36 @@ public void PlaySound(ISampleProvider provider, Action? onCompl /// the sample stream to stop public void StopSound(ISampleProvider provider) { - if (activeChannels.TryGetValue(provider, out var channel)) + try { - mixer.RemoveMixerInput(channel.convertedStream); - activeChannels.Remove(provider); + if (activeChannels.TryGetValue(provider, out var channel)) + { + mixer.RemoveMixerInput(channel.convertedStream); + activeChannels.Remove(provider); + } + } catch (Exception ex) + { + MainViewModel.Log("Error while trying to stop sound! \n" + ex, MainViewModel.LogLevel.Error); } } + + /// + /// Gets whether a sound stream is currently playing. + /// + /// + /// + public bool IsPlaying(ISampleProvider provider) => activeChannels.ContainsKey(provider); + + /// + /// Stops all sound sources. + /// + public void StopAllSounds() + { + mixer.RemoveAllMixerInputs(); + foreach (var channel in activeChannels) + channel.Value.completedCallback?.Invoke(channel.Key); + activeChannels.Clear(); + } } public class LoopingSampleProvider : WaveStream, ISampleProvider where T : WaveStream, ISampleProvider diff --git a/QPlayer/ViewModels/CueViewModel.cs b/QPlayer/ViewModels/CueViewModel.cs index bb471ac..d12644e 100644 --- a/QPlayer/ViewModels/CueViewModel.cs +++ b/QPlayer/ViewModels/CueViewModel.cs @@ -176,11 +176,13 @@ public virtual void Go() public virtual void Pause() { + goTimer.Stop(); State = CueState.Paused; } public virtual void Stop() { + goTimer.Stop(); State = CueState.Ready; mainViewModel?.ActiveCues.Remove(this); } diff --git a/QPlayer/ViewModels/SoundCueViewModel.cs b/QPlayer/ViewModels/SoundCueViewModel.cs index 39905e4..48e9c4e 100644 --- a/QPlayer/ViewModels/SoundCueViewModel.cs +++ b/QPlayer/ViewModels/SoundCueViewModel.cs @@ -94,8 +94,13 @@ public override void Go() base.Go(); if (oldState == CueState.Playing || oldState == CueState.PlayingLooped) StopAudio(); + if (audioFile == null || fadeInOutProvider == null || mainViewModel == null) return; + // There are a few edge cases where this can happen (notably when starting a cue while it's in the delay state). + if (mainViewModel.AudioPlaybackManager.IsPlaying(fadeInOutProvider)) + StopAudio(); + audioProgressUpdater.Start(); if (FadeOut > 0) { diff --git a/QPlayer/ViewModels/ViewModel.cs b/QPlayer/ViewModels/ViewModel.cs index 3291e3e..ea3efb6 100644 --- a/QPlayer/ViewModels/ViewModel.cs +++ b/QPlayer/ViewModels/ViewModel.cs @@ -368,6 +368,8 @@ public void StopExecute() { for(int i = ActiveCues.Count-1; i >= 0; i--) ActiveCues[i].Stop(); + + AudioPlaybackManager.StopAllSounds(); } public void MoveCueUpExecute()