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; } }