using System.ComponentModel; using System.Linq; using System.Reactive; using ReactiveUI; using RedBookPlayer.Common.Hardware; namespace RedBookPlayer.GUI.ViewModels { public class PlayerViewModel : ReactiveObject { /// /// Player representing the internal state /// private Player _player; #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 #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() { 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 /// /// Generate the digit string to be interpreted by the frontend /// /// String representing the digits for the frontend public 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))); } /// /// 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); /// /// 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; } /// /// 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; } #endregion } }