TRAX: error display, retain track on failure, gray idle dot

1. Error text shows in red inside the desktop audio meter bar (scrollable)
   when TRAX load or playback fails — user gets feedback, not just a
   silent red dot.
2. LoadTrax no longer nulls _music on error — the track path is retained
   so the user sees what failed and can retry.
3. OnTraxPlaybackEnded now receives the NAudio exception and surfaces it.
4. TRAX dot is green (playing) or gray (idle/no track) — no more yellow
   or red states.

207 tests, 0 warnings.
This commit is contained in:
2026-08-18 08:44:21 -07:00
parent 02cb4bc219
commit 30a193b2c9
3 changed files with 28 additions and 10 deletions
+6
View File
@@ -924,6 +924,12 @@
VerticalAlignment="Stretch"/>
<Rectangle Width="2" Fill="#00000080" HorizontalAlignment="Left" Margin="230,0,0,0"
VerticalAlignment="Stretch"/>
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled"
Visibility="{Binding TraxError, Converter={StaticResource NotNullToVis}}"
VerticalAlignment="Center" Margin="4,0">
<TextBlock Text="{Binding TraxError}" Foreground="#e94560" FontSize="9"
FontWeight="SemiBold" VerticalAlignment="Center"/>
</ScrollViewer>
</Grid>
</Border>
<Grid Width="16" Height="16" Cursor="Hand" Margin="0,0,8,0"
+2 -2
View File
@@ -26,7 +26,7 @@ public sealed class MusicPlayer : IDisposable
private float _localGain = 1f;
/// <summary>Raised when playback ends or is stopped (not on natural-end loop).</summary>
public event Action? PlaybackEnded;
public event Action<Exception?>? PlaybackEnded;
public string? TrackPath { get; private set; }
@@ -122,6 +122,6 @@ public sealed class MusicPlayer : IDisposable
return;
}
PlaybackEnded?.Invoke();
PlaybackEnded?.Invoke(e.Exception);
}
}
+20 -8
View File
@@ -476,6 +476,7 @@ public class MainViewModel : ViewModelBase
// ─── Mic status dot (the MIC button) ───
private static readonly SolidColorBrush MicProblemBrush = CreateBrush("#f1c40f");
private static readonly SolidColorBrush TraxIdleBrush = CreateBrush("#555");
/// <summary>Mic connection state, driven by the mixer's MicConnected/MicFailed
/// events and the startup device check (see StartMicCaptureAsync).</summary>
@@ -525,12 +526,19 @@ public class MainViewModel : ViewModelBase
}
}
/// <summary>TRAX status dot: green = playing, yellow = loaded/stopped, red = no track.</summary>
private string? _traxError;
/// <summary>Error message from the last TRAX load/playback failure; null = no error.</summary>
public string? TraxError
{
get => _traxError;
private set => SetProperty(ref _traxError, value);
}
/// <summary>TRAX status dot: green = playing, gray = idle or no track.</summary>
public SolidColorBrush TraxDotBrush => _musicPlayer.IsPlaying
? BarOnBrush
: _music != null && !string.IsNullOrWhiteSpace(_music.TrackPath)
? MicProblemBrush
: BarOffBrush;
: TraxIdleBrush;
public string TraxToolTip => _musicPlayer.TrackName != null
? $"TRAX: {_musicPlayer.TrackName} — left-click {(IsMusicPlaying ? "pauses" : "plays")}, right-click chooses a track"
@@ -574,13 +582,14 @@ public class MainViewModel : ViewModelBase
_musicPlayer.Load(_music.TrackPath);
UpdateTraxLocalGain();
_music.IsEnabled = true;
TraxError = null;
if (play)
_musicPlayer.Play();
}
catch (Exception ex)
{
AppLog.Write($"TRAX: failed to load '{_music.TrackPath}': {ex.Message}");
_music = null;
TraxError = $"Failed to load: {Path.GetFileName(_music.TrackPath)} — {ex.Message}";
}
RefreshTraxUi();
ScheduleSave();
@@ -590,10 +599,13 @@ public class MainViewModel : ViewModelBase
private void PauseTrax() => _musicPlayer.Pause();
private void OnTraxPlaybackEnded()
private void OnTraxPlaybackEnded(Exception? ex)
{
// Natural end loops inside the player; here only an explicit stop or a
// device failure lands — flip the dot back to loaded/stopped.
if (ex != null)
{
AppLog.Write($"TRAX: playback error: {ex.Message}");
TraxError = $"Playback error: {ex.Message}";
}
RefreshTraxUi();
}