diff --git a/ViewModels/MainViewModel.Trax.cs b/ViewModels/MainViewModel.Trax.cs
new file mode 100644
index 0000000..6afb6aa
--- /dev/null
+++ b/ViewModels/MainViewModel.Trax.cs
@@ -0,0 +1,132 @@
+using System.IO;
+using System.Windows.Media;
+using Microsoft.Win32;
+using ytLive.Helpers;
+using ytLive.Models;
+using ytLive.Services.Audio;
+
+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();
+ private Music? _music;
+ private bool _musicPlaying;
+
+ private static readonly SolidColorBrush TraxIdleBrush = CreateBrush("#555");
+ /// True while the TRAX background track is playing.
+ public bool IsMusicPlaying
+ {
+ get => _musicPlaying;
+ private set
+ {
+ if (SetProperty(ref _musicPlaying, value))
+ {
+ OnPropertyChanged(nameof(TraxDotBrush));
+ OnPropertyChanged(nameof(TraxToolTip));
+ }
+ }
+ }
+
+ private string? _traxError;
+
+ /// Error message from the last TRAX load/playback failure; null = no error.
+ public string? TraxError
+ {
+ get => _traxError;
+ private set => SetProperty(ref _traxError, value);
+ }
+
+ /// TRAX status dot: green = playing, gray = idle or no track.
+ public SolidColorBrush TraxDotBrush => _musicPlayer.IsPlaying
+ ? BarOnBrush
+ : TraxIdleBrush;
+
+ public string TraxToolTip => _musicPlayer.TrackName != null
+ ? $"TRAX: {_musicPlayer.TrackName} — left-click {(IsMusicPlaying ? "pauses" : "plays")}, right-click chooses a track"
+ : "No track — left-click or right-click to choose background music";
+
+ /// Left-click on TRAX: no track → pick one; otherwise toggle play/pause.
+ public void TraxLeftClick()
+ {
+ if (_music == null || string.IsNullOrWhiteSpace(_music.TrackPath))
+ PickTraxTrack();
+ else if (IsMusicPlaying)
+ PauseTrax();
+ else
+ PlayTrax();
+ }
+
+ /// Right-click on TRAX: always open the track picker (never the OS viewer).
+ public void TraxRightClick() => PickTraxTrack();
+
+ private void PickTraxTrack()
+ {
+ var dialog = new OpenFileDialog
+ {
+ Title = "Choose background music",
+ Filter = "Music (*.mp3;*.wav;*.m4a;*.aac;*.flac;*.ogg)|*.mp3;*.wav;*.m4a;*.aac;*.flac;*.ogg|All files (*.*)|*.*",
+ };
+ if (dialog.ShowDialog() != true)
+ return;
+
+ _music = new Music { TrackPath = dialog.FileName, IsEnabled = true };
+ LoadTrax(play: true);
+ }
+
+ private void LoadTrax(bool play)
+ {
+ if (_music == null || string.IsNullOrWhiteSpace(_music.TrackPath))
+ return;
+
+ try
+ {
+ _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}");
+ TraxError = $"Failed to load: {Path.GetFileName(_music.TrackPath)} — {ex.Message}";
+ }
+ RefreshTraxUi();
+ ScheduleSave();
+ }
+
+ private void PlayTrax() => _musicPlayer.Play();
+
+ private void PauseTrax() => _musicPlayer.Pause();
+
+ private void OnTraxPlaybackEnded(Exception? ex)
+ {
+ if (ex != null)
+ {
+ AppLog.Write($"TRAX: playback error: {ex.Message}");
+ TraxError = $"Playback error: {ex.Message}";
+ }
+ RefreshTraxUi();
+ }
+
+ private void RefreshTraxUi()
+ {
+ IsMusicPlaying = _musicPlayer.IsPlaying;
+ OnPropertyChanged(nameof(TraxDotBrush));
+ OnPropertyChanged(nameof(TraxToolTip));
+ }
+
+ /// Mirrors the desktop/game audio bar's volume and mute onto the
+ /// music so the creator hears the controls work in the headphones (the
+ /// track rides the loopback channel, so it must duck the same way locally).
+ /// Called whenever the game volume/mute change and on TRAX load.
+ private void UpdateTraxLocalGain()
+ {
+ _musicPlayer.LocalGain = GameMuted ? 0f : 1f;
+ }
+}
diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs
index 9f716eb..34ffa95 100644
--- a/ViewModels/MainViewModel.cs
+++ b/ViewModels/MainViewModel.cs
@@ -31,12 +31,6 @@ public partial class MainViewModel : ViewModelBase
private SceneElement? _selectedElement;
- // 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();
- private Music? _music;
- private bool _musicPlaying;
private StreamStatus _streamStatus = StreamStatus.Offline;
private StreamHealth _currentHealth = new();
private string _streamTitle = string.Empty;
@@ -234,121 +228,6 @@ public partial class MainViewModel : ViewModelBase
}
}
- private static readonly SolidColorBrush TraxIdleBrush = CreateBrush("#555");
- /// True while the TRAX background track is playing.
- public bool IsMusicPlaying
- {
- get => _musicPlaying;
- private set
- {
- if (SetProperty(ref _musicPlaying, value))
- {
- OnPropertyChanged(nameof(TraxDotBrush));
- OnPropertyChanged(nameof(TraxToolTip));
- }
- }
- }
-
- private string? _traxError;
-
- /// Error message from the last TRAX load/playback failure; null = no error.
- public string? TraxError
- {
- get => _traxError;
- private set => SetProperty(ref _traxError, value);
- }
-
- /// TRAX status dot: green = playing, gray = idle or no track.
- public SolidColorBrush TraxDotBrush => _musicPlayer.IsPlaying
- ? BarOnBrush
- : TraxIdleBrush;
-
- public string TraxToolTip => _musicPlayer.TrackName != null
- ? $"TRAX: {_musicPlayer.TrackName} — left-click {(IsMusicPlaying ? "pauses" : "plays")}, right-click chooses a track"
- : "No track — left-click or right-click to choose background music";
-
- /// Left-click on TRAX: no track → pick one; otherwise toggle play/pause.
- public void TraxLeftClick()
- {
- if (_music == null || string.IsNullOrWhiteSpace(_music.TrackPath))
- PickTraxTrack();
- else if (IsMusicPlaying)
- PauseTrax();
- else
- PlayTrax();
- }
-
- /// Right-click on TRAX: always open the track picker (never the OS viewer).
- public void TraxRightClick() => PickTraxTrack();
-
- private void PickTraxTrack()
- {
- var dialog = new OpenFileDialog
- {
- Title = "Choose background music",
- Filter = "Music (*.mp3;*.wav;*.m4a;*.aac;*.flac;*.ogg)|*.mp3;*.wav;*.m4a;*.aac;*.flac;*.ogg|All files (*.*)|*.*",
- };
- if (dialog.ShowDialog() != true)
- return;
-
- _music = new Music { TrackPath = dialog.FileName, IsEnabled = true };
- LoadTrax(play: true);
- }
-
- private void LoadTrax(bool play)
- {
- if (_music == null || string.IsNullOrWhiteSpace(_music.TrackPath))
- return;
-
- try
- {
- _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}");
- TraxError = $"Failed to load: {Path.GetFileName(_music.TrackPath)} — {ex.Message}";
- }
- RefreshTraxUi();
- ScheduleSave();
- }
-
- private void PlayTrax() => _musicPlayer.Play();
-
- private void PauseTrax() => _musicPlayer.Pause();
-
- private void OnTraxPlaybackEnded(Exception? ex)
- {
- if (ex != null)
- {
- AppLog.Write($"TRAX: playback error: {ex.Message}");
- TraxError = $"Playback error: {ex.Message}";
- }
- RefreshTraxUi();
- }
-
- private void RefreshTraxUi()
- {
- IsMusicPlaying = _musicPlayer.IsPlaying;
- OnPropertyChanged(nameof(TraxDotBrush));
- OnPropertyChanged(nameof(TraxToolTip));
- }
-
- /// Mirrors the desktop/game audio bar's volume and mute onto the
- /// music so the creator hears the controls work in the headphones (the
- /// track rides the loopback channel, so it must duck the same way locally).
- /// Called whenever the game volume/mute change and on TRAX load.
- private void UpdateTraxLocalGain()
- {
- _musicPlayer.LocalGain = GameMuted ? 0f : 1f;
- }
-
-
public bool IsOffline => StreamStatus == StreamStatus.Offline;
public bool IsLive => StreamStatus == StreamStatus.Streaming;
public bool IsLivePrivate => IsLive && string.Equals(StreamVisibility, "Private", StringComparison.OrdinalIgnoreCase);
diff --git a/ViewModels/index.md b/ViewModels/index.md
index 9ee6889..ca393e8 100644
--- a/ViewModels/index.md
+++ b/ViewModels/index.md
@@ -68,14 +68,20 @@ Progress (Commit N/12 · tag):
`OnLoopbackLevelChanged`/`OnMicConnected`/`OnMicFailed`, `StartMicCaptureAsync`,
audio fields + `ToggleMicMuteCommand`/`ToggleGameMuteCommand`/`OpenMicPickerCommand`).
TRAX stays in core (next commit).
+- **Commit 7/12 · `refactor-commit-7`: Trax landed** → `MainViewModel.Trax.cs`
+ (`_musicPlayer`/`_music`/`_musicPlaying` + `TraxIdleBrush`, `IsMusicPlaying`/
+ `TraxError`/`TraxDotBrush`/`TraxToolTip`, `TraxLeftClick`/`TraxRightClick`/
+ `PickTraxTrack`/`LoadTrax`/`PlayTrax`/`PauseTrax`/`OnTraxPlaybackEnded`/
+ `RefreshTraxUi`/`UpdateTraxLocalGain`). Core keeps the ctor hookup
+ (`_musicPlayer.PlaybackEnded += ...`), the `LoadLayout`/`Shutdown`/
+ `SaveLayoutNow` call sites, and the Scenes partial's hotkey dispatch into `TraxLeftClick`.
- Core file keeps: fields, ctor, `Shutdown`/`SaveLayoutNow`/`ScheduleSave`/`LoadLayout`,
`SetPanel`/`ShowOverlay`/`ShowLicenses`, `CreateBrush`, `OpenUrl`/`ComposeEmail`,
`SubmitBug`/`SubmitFeature`, window-geometry pass-through.
- Partials: `Account` ✔ (Commit 1), `License` ✔ (Commit 2), `Scenes` ✔ (Commit 3), `Background` ✔ (Commit 4),
- `Webcam` ✔ (Commit 5), `Audio` ✔ (Commit 6),
- `Trax` (music player), `Socials` (social bar + dialog),
- `Streaming` (go-live/record + health + quality tiers + media source pipeline),
+ `Webcam` ✔ (Commit 5), `Audio` ✔ (Commit 6), `Trax` ✔ (Commit 7),
+ `Socials` (social bar + dialog), `Streaming` (go-live/record + health + quality tiers + media source pipeline),
`Chat` (YouTube chat + mock preview + chatbox render), `Overlays` (settings/bug/feature/about
panels + license entry).