Files
LlamaCasty/ViewModels/MainViewModel.Trax.cs
gramps 535a50192c refactor: split Trax out of MainViewModel — Commit 7/12
MainViewModel.Trax.cs: music-player state (_musicPlayer/_music/_musicPlaying,
TraxIdleBrush), TRAX UI (IsMusicPlaying/TraxError/TraxDotBrush/TraxToolTip),
interaction (TraxLeftClick/TraxRightClick/PickTraxTrack), playback
(LoadTrax/PlayTrax/PauseTrax/OnTraxPlaybackEnded/RefreshTraxUi), local
loopback ducking (UpdateTraxLocalGain).

Core keeps the ctor event hookup, LoadLayout/Shutdown/SaveLayoutNow call
sites, and Scenes partial's hotkey dispatch.

Verify: 0 warnings, 246 passed, 2 known failures only.
2026-08-30 08:24:22 -07:00

133 lines
4.2 KiB
C#

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");
/// <summary>True while the TRAX background track is playing.</summary>
public bool IsMusicPlaying
{
get => _musicPlaying;
private set
{
if (SetProperty(ref _musicPlaying, value))
{
OnPropertyChanged(nameof(TraxDotBrush));
OnPropertyChanged(nameof(TraxToolTip));
}
}
}
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
: 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";
/// <summary>Left-click on TRAX: no track → pick one; otherwise toggle play/pause.</summary>
public void TraxLeftClick()
{
if (_music == null || string.IsNullOrWhiteSpace(_music.TrackPath))
PickTraxTrack();
else if (IsMusicPlaying)
PauseTrax();
else
PlayTrax();
}
/// <summary>Right-click on TRAX: always open the track picker (never the OS viewer).</summary>
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));
}
/// <summary>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.</summary>
private void UpdateTraxLocalGain()
{
_musicPlayer.LocalGain = GameMuted ? 0f : 1f;
}
}