using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Linq; using System.Reactive; using System.Threading.Tasks; using System.Xml; using Avalonia.Controls; using Avalonia.Markup.Xaml; using Avalonia.Media.Imaging; using Avalonia.Threading; using ReactiveUI; using RedBookPlayer.Models; using RedBookPlayer.Models.Discs; using RedBookPlayer.Models.Hardware; namespace RedBookPlayer.GUI.ViewModels { public class PlayerViewModel : ReactiveObject { /// /// Player representing the internal state /// private readonly Player _player; /// /// Set of images representing the digits for the UI /// private Image[] _digits; #region Player Passthrough /// /// Currently selected disc /// public int CurrentDisc { get => _currentDisc; private set => this.RaiseAndSetIfChanged(ref _currentDisc, value); } private int _currentDisc; #region OpticalDisc Passthrough /// /// Path to the disc image /// public string ImagePath { get => _imagePath; private set => this.RaiseAndSetIfChanged(ref _imagePath, value); } /// /// 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 track session /// public ushort CurrentTrackSession { get => _currentTrackSession; private set => this.RaiseAndSetIfChanged(ref _currentTrackSession, 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 string _imagePath; private int _currentTrackNumber; private ushort _currentTrackIndex; private ushort _currentTrackSession; 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 { get => _initialized; private set => this.RaiseAndSetIfChanged(ref _initialized, value); } /// /// Indicate if the output is playing /// public PlayerState PlayerState { get => _playerState; private set => this.RaiseAndSetIfChanged(ref _playerState, value); } /// /// Indicates how to handle playback of data tracks /// public DataPlayback DataPlayback { get => _dataPlayback; private set => this.RaiseAndSetIfChanged(ref _dataPlayback, value); } /// /// Indicates the repeat mode /// public RepeatMode RepeatMode { get => _repeatMode; private set => this.RaiseAndSetIfChanged(ref _repeatMode, 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 _initialized; private PlayerState _playerState; private DataPlayback _dataPlayback; private RepeatMode _repeatMode; 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 ejecting the current disc /// public ReactiveCommand EjectCommand { get; } /// /// Command for moving to the next disc /// public ReactiveCommand NextDiscCommand { get; } /// /// Command for moving to the previous disc /// public ReactiveCommand PreviousDiscCommand { 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() { // Initialize commands LoadCommand = ReactiveCommand.Create(ExecuteLoad); PlayCommand = ReactiveCommand.Create(ExecutePlay); PauseCommand = ReactiveCommand.Create(ExecutePause); TogglePlayPauseCommand = ReactiveCommand.Create(ExecuteTogglePlayPause); StopCommand = ReactiveCommand.Create(ExecuteStop); EjectCommand = ReactiveCommand.Create(ExecuteEject); NextDiscCommand = ReactiveCommand.Create(ExecuteNextDisc); PreviousDiscCommand = ReactiveCommand.Create(ExecutePreviousDisc); 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 Player _player = new Player(App.Settings.NumberOfDiscs, App.Settings.Volume); PlayerState = PlayerState.NoDisc; } /// /// Initialize the view model with a given image path /// /// Path to the disc image /// Options to pass to the player /// Options to pass to the optical disc factory /// True if playback should begin immediately, false otherwise public void Init(string path, PlayerOptions playerOptions, OpticalDiscOptions opticalDiscOptions, bool autoPlay) { // Stop current playback, if necessary if(PlayerState != PlayerState.NoDisc) ExecuteStop(); // Attempt to initialize Player _player.Init(path, playerOptions, opticalDiscOptions, autoPlay); if(_player.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(); /// /// Eject the currently loaded disc /// public void ExecuteEject() => _player?.Eject(); /// /// Move to the next disc /// public void ExecuteNextDisc() => _player?.NextDisc(); /// /// Move to the previous disc /// public void ExecutePreviousDisc() => _player?.PreviousDisc(); /// /// Move to the next playable track /// public void ExecuteNextTrack() => _player?.NextTrack(); /// /// Move to the previous playable track /// public void ExecutePreviousTrack() => _player?.PreviousTrack(); /// /// Shuffle the current track list /// public void ExecuteShuffle() => _player?.ShuffleTracks(); /// /// 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 /// /// Apply a custom theme to the player /// /// Path to the theme under the themes directory public void ApplyTheme(string theme) { // If the PlayerView isn't set, don't do anything if(App.PlayerView == null) return; // If no theme path is provided, we can ignore if(string.IsNullOrWhiteSpace(theme)) return; string themeDirectory = $"{Directory.GetCurrentDirectory()}/themes/{theme}"; string xamlPath = $"{themeDirectory}/view.xaml"; if(!File.Exists(xamlPath)) { Console.WriteLine("Warning: specified theme doesn't exist, reverting to default"); return; } try { string xaml = File.ReadAllText(xamlPath); xaml = xaml.Replace("Source=\"", $"Source=\"file://{themeDirectory}/"); LoadTheme(xaml); } catch(XmlException ex) { Console.WriteLine($"Error: invalid theme XAML ({ex.Message}), reverting to default"); LoadTheme(null); } App.MainWindow.Width = App.PlayerView.Width; App.MainWindow.Height = App.PlayerView.Height; InitializeDigits(); } /// /// Load a disc image from a selection box /// public async void ExecuteLoad() { string[] paths = await GetPaths(); if(paths == null || paths.Length == 0) { return; } else if(paths.Length == 1) { await LoadImage(paths[0]); } else { int lastDisc = CurrentDisc; foreach(string path in paths) { await LoadImage(path); if(Initialized) ExecuteNextDisc(); } SelectDisc(lastDisc); } } /// /// Initialize the displayed digits array /// public void InitializeDigits() { if(App.PlayerView == null) return; _digits = new Image[] { App.PlayerView.FindControl("TrackDigit1"), App.PlayerView.FindControl("TrackDigit2"), App.PlayerView.FindControl("IndexDigit1"), App.PlayerView.FindControl("IndexDigit2"), App.PlayerView.FindControl("TimeDigit1"), App.PlayerView.FindControl("TimeDigit2"), App.PlayerView.FindControl("TimeDigit3"), App.PlayerView.FindControl("TimeDigit4"), App.PlayerView.FindControl("TimeDigit5"), App.PlayerView.FindControl("TimeDigit6"), App.PlayerView.FindControl("TotalTracksDigit1"), App.PlayerView.FindControl("TotalTracksDigit2"), App.PlayerView.FindControl("TotalIndexesDigit1"), App.PlayerView.FindControl("TotalIndexesDigit2"), App.PlayerView.FindControl("TotalTimeDigit1"), App.PlayerView.FindControl("TotalTimeDigit2"), App.PlayerView.FindControl("TotalTimeDigit3"), App.PlayerView.FindControl("TotalTimeDigit4"), App.PlayerView.FindControl("TotalTimeDigit5"), App.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(() => { PlayerOptions playerOptions = new PlayerOptions { DataPlayback = App.Settings.DataPlayback, DiscHandling = App.Settings.DiscHandling, LoadHiddenTracks = App.Settings.PlayHiddenTracks, RepeatMode = App.Settings.RepeatMode, SessionHandling = App.Settings.SessionHandling, }; OpticalDiscOptions opticalDiscOptions = new OpticalDiscOptions { GenerateMissingToc = App.Settings.GenerateMissingTOC, }; // Ensure the context and view model are set App.PlayerView.DataContext = this; App.PlayerView.ViewModel = this; Init(path, playerOptions, opticalDiscOptions, App.Settings.AutoPlay); if(Initialized) App.MainWindow.Title = "RedBookPlayer - " + path.Split('/').Last().Split('\\').Last(); return Initialized; }); } /// /// Refresh the view model from the current settings /// public void RefreshFromSettings() { SetDataPlayback(App.Settings.DataPlayback); SetDiscHandling(App.Settings.DiscHandling); SetLoadHiddenTracks(App.Settings.PlayHiddenTracks); SetRepeatMode(App.Settings.RepeatMode); SetSessionHandling(App.Settings.SessionHandling); } /// /// Extract a single track from the image to WAV /// /// /// Output path to write data to public void ExtractSingleTrackToWav(uint trackNumber, string outputDirectory) => _player?.ExtractSingleTrackToWav(trackNumber, outputDirectory); /// /// Extract all tracks from the image to WAV /// /// Output path to write data to public void ExtractAllTracksToWav(string outputDirectory) => _player?.ExtractAllTracksToWav(outputDirectory); /// /// Select a particular disc by number /// /// Disc number to attempt to load public void SelectDisc(int discNumber) => _player?.SelectDisc(discNumber); /// /// Select a particular index by number /// /// Track index to attempt to load /// True if index changes can trigger a track change, false otherwise public void SelectIndex(ushort index, bool changeTrack) => _player?.SelectIndex(index, changeTrack); /// /// Select a particular track by number /// /// Track number to attempt to load public void SelectTrack(int trackNumber) => _player?.SelectTrack(trackNumber); /// /// Set data playback method [CompactDisc only] /// /// New playback value public void SetDataPlayback(DataPlayback dataPlayback) => _player?.SetDataPlayback(dataPlayback); /// /// Set disc handling method /// /// New playback value public void SetDiscHandling(DiscHandling discHandling) => _player?.SetDiscHandling(discHandling); /// /// 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); /// /// Set repeat mode /// /// New repeat mode value public void SetRepeatMode(RepeatMode repeatMode) => _player?.SetRepeatMode(repeatMode); /// /// Set session handling /// /// New session handling value public void SetSessionHandling(SessionHandling sessionHandling) => _player?.SetSessionHandling(sessionHandling); /// /// 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 { 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 paths, if possible private async Task GetPaths() { return await Dispatcher.UIThread.InvokeAsync(async () => { var dialog = new OpenFileDialog { AllowMultiple = true }; 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(App.MainWindow)); }); } /// /// Load the theme from a XAML, if possible /// /// XAML data representing the theme, null for default private void LoadTheme(string xaml) { // If the view is null, we can't load the theme if(App.PlayerView == null) return; try { if(xaml != null) new AvaloniaXamlLoader().Load(xaml, null, App.PlayerView); else AvaloniaXamlLoader.Load(App.PlayerView); } catch(Exception ex) { Console.Error.WriteLine(ex); } // Ensure the context and view model are set App.PlayerView.DataContext = this; App.PlayerView.ViewModel = this; UpdateDigits(); } /// /// Update the view-model from the Player /// private void PlayerStateChanged(object sender, PropertyChangedEventArgs e) { if(_player == null) return; if(!_player.Initialized) { Dispatcher.UIThread.InvokeAsync(() => { App.MainWindow.Title = "RedBookPlayer"; }); } ImagePath = _player.ImagePath; Initialized = _player.Initialized; if (!string.IsNullOrWhiteSpace(ImagePath) && Initialized) { Dispatcher.UIThread.InvokeAsync(() => { App.MainWindow.Title = "RedBookPlayer - " + ImagePath.Split('/').Last().Split('\\').Last(); }); } CurrentDisc = _player.CurrentDisc; CurrentTrackNumber = _player.CurrentTrackNumber; CurrentTrackIndex = _player.CurrentTrackIndex; CurrentTrackSession = _player.CurrentTrackSession; CurrentSector = _player.CurrentSector; SectionStartSector = _player.SectionStartSector; HiddenTrack = _player.HiddenTrack; QuadChannel = _player.QuadChannel; IsDataTrack = _player.IsDataTrack; CopyAllowed = _player.CopyAllowed; TrackHasEmphasis = _player.TrackHasEmphasis; PlayerState = _player.PlayerState; DataPlayback = _player.DataPlayback; RepeatMode = _player.RepeatMode; ApplyDeEmphasis = _player.ApplyDeEmphasis; Volume = _player.Volume; UpdateDigits(); } /// /// Update UI /// private void UpdateDigits() { // Ensure the digits if(_digits == null) InitializeDigits(); Dispatcher.UIThread.Post(() => { 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 } }