Merge pull request #36 from HueByte/dev_fix_notification_sound

fix: error when playing notification sounds in quick succession
This commit is contained in:
Hue
2026-04-20 17:42:06 +02:00
committed by HueByte
2 changed files with 21 additions and 3 deletions
+1
View File
@@ -13,6 +13,7 @@ Follow-up patch release for v0.2.9 addressing auto-updater regressions, adding a
- Fix pre-update backup failing when a Serilog-held log file is locked — `UpdateBackupService` now enumerates files manually, skips the `logs/` directory and `.log` files, and logs-and-continues on `IOException`/`UnauthorizedAccessException` instead of aborting the whole backup
- Simplify update progress dispatch — remove redundant `Application.Invoke` wrappers around progress updates that are already called from the UI thread (introduced while fixing the freeze above)
- Fix cursor position being reset to the start of the line when auto-completing commands in the CLI app — insertion point is now moved to the end of the completed text
- Fix notification sounds crashing or being silently dropped when several arrive in quick succession — playback is now serialized through a semaphore that's held for the duration of each sound (using `PlaybackFinished` with a 10s safety timeout) and always released in `finally`, so back-to-back notifications queue up and play in order instead of racing the underlying audio player (fixes #20)
## Refactoring
@@ -6,7 +6,11 @@ namespace EchoHub.Client.Services;
public class NotificationSoundService
{
// Safety net: if PlaybackFinished never fires we don't want to block future notifications forever.
private static readonly TimeSpan PlaybackTimeout = TimeSpan.FromSeconds(10);
private readonly Player _player = new();
private readonly SemaphoreSlim _lock = new(1, 1);
private readonly NotificationConfig _config;
private string? _resolvedSoundPath;
@@ -41,18 +45,31 @@ public class NotificationSoundService
private async Task PlayInternal()
{
await _lock.WaitAsync();
// _player.Play returns as soon as playback starts, so we wait on PlaybackFinished
// to hold the lock for the duration of the sound. A one-shot handler + timeout
// keeps the finally release robust: never-fires → timeout; fires twice → ignored
// (TrySetResult); handler throws → caller's catch still runs finally.
var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
void OnFinished(object? s, EventArgs e) => completion.TrySetResult();
_player.PlaybackFinished += OnFinished;
try
{
if (_player.Playing)
await _player.Stop();
await _player.SetVolume(_config.Volume);
await _player.Play(_resolvedSoundPath!);
await Task.WhenAny(completion.Task, Task.Delay(PlaybackTimeout));
}
catch (Exception ex)
{
Log.Warning(ex, "Failed to play notification sound");
}
finally
{
_player.PlaybackFinished -= OnFinished;
_lock.Release();
}
}
private void ResolveSoundPath()