refactor: enhance audio playback service with semaphore locking for thread safety

This commit is contained in:
HueByte
2026-02-22 16:43:05 +01:00
parent 4f96b8d986
commit 993bb1f973
2 changed files with 81 additions and 28 deletions
@@ -6,6 +6,7 @@ namespace EchoHub.Client.Services;
public class AudioPlaybackService public class AudioPlaybackService
{ {
private readonly Player _player = new(); private readonly Player _player = new();
private readonly SemaphoreSlim _lock = new(1, 1);
public bool IsPlaying => _player.Playing; public bool IsPlaying => _player.Playing;
public bool IsPaused => _player.Paused; public bool IsPaused => _player.Paused;
@@ -19,6 +20,7 @@ public class AudioPlaybackService
public async Task PlayAsync(string filePath) public async Task PlayAsync(string filePath)
{ {
await _lock.WaitAsync();
try try
{ {
if (_player.Playing) if (_player.Playing)
@@ -30,10 +32,15 @@ public class AudioPlaybackService
{ {
Log.Warning(ex, "Failed to play audio file: {Path}", filePath); Log.Warning(ex, "Failed to play audio file: {Path}", filePath);
} }
finally
{
_lock.Release();
}
} }
public async Task PauseAsync() public async Task PauseAsync()
{ {
await _lock.WaitAsync();
try try
{ {
if (_player.Playing && !_player.Paused) if (_player.Playing && !_player.Paused)
@@ -43,10 +50,15 @@ public class AudioPlaybackService
{ {
Log.Warning(ex, "Failed to pause audio playback"); Log.Warning(ex, "Failed to pause audio playback");
} }
finally
{
_lock.Release();
}
} }
public async Task ResumeAsync() public async Task ResumeAsync()
{ {
await _lock.WaitAsync();
try try
{ {
if (_player.Paused) if (_player.Paused)
@@ -56,23 +68,33 @@ public class AudioPlaybackService
{ {
Log.Warning(ex, "Failed to resume audio playback"); Log.Warning(ex, "Failed to resume audio playback");
} }
finally
{
_lock.Release();
}
} }
public async Task StopAsync() public async Task StopAsync()
{ {
await _lock.WaitAsync();
try try
{ {
if (_player.Playing) if (_player.Playing || _player.Paused)
await _player.Stop(); await _player.Stop();
} }
catch (Exception ex) catch (Exception ex)
{ {
Log.Warning(ex, "Failed to stop audio playback"); Log.Warning(ex, "Failed to stop audio playback");
} }
finally
{
_lock.Release();
}
} }
public async Task SetVolumeAsync(byte volume) public async Task SetVolumeAsync(byte volume)
{ {
await _lock.WaitAsync();
try try
{ {
await _player.SetVolume(Math.Min(volume, (byte)100)); await _player.SetVolume(Math.Min(volume, (byte)100));
@@ -81,5 +103,9 @@ public class AudioPlaybackService
{ {
Log.Warning(ex, "Failed to set audio volume"); Log.Warning(ex, "Failed to set audio volume");
} }
finally
{
_lock.Release();
}
} }
} }
@@ -23,7 +23,7 @@ public sealed class AudioPlayerDialog
public static void Show(IApplication app, AudioPlaybackService audioService, string filePath, string fileName) public static void Show(IApplication app, AudioPlaybackService audioService, string filePath, string fileName)
{ {
var dialog = new Dialog { Title = "Audio Player", Width = 52, Height = 14 }; var dialog = new Dialog { Title = "Audio Player", Width = 52, Height = 12 };
// ── File name ── // ── File name ──
var fileLabel = new Label var fileLabel = new Label
@@ -61,38 +61,47 @@ public sealed class AudioPlayerDialog
}; };
byte currentVolume = 50; byte currentVolume = 50;
var volumeBar = new ProgressBar
{
X = 14,
Y = 7,
Width = 20,
Height = 1,
Fraction = currentVolume / 100f,
ProgressBarStyle = ProgressBarStyle.Continuous
};
var volumePercentLabel = new Label
{
Text = $"{currentVolume}%",
X = 35,
Y = 7,
Width = 5
};
var volDownButton = new Button var volDownButton = new Button
{ {
Text = "-", Text = "-",
X = 10, X = 10,
Y = 7, Y = 7,
Width = 3 Width = 1,
Height = 1,
NoDecorations = true,
NoPadding = true,
ShadowStyle = ShadowStyle.None
};
var volumeBar = new ProgressBar
{
X = 12,
Y = 7,
Width = 22,
Height = 1,
Fraction = currentVolume / 100f,
ProgressBarStyle = ProgressBarStyle.Continuous
}; };
var volUpButton = new Button var volUpButton = new Button
{ {
Text = "+", Text = "+",
X = 41, X = 35,
Y = 7, Y = 7,
Width = 3 Width = 1,
Height = 1,
NoDecorations = true,
NoPadding = true,
ShadowStyle = ShadowStyle.None
};
var volumePercentLabel = new Label
{
Text = $"{currentVolume}%",
X = 37,
Y = 7,
Width = 5
}; };
// ── Playback controls ── // ── Playback controls ──
@@ -100,22 +109,25 @@ public sealed class AudioPlayerDialog
{ {
Text = "\u25b6 Play", Text = "\u25b6 Play",
X = 2, X = 2,
Y = 10, Y = 9,
IsDefault = true IsDefault = true,
ShadowStyle = ShadowStyle.None
}; };
var stopButton = new Button var stopButton = new Button
{ {
Text = "\u25a0 Stop", Text = "\u25a0 Stop",
X = Pos.Right(playButton) + 2, X = Pos.Right(playButton) + 1,
Y = 10 Y = 9,
ShadowStyle = ShadowStyle.None
}; };
var closeButton = new Button var closeButton = new Button
{ {
Text = "Close", Text = "Close",
X = Pos.Right(stopButton) + 2, X = Pos.Right(stopButton) + 1,
Y = 10 Y = 9,
ShadowStyle = ShadowStyle.None
}; };
// ── Animation state ── // ── Animation state ──
@@ -128,6 +140,7 @@ public sealed class AudioPlayerDialog
Timer? animationTimer = null; Timer? animationTimer = null;
var isDisposed = false; var isDisposed = false;
var isBusy = false;
// ── Helper functions ── // ── Helper functions ──
void UpdateWave(bool isActive) void UpdateWave(bool isActive)
@@ -278,6 +291,8 @@ public sealed class AudioPlayerDialog
playButton.Accepting += (s, e) => playButton.Accepting += (s, e) =>
{ {
e.Handled = true; e.Handled = true;
if (isBusy) return;
isBusy = true;
Task.Run(async () => Task.Run(async () =>
{ {
if (audioService.IsPaused) if (audioService.IsPaused)
@@ -287,6 +302,7 @@ public sealed class AudioPlayerDialog
{ {
UpdateStatus(); UpdateStatus();
StartAnimation(); StartAnimation();
isBusy = false;
}); });
} }
else if (audioService.IsPlaying) else if (audioService.IsPlaying)
@@ -296,6 +312,7 @@ public sealed class AudioPlayerDialog
{ {
UpdateStatus(); UpdateStatus();
StopAnimation(); StopAnimation();
isBusy = false;
}); });
} }
else else
@@ -306,6 +323,7 @@ public sealed class AudioPlayerDialog
{ {
UpdateStatus(); UpdateStatus();
StartAnimation(); StartAnimation();
isBusy = false;
}); });
} }
}); });
@@ -314,6 +332,8 @@ public sealed class AudioPlayerDialog
stopButton.Accepting += (s, e) => stopButton.Accepting += (s, e) =>
{ {
e.Handled = true; e.Handled = true;
if (isBusy) return;
isBusy = true;
Task.Run(async () => Task.Run(async () =>
{ {
await audioService.StopAsync(); await audioService.StopAsync();
@@ -321,6 +341,7 @@ public sealed class AudioPlayerDialog
{ {
UpdateStatus(); UpdateStatus();
StopAnimation(); StopAnimation();
isBusy = false;
}); });
}); });
}; };
@@ -337,6 +358,8 @@ public sealed class AudioPlayerDialog
volDownButton.Accepting += (s, e) => volDownButton.Accepting += (s, e) =>
{ {
e.Handled = true; e.Handled = true;
if (isBusy) return;
isBusy = true;
var newVol = (byte)Math.Max(0, currentVolume - 10); var newVol = (byte)Math.Max(0, currentVolume - 10);
Task.Run(async () => Task.Run(async () =>
{ {
@@ -345,6 +368,7 @@ public sealed class AudioPlayerDialog
{ {
volumeBar.SetNeedsDraw(); volumeBar.SetNeedsDraw();
volumePercentLabel.SetNeedsDraw(); volumePercentLabel.SetNeedsDraw();
isBusy = false;
}); });
}); });
}; };
@@ -352,6 +376,8 @@ public sealed class AudioPlayerDialog
volUpButton.Accepting += (s, e) => volUpButton.Accepting += (s, e) =>
{ {
e.Handled = true; e.Handled = true;
if (isBusy) return;
isBusy = true;
var newVol = (byte)Math.Min(100, currentVolume + 10); var newVol = (byte)Math.Min(100, currentVolume + 10);
Task.Run(async () => Task.Run(async () =>
{ {
@@ -360,6 +386,7 @@ public sealed class AudioPlayerDialog
{ {
volumeBar.SetNeedsDraw(); volumeBar.SetNeedsDraw();
volumePercentLabel.SetNeedsDraw(); volumePercentLabel.SetNeedsDraw();
isBusy = false;
}); });
}); });
}; };