TRAX: toggle refreshes the status light on play/pause, terse scrolling error on failure, fixed quiet volume (0.5), ITraxPlayer seam. Deadlocked test fixed via RunAsync (was GetAwaiter().GetResult on the UI thread). 416 pass / 1 known env flake. Refs: https://learn.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/async-programming-model-tap#dont-block-on-async-code (Unit: trax toggle)
This commit is contained in:
@@ -10,9 +10,13 @@ namespace ytLive.ViewModels;
|
||||
public partial class MainViewModel
|
||||
{
|
||||
// TRAX (TASK 9): free background music at a fixed quiet volume, playing to
|
||||
// the default device so it rides the loopback into the stream. The dot is
|
||||
// red (no track) / yellow (loaded, stopped) / green (playing).
|
||||
private readonly MusicPlayer _musicPlayer = new();
|
||||
// the default device so it rides the loopback into the stream. The button is a
|
||||
// toggle; its dot is green (playing) / red (error — a terse message scrolls in the
|
||||
// desktop-audio bar) / gray (idle or no track).
|
||||
private readonly ITraxPlayer _musicPlayer = TraxPlayerOverride?.Invoke() ?? new MusicPlayer();
|
||||
|
||||
/// <summary>Test seam: a fake TRAX player (no audio device). Set before constructing.</summary>
|
||||
internal static Func<ITraxPlayer>? TraxPlayerOverride { get; set; }
|
||||
private Music? _music;
|
||||
private bool _musicPlaying;
|
||||
private bool _traxLoadInFlight;
|
||||
@@ -46,13 +50,18 @@ public partial class MainViewModel
|
||||
public string? TraxError
|
||||
{
|
||||
get => _traxError;
|
||||
private set => SetProperty(ref _traxError, value);
|
||||
private set
|
||||
{
|
||||
if (SetProperty(ref _traxError, value))
|
||||
OnPropertyChanged(nameof(TraxDotBrush));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>TRAX status dot: green = playing, gray = idle or no track.</summary>
|
||||
/// <summary>TRAX status dot: green = playing, red = the last load/play failed (the
|
||||
/// message scrolls in the desktop-audio bar), gray = idle or no track.</summary>
|
||||
public SolidColorBrush TraxDotBrush => _musicPlayer.IsPlaying
|
||||
? BarOnBrush
|
||||
: TraxIdleBrush;
|
||||
: TraxError != null ? BarOffBrush : TraxIdleBrush;
|
||||
|
||||
public string TraxToolTip => _musicPlayer.TrackName != null
|
||||
? $"TRAX: {_musicPlayer.TrackName} — left-click {(IsMusicPlaying ? "pauses" : "plays")}, right-click chooses a track"
|
||||
@@ -82,8 +91,15 @@ public partial class MainViewModel
|
||||
if (dialog.ShowDialog() != true)
|
||||
return;
|
||||
|
||||
_music = new Music { TrackPath = dialog.FileName, IsEnabled = true };
|
||||
LoadTrax(play: true);
|
||||
_ = SetTraxTrackAsync(dialog.FileName, play: true);
|
||||
}
|
||||
|
||||
/// <summary>Points TRAX at a track and loads it (optionally playing). The picker's path;
|
||||
/// internal so tests can load a track without the file dialog.</summary>
|
||||
internal Task SetTraxTrackAsync(string path, bool play)
|
||||
{
|
||||
_music = new Music { TrackPath = path, IsEnabled = true };
|
||||
return LoadTraxAsync(play);
|
||||
}
|
||||
|
||||
/// <summary>Probes a path off the UI thread and gives up at <paramref name="budget"/>.
|
||||
@@ -128,20 +144,20 @@ public partial class MainViewModel
|
||||
UpdateTraxLocalGain();
|
||||
music.IsEnabled = true;
|
||||
TraxError = null;
|
||||
if (play)
|
||||
_musicPlayer.Play();
|
||||
loaded = true;
|
||||
if (play)
|
||||
PlayTrax();
|
||||
}
|
||||
else
|
||||
{
|
||||
AppLog.Write($"TRAX: '{path}' unreachable within {TraxProbeBudget.TotalSeconds:0}s — not loading");
|
||||
TraxError = $"Can't reach {Path.GetFileName(path)} — is the drive on?";
|
||||
TraxError = "TRAX: can't reach the track";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLog.Write($"TRAX: failed to load '{music.TrackPath}': {ex.Message}");
|
||||
TraxError = $"Failed to load: {Path.GetFileName(music.TrackPath)} — {ex.Message}";
|
||||
TraxError = "TRAX: can't load the track";
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -152,16 +168,45 @@ public partial class MainViewModel
|
||||
}
|
||||
}
|
||||
|
||||
private void PlayTrax() => _musicPlayer.Play();
|
||||
// Both toggle halves MUST refresh the UI: the dot and IsMusicPlaying used to update only
|
||||
// on load/playback-ended, so pressing TRAX started the music with the light still off —
|
||||
// and because IsMusicPlaying stayed false, the next press tried to play again instead of
|
||||
// pausing (2026-10-09 bug).
|
||||
private void PlayTrax()
|
||||
{
|
||||
try
|
||||
{
|
||||
_musicPlayer.Play();
|
||||
TraxError = _musicPlayer.IsPlaying ? null : "TRAX: no audio output";
|
||||
if (TraxError != null) AppLog.Write("TRAX: Play() returned but playback did not start (no output device?)");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLog.Write($"TRAX: play failed: {ex.Message}");
|
||||
TraxError = "TRAX: can't play the track";
|
||||
}
|
||||
RefreshTraxUi();
|
||||
}
|
||||
|
||||
private void PauseTrax() => _musicPlayer.Pause();
|
||||
private void PauseTrax()
|
||||
{
|
||||
try
|
||||
{
|
||||
_musicPlayer.Pause();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLog.Write($"TRAX: pause failed: {ex.Message}");
|
||||
}
|
||||
RefreshTraxUi();
|
||||
}
|
||||
|
||||
private void OnTraxPlaybackEnded(Exception? ex)
|
||||
{
|
||||
if (ex != null)
|
||||
{
|
||||
AppLog.Write($"TRAX: playback error: {ex.Message}");
|
||||
TraxError = $"Playback error: {ex.Message}";
|
||||
TraxError = "TRAX: playback stopped";
|
||||
}
|
||||
RefreshTraxUi();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user