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:
2026-10-09 19:05:04 -07:00
parent 87f05ba2ce
commit 5685c5dc3a
6 changed files with 199 additions and 27 deletions
+60 -15
View File
@@ -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();
}