using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Linq; using System.Reactive; using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Media.Imaging; using Avalonia.Platform; using Avalonia.Threading; using ReactiveUI; using RedBookPlayer.GUI.Views; using RedBookPlayer.Models.Hardware; namespace RedBookPlayer.GUI.ViewModels { public class PlayerViewModel : ReactiveObject { /// /// Player representing the internal state /// private Player _player; /// /// Set of images representing the digits for the UI /// private Image[] _digits; #region Player Passthrough #region OpticalDisc Passthrough /// /// Current track number /// public int CurrentTrackNumber { get => _currentTrackNumber; private set => this.RaiseAndSetIfChanged(ref _currentTrackNumber, value); } /// /// Current track index /// public ushort CurrentTrackIndex { get => _currentTrackIndex; private set => this.RaiseAndSetIfChanged(ref _currentTrackIndex, value); } /// /// Current sector number /// public ulong CurrentSector { get => _currentSector; private set => this.RaiseAndSetIfChanged(ref _currentSector, value); } /// /// Represents the sector starting the section /// public ulong SectionStartSector { get => _sectionStartSector; private set => this.RaiseAndSetIfChanged(ref _sectionStartSector, value); } /// /// Represents if the disc has a hidden track /// public bool HiddenTrack { get => _hasHiddenTrack; private set => this.RaiseAndSetIfChanged(ref _hasHiddenTrack, value); } /// /// Represents the 4CH flag [CompactDisc only] /// public bool QuadChannel { get => _quadChannel; private set => this.RaiseAndSetIfChanged(ref _quadChannel, value); } /// /// Represents the DATA flag [CompactDisc only] /// public bool IsDataTrack { get => _isDataTrack; private set => this.RaiseAndSetIfChanged(ref _isDataTrack, value); } /// /// Represents the DCP flag [CompactDisc only] /// public bool CopyAllowed { get => _copyAllowed; private set => this.RaiseAndSetIfChanged(ref _copyAllowed, value); } /// /// Represents the PRE flag [CompactDisc only] /// public bool TrackHasEmphasis { get => _trackHasEmphasis; private set => this.RaiseAndSetIfChanged(ref _trackHasEmphasis, value); } /// /// Represents the total tracks on the disc /// public int TotalTracks => _player.TotalTracks; /// /// Represents the total indices on the disc /// public int TotalIndexes => _player.TotalIndexes; /// /// Total sectors in the image /// public ulong TotalSectors => _player.TotalSectors; /// /// Represents the time adjustment offset for the disc /// public ulong TimeOffset => _player.TimeOffset; /// /// Represents the total playing time for the disc /// public ulong TotalTime => _player.TotalTime; private int _currentTrackNumber; private ushort _currentTrackIndex; private ulong _currentSector; private ulong _sectionStartSector; private bool _hasHiddenTrack; private bool _quadChannel; private bool _isDataTrack; private bool _copyAllowed; private bool _trackHasEmphasis; #endregion #region SoundOutput Passthrough /// /// Indicate if the model is ready to be used /// public bool Initialized => _player?.Initialized ?? false; /// /// Indicate if the output is playing /// public bool? Playing { get => _playing; private set => this.RaiseAndSetIfChanged(ref _playing, value); } /// /// Indicates if de-emphasis should be applied /// public bool ApplyDeEmphasis { get => _applyDeEmphasis; private set => this.RaiseAndSetIfChanged(ref _applyDeEmphasis, value); } /// /// Current playback volume /// public int Volume { get => _volume; private set => this.RaiseAndSetIfChanged(ref _volume, value); } private bool? _playing; private bool _applyDeEmphasis; private int _volume; #endregion #endregion #region Commands /// /// Command for loading a disc /// public ReactiveCommand LoadCommand { get; } #region Playback /// /// Command for beginning playback /// public ReactiveCommand PlayCommand { get; } /// /// Command for pausing playback /// public ReactiveCommand PauseCommand { get; } /// /// Command for pausing playback /// public ReactiveCommand TogglePlayPauseCommand { get; } /// /// Command for stopping playback /// public ReactiveCommand StopCommand { get; } /// /// Command for moving to the next track /// public ReactiveCommand NextTrackCommand { get; } /// /// Command for moving to the previous track /// public ReactiveCommand PreviousTrackCommand { get; } /// /// Command for moving to the next index /// public ReactiveCommand NextIndexCommand { get; } /// /// Command for moving to the previous index /// public ReactiveCommand PreviousIndexCommand { get; } /// /// Command for fast forwarding /// public ReactiveCommand FastForwardCommand { get; } /// /// Command for rewinding /// public ReactiveCommand RewindCommand { get; } #endregion #region Volume /// /// Command for incrementing volume /// public ReactiveCommand VolumeUpCommand { get; } /// /// Command for decrementing volume /// public ReactiveCommand VolumeDownCommand { get; } /// /// Command for toggling mute /// public ReactiveCommand ToggleMuteCommand { get; } #endregion #region Emphasis /// /// Command for enabling de-emphasis /// public ReactiveCommand EnableDeEmphasisCommand { get; } /// /// Command for disabling de-emphasis /// public ReactiveCommand DisableDeEmphasisCommand { get; } /// /// Command for toggling de-emphasis /// public ReactiveCommand ToggleDeEmphasisCommand { get; } #endregion #endregion /// /// Constructor /// public PlayerViewModel() { LoadCommand = ReactiveCommand.Create(ExecuteLoad); PlayCommand = ReactiveCommand.Create(ExecutePlay); PauseCommand = ReactiveCommand.Create(ExecutePause); TogglePlayPauseCommand = ReactiveCommand.Create(ExecuteTogglePlayPause); StopCommand = ReactiveCommand.Create(ExecuteStop); NextTrackCommand = ReactiveCommand.Create(ExecuteNextTrack); PreviousTrackCommand = ReactiveCommand.Create(ExecutePreviousTrack); NextIndexCommand = ReactiveCommand.Create(ExecuteNextIndex); PreviousIndexCommand = ReactiveCommand.Create(ExecutePreviousIndex); FastForwardCommand = ReactiveCommand.Create(ExecuteFastForward); RewindCommand = ReactiveCommand.Create(ExecuteRewind); VolumeUpCommand = ReactiveCommand.Create(ExecuteVolumeUp); VolumeDownCommand = ReactiveCommand.Create(ExecuteVolumeDown); ToggleMuteCommand = ReactiveCommand.Create(ExecuteToggleMute); EnableDeEmphasisCommand = ReactiveCommand.Create(ExecuteEnableDeEmphasis); DisableDeEmphasisCommand = ReactiveCommand.Create(ExecuteDisableDeEmphasis); ToggleDeEmphasisCommand = ReactiveCommand.Create(ExecuteToggleDeEmphasis); } /// /// Initialize the view model with a given image path /// /// Path to the disc image /// Generate a TOC if the disc is missing one [CompactDisc only] /// Load hidden tracks for playback [CompactDisc only] /// Load data tracks for playback [CompactDisc only] /// True if playback should begin immediately, false otherwise /// Default volume between 0 and 100 to use when starting playback public void Init(string path, bool generateMissingToc, bool loadHiddenTracks, bool loadDataTracks, bool autoPlay, int defaultVolume) { // Stop current playback, if necessary if(Playing != null) ExecuteStop(); // Create and attempt to initialize new Player _player = new Player(path, generateMissingToc, loadHiddenTracks, loadDataTracks, autoPlay, defaultVolume); if(Initialized) { _player.PropertyChanged += PlayerStateChanged; PlayerStateChanged(this, null); } } #region Playback /// /// Begin playback /// public void ExecutePlay() => _player?.Play(); /// /// Pause current playback /// public void ExecutePause() => _player?.Pause(); /// /// Toggle playback /// public void ExecuteTogglePlayPause() => _player?.TogglePlayback(); /// /// Stop current playback /// public void ExecuteStop() => _player?.Stop(); /// /// Move to the next playable track /// public void ExecuteNextTrack() => _player?.NextTrack(); /// /// Move to the previous playable track /// public void ExecutePreviousTrack() => _player?.PreviousTrack(); /// /// Move to the next index /// public void ExecuteNextIndex() => _player?.NextIndex(App.Settings.IndexButtonChangeTrack); /// /// Move to the previous index /// public void ExecutePreviousIndex() => _player?.PreviousIndex(App.Settings.IndexButtonChangeTrack); /// /// Fast-forward playback by 75 sectors, if possible /// public void ExecuteFastForward() => _player?.FastForward(); /// /// Rewind playback by 75 sectors, if possible /// public void ExecuteRewind() => _player?.Rewind(); #endregion #region Volume /// /// Increment the volume value /// public void ExecuteVolumeUp() => _player?.VolumeUp(); /// /// Decrement the volume value /// public void ExecuteVolumeDown() => _player?.VolumeDown(); /// /// Set the value for the volume /// /// New volume value public void ExecuteSetVolume(int volume) => _player?.SetVolume(volume); /// /// Temporarily mute playback /// public void ExecuteToggleMute() => _player?.ToggleMute(); #endregion #region Emphasis /// /// Enable de-emphasis /// public void ExecuteEnableDeEmphasis() => _player?.EnableDeEmphasis(); /// /// Disable de-emphasis /// public void ExecuteDisableDeEmphasis() => _player?.DisableDeEmphasis(); /// /// Toggle de-emphasis /// public void ExecuteToggleDeEmphasis() => _player?.ToggleDeEmphasis(); #endregion #region Helpers /// /// Load a disc image from a selection box /// public async void ExecuteLoad() { string path = await GetPath(); if(path == null) return; await LoadImage(path); } /// /// Initialize the displayed digits array /// public void InitializeDigits() { PlayerView playerView = MainWindow.Instance.ContentControl.Content as PlayerView; _digits = new Image[] { playerView.FindControl("TrackDigit1"), playerView.FindControl("TrackDigit2"), playerView.FindControl("IndexDigit1"), playerView.FindControl("IndexDigit2"), playerView.FindControl("TimeDigit1"), playerView.FindControl("TimeDigit2"), playerView.FindControl("TimeDigit3"), playerView.FindControl("TimeDigit4"), playerView.FindControl("TimeDigit5"), playerView.FindControl("TimeDigit6"), playerView.FindControl("TotalTracksDigit1"), playerView.FindControl("TotalTracksDigit2"), playerView.FindControl("TotalIndexesDigit1"), playerView.FindControl("TotalIndexesDigit2"), playerView.FindControl("TotalTimeDigit1"), playerView.FindControl("TotalTimeDigit2"), playerView.FindControl("TotalTimeDigit3"), playerView.FindControl("TotalTimeDigit4"), playerView.FindControl("TotalTimeDigit5"), playerView.FindControl("TotalTimeDigit6"), }; } /// /// Load an image from the path /// /// Path to the image to load public async Task LoadImage(string path) { return await Dispatcher.UIThread.InvokeAsync(() => { Init(path, App.Settings.GenerateMissingTOC, App.Settings.PlayHiddenTracks, App.Settings.PlayDataTracks, App.Settings.AutoPlay, App.Settings.Volume); if(Initialized) MainWindow.Instance.Title = "RedBookPlayer - " + path.Split('/').Last().Split('\\').Last(); return Initialized; }); } /// /// Set the value for loading data tracks [CompactDisc only] /// /// True to enable loading data tracks, false otherwise public void SetLoadDataTracks(bool load) => _player?.SetLoadDataTracks(load); /// /// Set the value for loading hidden tracks [CompactDisc only] /// /// True to enable loading hidden tracks, false otherwise public void SetLoadHiddenTracks(bool load) => _player?.SetLoadHiddenTracks(load); /// /// Generate the digit string to be interpreted by the frontend /// /// String representing the digits for the frontend private string GenerateDigitString() { // If the disc isn't initialized, return all '-' characters if(Initialized != true) return string.Empty.PadLeft(20, '-'); int usableTrackNumber = CurrentTrackNumber; if(usableTrackNumber < 0) usableTrackNumber = 0; else if(usableTrackNumber > 99) usableTrackNumber = 99; // Otherwise, take the current time into account ulong sectorTime = GetCurrentSectorTime(); int[] numbers = new int[] { usableTrackNumber, CurrentTrackIndex, (int)(sectorTime / (75 * 60)), (int)(sectorTime / 75 % 60), (int)(sectorTime % 75), TotalTracks, TotalIndexes, (int)(TotalTime / (75 * 60)), (int)(TotalTime / 75 % 60), (int)(TotalTime % 75), }; return string.Join("", numbers.Select(i => i.ToString().PadLeft(2, '0').Substring(0, 2))); } /// /// Load the png image for a given character based on the theme /// /// Character to load the image for /// Bitmap representing the loaded image private Bitmap GetBitmap(char character) { try { if(App.Settings.SelectedTheme == "default") { IAssetLoader assets = AvaloniaLocator.Current.GetService(); return new Bitmap(assets.Open(new Uri($"avares://RedBookPlayer/Assets/{character}.png"))); } else { string themeDirectory = $"{Directory.GetCurrentDirectory()}/themes/{App.Settings.SelectedTheme}"; using FileStream stream = File.Open($"{themeDirectory}/{character}.png", FileMode.Open); return new Bitmap(stream); } } catch { return null; } } /// /// Get current sector time, accounting for offsets /// /// ulong representing the current sector time private ulong GetCurrentSectorTime() { ulong sectorTime = CurrentSector; if(SectionStartSector != 0) sectorTime -= SectionStartSector; else if(CurrentTrackNumber > 0) sectorTime += TimeOffset; return sectorTime; } /// /// Generate a path selection dialog box /// /// User-selected path, if possible private async Task GetPath() { return await Dispatcher.UIThread.InvokeAsync(async () => { var dialog = new OpenFileDialog { AllowMultiple = false }; List knownExtensions = new Aaru.DiscImages.AaruFormat().KnownExtensions.ToList(); dialog.Filters.Add(new FileDialogFilter() { Name = "Aaru Image Format (*" + string.Join(", *", knownExtensions) + ")", Extensions = knownExtensions.ConvertAll(e => e.TrimStart('.')) }); return (await dialog.ShowAsync(MainWindow.Instance))?.FirstOrDefault(); }); } /// /// Update the view-model from the Player /// private void PlayerStateChanged(object sender, PropertyChangedEventArgs e) { if(_player?.Initialized != true) return; CurrentTrackNumber = _player.CurrentTrackNumber; CurrentTrackIndex = _player.CurrentTrackIndex; CurrentSector = _player.CurrentSector; SectionStartSector = _player.SectionStartSector; HiddenTrack = _player.HiddenTrack; QuadChannel = _player.QuadChannel; IsDataTrack = _player.IsDataTrack; CopyAllowed = _player.CopyAllowed; TrackHasEmphasis = _player.TrackHasEmphasis; Playing = _player.Playing; ApplyDeEmphasis = _player.ApplyDeEmphasis; Volume = _player.Volume; Dispatcher.UIThread.InvokeAsync(() => { string digitString = GenerateDigitString() ?? string.Empty.PadLeft(20, '-'); for(int i = 0; i < _digits.Length; i++) { Bitmap digitImage = GetBitmap(digitString[i]); if(_digits[i] != null && digitImage != null) _digits[i].Source = digitImage; } }); } #endregion } }