diff --git a/.gitmodules b/.gitmodules index 38b3a5b..59d2a97 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,4 +3,5 @@ url = https://github.com/aaru-dps/Aaru.git [submodule "cscore"] path = cscore - url = https://github.com/filoe/cscore.git + url = https://github.com/sk-zk/cscore.git + branch = netstandard diff --git a/RedBookPlayer/App.xaml b/RedBookPlayer.GUI/App.xaml similarity index 100% rename from RedBookPlayer/App.xaml rename to RedBookPlayer.GUI/App.xaml diff --git a/RedBookPlayer/App.xaml.cs b/RedBookPlayer.GUI/App.xaml.cs similarity index 97% rename from RedBookPlayer/App.xaml.cs rename to RedBookPlayer.GUI/App.xaml.cs index 03c77fd..20e6a9d 100644 --- a/RedBookPlayer/App.xaml.cs +++ b/RedBookPlayer.GUI/App.xaml.cs @@ -6,6 +6,7 @@ using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using RedBookPlayer.GUI; +using RedBookPlayer.GUI.Views; namespace RedBookPlayer { diff --git a/RedBookPlayer/Assets/-.png b/RedBookPlayer.GUI/Assets/-.png similarity index 100% rename from RedBookPlayer/Assets/-.png rename to RedBookPlayer.GUI/Assets/-.png diff --git a/RedBookPlayer/Assets/0.png b/RedBookPlayer.GUI/Assets/0.png similarity index 100% rename from RedBookPlayer/Assets/0.png rename to RedBookPlayer.GUI/Assets/0.png diff --git a/RedBookPlayer/Assets/1.png b/RedBookPlayer.GUI/Assets/1.png similarity index 100% rename from RedBookPlayer/Assets/1.png rename to RedBookPlayer.GUI/Assets/1.png diff --git a/RedBookPlayer/Assets/2.png b/RedBookPlayer.GUI/Assets/2.png similarity index 100% rename from RedBookPlayer/Assets/2.png rename to RedBookPlayer.GUI/Assets/2.png diff --git a/RedBookPlayer/Assets/3.png b/RedBookPlayer.GUI/Assets/3.png similarity index 100% rename from RedBookPlayer/Assets/3.png rename to RedBookPlayer.GUI/Assets/3.png diff --git a/RedBookPlayer/Assets/4.png b/RedBookPlayer.GUI/Assets/4.png similarity index 100% rename from RedBookPlayer/Assets/4.png rename to RedBookPlayer.GUI/Assets/4.png diff --git a/RedBookPlayer/Assets/5.png b/RedBookPlayer.GUI/Assets/5.png similarity index 100% rename from RedBookPlayer/Assets/5.png rename to RedBookPlayer.GUI/Assets/5.png diff --git a/RedBookPlayer/Assets/6.png b/RedBookPlayer.GUI/Assets/6.png similarity index 100% rename from RedBookPlayer/Assets/6.png rename to RedBookPlayer.GUI/Assets/6.png diff --git a/RedBookPlayer/Assets/7.png b/RedBookPlayer.GUI/Assets/7.png similarity index 100% rename from RedBookPlayer/Assets/7.png rename to RedBookPlayer.GUI/Assets/7.png diff --git a/RedBookPlayer/Assets/8.png b/RedBookPlayer.GUI/Assets/8.png similarity index 100% rename from RedBookPlayer/Assets/8.png rename to RedBookPlayer.GUI/Assets/8.png diff --git a/RedBookPlayer/Assets/9.png b/RedBookPlayer.GUI/Assets/9.png similarity index 100% rename from RedBookPlayer/Assets/9.png rename to RedBookPlayer.GUI/Assets/9.png diff --git a/RedBookPlayer/Assets/colon.png b/RedBookPlayer.GUI/Assets/colon.png similarity index 100% rename from RedBookPlayer/Assets/colon.png rename to RedBookPlayer.GUI/Assets/colon.png diff --git a/RedBookPlayer/Program.cs b/RedBookPlayer.GUI/Program.cs similarity index 95% rename from RedBookPlayer/Program.cs rename to RedBookPlayer.GUI/Program.cs index 731908b..b1919b6 100644 --- a/RedBookPlayer/Program.cs +++ b/RedBookPlayer.GUI/Program.cs @@ -5,7 +5,7 @@ using System.Runtime.InteropServices; using Avalonia; using Avalonia.Logging.Serilog; -namespace RedBookPlayer +namespace RedBookPlayer.GUI { internal class Program { diff --git a/RedBookPlayer/RedBookPlayer.csproj b/RedBookPlayer.GUI/RedBookPlayer.GUI.csproj similarity index 58% rename from RedBookPlayer/RedBookPlayer.csproj rename to RedBookPlayer.GUI/RedBookPlayer.GUI.csproj index bc7ee1e..36d6bb4 100644 --- a/RedBookPlayer/RedBookPlayer.csproj +++ b/RedBookPlayer.GUI/RedBookPlayer.GUI.csproj @@ -2,8 +2,8 @@ WinExe netcoreapp3.1 - true win-x64;linux-x64 + linux-x64 embedded @@ -21,17 +21,9 @@ - - - - - - - - - + diff --git a/RedBookPlayer/Settings.cs b/RedBookPlayer.GUI/Settings.cs similarity index 93% rename from RedBookPlayer/Settings.cs rename to RedBookPlayer.GUI/Settings.cs index 36e95fb..df14848 100644 --- a/RedBookPlayer/Settings.cs +++ b/RedBookPlayer.GUI/Settings.cs @@ -2,9 +2,9 @@ using System; using System.IO; using System.Text.Json; using Avalonia.Input; -using RedBookPlayer.GUI; +using RedBookPlayer.GUI.Views; -namespace RedBookPlayer +namespace RedBookPlayer.GUI { public class Settings { @@ -21,9 +21,14 @@ namespace RedBookPlayer public bool IndexButtonChangeTrack { get; set; } = false; /// - /// Indicates if the index 0 of track 1 is treated like a hidden track + /// Indicates if hidden tracks should be played /// - public bool AllowSkipHiddenTrack { get; set; } = false; + /// + /// Hidden tracks can be one of the following: + /// - TrackSequence == 0 + /// - Larget pregap of track 1 (> 150 sectors) + /// + public bool PlayHiddenTracks { get; set; } = false; /// /// Indicates if data tracks should be played like old, non-compliant players diff --git a/RedBookPlayer.GUI/ViewModels/PlayerViewModel.cs b/RedBookPlayer.GUI/ViewModels/PlayerViewModel.cs new file mode 100644 index 0000000..41edc50 --- /dev/null +++ b/RedBookPlayer.GUI/ViewModels/PlayerViewModel.cs @@ -0,0 +1,672 @@ +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; + + UpdateDigits(); + } + + /// + /// Update UI + /// + private void UpdateDigits() + { + 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 + } +} \ No newline at end of file diff --git a/RedBookPlayer/GUI/MainWindow.xaml b/RedBookPlayer.GUI/Views/MainWindow.xaml similarity index 76% rename from RedBookPlayer/GUI/MainWindow.xaml rename to RedBookPlayer.GUI/Views/MainWindow.xaml index 56dbf79..1a3e211 100644 --- a/RedBookPlayer/GUI/MainWindow.xaml +++ b/RedBookPlayer.GUI/Views/MainWindow.xaml @@ -1,7 +1,7 @@ \ No newline at end of file diff --git a/RedBookPlayer/GUI/MainWindow.xaml.cs b/RedBookPlayer.GUI/Views/MainWindow.xaml.cs similarity index 78% rename from RedBookPlayer/GUI/MainWindow.xaml.cs rename to RedBookPlayer.GUI/Views/MainWindow.xaml.cs index 9c46297..3e5a26a 100644 --- a/RedBookPlayer/GUI/MainWindow.xaml.cs +++ b/RedBookPlayer.GUI/Views/MainWindow.xaml.cs @@ -5,8 +5,9 @@ using System.Xml; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Markup.Xaml; +using RedBookPlayer.GUI.ViewModels; -namespace RedBookPlayer.GUI +namespace RedBookPlayer.GUI.Views { public class MainWindow : Window { @@ -30,10 +31,13 @@ namespace RedBookPlayer.GUI if(string.IsNullOrWhiteSpace(theme)) return; + // If we already have a view, cache the view model + PlayerViewModel pvm = ((PlayerView)Instance.ContentControl.Content).PlayerViewModel; + // If the theme name is "default", we assume the internal theme is used if(theme.Equals("default", StringComparison.CurrentCultureIgnoreCase)) { - Instance.ContentControl.Content = new PlayerView(); + Instance.ContentControl.Content = new PlayerView(pvm); } else { @@ -50,17 +54,19 @@ namespace RedBookPlayer.GUI { string xaml = File.ReadAllText(xamlPath); xaml = xaml.Replace("Source=\"", $"Source=\"file://{themeDirectory}/"); - Instance.ContentControl.Content = new PlayerView(xaml); + Instance.ContentControl.Content = new PlayerView(xaml, pvm); } catch(XmlException ex) { Console.WriteLine($"Error: invalid theme XAML ({ex.Message}), reverting to default"); - Instance.ContentControl.Content = new PlayerView(); + Instance.ContentControl.Content = new PlayerView(pvm); } } Instance.Width = ((PlayerView)Instance.ContentControl.Content).Width; Instance.Height = ((PlayerView)Instance.ContentControl.Content).Height; + + pvm.InitializeDigits(); } /// @@ -78,6 +84,8 @@ namespace RedBookPlayer.GUI ContentControl.Content = new PlayerView(); + ((PlayerView)ContentControl.Content).PlayerViewModel.InitializeDigits(); + CanResize = false; KeyDown += OnKeyDown; @@ -90,7 +98,7 @@ namespace RedBookPlayer.GUI Closing += (e, f) => { - ((PlayerView)ContentControl.Content).StopButton_Click(this, null); + ((PlayerView)ContentControl.Content).PlayerViewModel.ExecuteStop(); }; AddHandler(DragDrop.DropEvent, MainWindow_Drop); @@ -107,7 +115,7 @@ namespace RedBookPlayer.GUI IEnumerable fileNames = e.Data.GetFileNames(); foreach(string filename in fileNames) { - bool loaded = await playerView.LoadImage(filename); + bool loaded = await playerView?.PlayerViewModel?.LoadImage(filename); if(loaded) break; } @@ -121,61 +129,62 @@ namespace RedBookPlayer.GUI if(e.Key == App.Settings.OpenSettingsKey) { settingsWindow = new SettingsWindow(App.Settings); + settingsWindow.Closed += OnSettingsClosed; settingsWindow.Show(); } // Load image else if (e.Key == App.Settings.LoadImageKey) { - playerView?.LoadButton_Click(this, null); + playerView?.PlayerViewModel?.ExecuteLoad(); } // Toggle playback else if(e.Key == App.Settings.TogglePlaybackKey || e.Key == Key.MediaPlayPause) { - playerView?.PlayPauseButton_Click(this, null); + playerView?.PlayerViewModel?.ExecuteTogglePlayPause(); } // Stop playback else if(e.Key == App.Settings.StopPlaybackKey || e.Key == Key.MediaStop) { - playerView?.StopButton_Click(this, null); + playerView?.PlayerViewModel?.ExecuteStop(); } // Next Track else if(e.Key == App.Settings.NextTrackKey || e.Key == Key.MediaNextTrack) { - playerView?.NextTrackButton_Click(this, null); + playerView?.PlayerViewModel?.ExecuteNextTrack(); } // Previous Track else if(e.Key == App.Settings.PreviousTrackKey || e.Key == Key.MediaPreviousTrack) { - playerView?.PreviousTrackButton_Click(this, null); + playerView?.PlayerViewModel?.ExecutePreviousTrack(); } // Next Index else if(e.Key == App.Settings.NextIndexKey) { - playerView?.NextIndexButton_Click(this, null); + playerView?.PlayerViewModel?.ExecuteNextIndex(); } // Previous Index else if(e.Key == App.Settings.PreviousIndexKey) { - playerView?.PreviousIndexButton_Click(this, null); + playerView?.PlayerViewModel?.ExecutePreviousIndex(); } // Fast Foward else if(e.Key == App.Settings.FastForwardPlaybackKey) { - playerView?.FastForwardButton_Click(this, null); + playerView?.PlayerViewModel?.ExecuteFastForward(); } // Rewind else if(e.Key == App.Settings.RewindPlaybackKey) { - playerView?.RewindButton_Click(this, null); + playerView?.PlayerViewModel?.ExecuteRewind(); } // Volume Up @@ -188,7 +197,7 @@ namespace RedBookPlayer.GUI increment *= 5; if(playerView?.PlayerViewModel?.Volume != null) - playerView.PlayerViewModel.Volume += increment; + playerView.PlayerViewModel.ExecuteSetVolume(playerView.PlayerViewModel.Volume + increment); } // Volume Down @@ -201,22 +210,28 @@ namespace RedBookPlayer.GUI decrement *= 5; if (playerView?.PlayerViewModel?.Volume != null) - playerView.PlayerViewModel.Volume -= decrement; + playerView.PlayerViewModel.ExecuteSetVolume(playerView.PlayerViewModel.Volume - decrement); } // Mute Toggle else if(e.Key == App.Settings.ToggleMuteKey || e.Key == Key.VolumeMute) { - playerView?.MuteToggleButton_Click(this, null); + playerView?.PlayerViewModel?.ExecuteToggleMute(); } // Emphasis Toggle else if(e.Key == App.Settings.ToggleDeEmphasisKey) { - playerView?.EnableDisableDeEmphasisButton_Click(this, null); + playerView?.PlayerViewModel?.ExecuteToggleDeEmphasis(); } } + public void OnSettingsClosed(object sender, EventArgs e) + { + PlayerView playerView = ContentControl.Content as PlayerView; + playerView?.UpdateViewModel(); + } + #endregion } } \ No newline at end of file diff --git a/RedBookPlayer/GUI/PlayerView.xaml b/RedBookPlayer.GUI/Views/PlayerView.xaml similarity index 81% rename from RedBookPlayer/GUI/PlayerView.xaml rename to RedBookPlayer.GUI/Views/PlayerView.xaml index 9221ecd..941522e 100644 --- a/RedBookPlayer/GUI/PlayerView.xaml +++ b/RedBookPlayer.GUI/Views/PlayerView.xaml @@ -1,21 +1,21 @@ + x:Class="RedBookPlayer.GUI.Views.PlayerView" Width="900" Height="400"> - + - - - + + + - - - - - Rewind - Fast Forward + + + + + Rewind + Fast Forward @@ -76,11 +76,11 @@ - - @@ -98,6 +98,7 @@ 4CH HIDDEN HIDDEN + \ No newline at end of file diff --git a/RedBookPlayer.GUI/Views/PlayerView.xaml.cs b/RedBookPlayer.GUI/Views/PlayerView.xaml.cs new file mode 100644 index 0000000..253fd9b --- /dev/null +++ b/RedBookPlayer.GUI/Views/PlayerView.xaml.cs @@ -0,0 +1,73 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using RedBookPlayer.GUI.ViewModels; + +namespace RedBookPlayer.GUI.Views +{ + public class PlayerView : UserControl + { + /// + /// Read-only access to the view model + /// + public PlayerViewModel PlayerViewModel => DataContext as PlayerViewModel; + + /// + /// Initialize the UI based on the default theme + /// + public PlayerView() : this(null, null) { } + + /// + /// Initialize the UI based on the default theme with an existing view model + /// + /// XAML data representing the theme, null for default + /// Existing PlayerViewModel to load in instead of creating a new one + public PlayerView(PlayerViewModel playerViewModel) : this(null, playerViewModel) { } + + /// + /// Initialize the UI based on the currently selected theme + /// + /// XAML data representing the theme, null for default + /// Existing PlayerViewModel to load in instead of creating a new one + public PlayerView(string xaml, PlayerViewModel playerViewModel) + { + LoadTheme(xaml); + + if(playerViewModel != null) + DataContext = playerViewModel; + else + DataContext = new PlayerViewModel(); + } + + #region Helpers + + /// + /// Update the view model with new settings + /// + public void UpdateViewModel() + { + PlayerViewModel.SetLoadDataTracks(App.Settings.PlayDataTracks); + PlayerViewModel.SetLoadHiddenTracks(App.Settings.PlayHiddenTracks); + } + + /// + /// Load the theme from a XAML, if possible + /// + /// XAML data representing the theme, null for default + private void LoadTheme(string xaml) + { + try + { + if(xaml != null) + new AvaloniaXamlLoader().Load(xaml, null, this); + else + AvaloniaXamlLoader.Load(this); + } + catch + { + AvaloniaXamlLoader.Load(this); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/RedBookPlayer/GUI/SettingsWindow.xaml b/RedBookPlayer.GUI/Views/SettingsWindow.xaml similarity index 96% rename from RedBookPlayer/GUI/SettingsWindow.xaml rename to RedBookPlayer.GUI/Views/SettingsWindow.xaml index ba416ba..d64c388 100644 --- a/RedBookPlayer/GUI/SettingsWindow.xaml +++ b/RedBookPlayer.GUI/Views/SettingsWindow.xaml @@ -1,7 +1,7 @@ + d:DesignHeight="450" x:Class="RedBookPlayer.GUI.Views.SettingsWindow" Title="Settings" SizeToContent="WidthAndHeight"> @@ -17,8 +17,8 @@ Index navigation can change track - - Treat index 0 of track 1 as track 0 (hidden track) + + Play hidden tracks diff --git a/RedBookPlayer/GUI/SettingsWindow.xaml.cs b/RedBookPlayer.GUI/Views/SettingsWindow.xaml.cs similarity index 99% rename from RedBookPlayer/GUI/SettingsWindow.xaml.cs rename to RedBookPlayer.GUI/Views/SettingsWindow.xaml.cs index 3f1d226..9b5c15d 100644 --- a/RedBookPlayer/GUI/SettingsWindow.xaml.cs +++ b/RedBookPlayer.GUI/Views/SettingsWindow.xaml.cs @@ -6,7 +6,7 @@ using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; -namespace RedBookPlayer.GUI +namespace RedBookPlayer.GUI.Views { public class SettingsWindow : Window { diff --git a/RedBookPlayer/nuget.config b/RedBookPlayer.GUI/nuget.config similarity index 100% rename from RedBookPlayer/nuget.config rename to RedBookPlayer.GUI/nuget.config diff --git a/RedBookPlayer/Discs/CompactDisc.cs b/RedBookPlayer.Models/Discs/CompactDisc.cs similarity index 54% rename from RedBookPlayer/Discs/CompactDisc.cs rename to RedBookPlayer.Models/Discs/CompactDisc.cs index 21e8fc8..6690b6e 100644 --- a/RedBookPlayer/Discs/CompactDisc.cs +++ b/RedBookPlayer.Models/Discs/CompactDisc.cs @@ -6,11 +6,12 @@ using Aaru.CommonTypes.Interfaces; using Aaru.CommonTypes.Structs; using Aaru.Decoders.CD; using Aaru.Helpers; +using ReactiveUI; using static Aaru.Decoders.CD.FullTOC; -namespace RedBookPlayer.Discs +namespace RedBookPlayer.Models.Discs { - public class CompactDisc : OpticalDisc + public class CompactDisc : OpticalDiscBase, IReactiveObject { #region Public Fields @@ -24,35 +25,52 @@ namespace RedBookPlayer.Discs if(_image == null) return; + // Data tracks only and flag disabled means we can't do anything + if(_image.Tracks.All(t => t.TrackType != TrackType.Audio) && !LoadDataTracks) + return; + // Cache the value and the current track number int cachedValue = value; - int cachedTrackNumber = _currentTrackNumber; + int cachedTrackNumber; // Check if we're incrementing or decrementing the track bool increment = cachedValue >= _currentTrackNumber; do { - // Ensure that the value is valid, wrapping around if necessary - if(cachedValue >= _image.Tracks.Count) - cachedValue = 0; - else if(cachedValue < 0) - cachedValue = _image.Tracks.Count - 1; + // If we're over the last track, wrap around + if(cachedValue > _image.Tracks.Max(t => t.TrackSequence)) + { + cachedValue = (int)_image.Tracks.Min(t => t.TrackSequence); + if(cachedValue == 0 && !LoadHiddenTracks) + cachedValue++; + } - _currentTrackNumber = cachedValue; + // If we're under the first track and we're not loading hidden tracks, wrap around + else if(cachedValue < 1 && !LoadHiddenTracks) + { + cachedValue = (int)_image.Tracks.Max(t => t.TrackSequence); + } + + // If we're under the first valid track, wrap around + else if(cachedValue < _image.Tracks.Min(t => t.TrackSequence)) + { + cachedValue = (int)_image.Tracks.Max(t => t.TrackSequence); + } + + cachedTrackNumber = cachedValue; // Cache the current track for easy access - Track track = _image.Tracks[_currentTrackNumber]; + Track track = GetTrack(cachedTrackNumber); + if(track == null) + return; // Set track flags from subchannel data, if possible SetTrackFlags(track); - TotalIndexes = track.Indexes.Keys.Max(); - CurrentTrackIndex = track.Indexes.Keys.Min(); - // If the track is playable, just return - if(TrackType == TrackType.Audio || App.Settings.PlayDataTracks) - return; + if(TrackType == TrackType.Audio || LoadDataTracks) + break; // If we're not playing the track, skip if(increment) @@ -60,7 +78,28 @@ namespace RedBookPlayer.Discs else cachedValue--; } - while(cachedValue != cachedTrackNumber); + while(cachedValue != _currentTrackNumber); + + // If we looped around, ensure it reloads + if(cachedValue == _currentTrackNumber) + { + this.RaiseAndSetIfChanged(ref _currentTrackNumber, -1); + + Track track = GetTrack(cachedValue); + if(track == null) + return; + + SetTrackFlags(track); + } + + this.RaiseAndSetIfChanged(ref _currentTrackNumber, cachedValue); + + Track cachedTrack = GetTrack(cachedValue); + if(cachedTrack == null) + return; + + TotalIndexes = cachedTrack.Indexes.Keys.Max(); + CurrentTrackIndex = cachedTrack.Indexes.Keys.Min(); } } @@ -75,15 +114,18 @@ namespace RedBookPlayer.Discs return; // Cache the current track for easy access - Track track = _image.Tracks[CurrentTrackNumber]; + Track track = GetTrack(CurrentTrackNumber); + if(track == null) + return; // Ensure that the value is valid, wrapping around if necessary + ushort fixedValue = value; if(value > track.Indexes.Keys.Max()) - _currentTrackIndex = track.Indexes.Keys.Min(); + fixedValue = track.Indexes.Keys.Min(); else if(value < track.Indexes.Keys.Min()) - _currentTrackIndex = track.Indexes.Keys.Max(); - else - _currentTrackIndex = value; + fixedValue = track.Indexes.Keys.Max(); + + this.RaiseAndSetIfChanged(ref _currentTrackIndex, fixedValue); // Set new index-specific data SectionStartSector = (ulong)track.Indexes[CurrentTrackIndex]; @@ -95,25 +137,27 @@ namespace RedBookPlayer.Discs public override ulong CurrentSector { get => _currentSector; - set + protected set { // Unset image means we can't do anything if(_image == null) return; // Cache the current track for easy access - Track track = _image.Tracks[CurrentTrackNumber]; + Track track = GetTrack(CurrentTrackNumber); + if(track == null) + return; - _currentSector = value; + this.RaiseAndSetIfChanged(ref _currentSector, value); - if((CurrentTrackNumber < _image.Tracks.Count - 1 && CurrentSector >= _image.Tracks[CurrentTrackNumber + 1].TrackStartSector) + if((CurrentTrackNumber < _image.Tracks.Count - 1 && CurrentSector >= (GetTrack(CurrentTrackNumber + 1)?.TrackStartSector ?? 0)) || (CurrentTrackNumber > 0 && CurrentSector < track.TrackStartSector)) { foreach(Track trackData in _image.Tracks.ToArray().Reverse()) { if(CurrentSector >= trackData.TrackStartSector) { - CurrentTrackNumber = (int)trackData.TrackSequence - 1; + CurrentTrackNumber = (int)trackData.TrackSequence; break; } } @@ -132,25 +176,59 @@ namespace RedBookPlayer.Discs } } + /// + public override int BytesPerSector => GetTrack(CurrentTrackNumber)?.TrackRawBytesPerSector ?? 0; + /// /// Represents the 4CH flag /// - public bool QuadChannel { get; private set; } = false; + public bool QuadChannel + { + get => _quadChannel; + private set => this.RaiseAndSetIfChanged(ref _quadChannel, value); + } /// /// Represents the DATA flag /// - public bool IsDataTrack => TrackType != TrackType.Audio; + public bool IsDataTrack + { + get => _isDataTrack; + private set => this.RaiseAndSetIfChanged(ref _isDataTrack, value); + } /// /// Represents the DCP flag /// - public bool CopyAllowed { get; private set; } = false; + public bool CopyAllowed + { + get => _copyAllowed; + private set => this.RaiseAndSetIfChanged(ref _copyAllowed, value); + } /// /// Represents the PRE flag /// - public bool TrackHasEmphasis { get; private set; } = false; + public bool TrackHasEmphasis + { + get => _trackHasEmphasis; + private set => this.RaiseAndSetIfChanged(ref _trackHasEmphasis, value); + } + + /// + /// Indicate if data tracks should be loaded + /// + public bool LoadDataTracks { get; set; } = false; + + /// + /// Indicate if hidden tracks should be loaded + /// + public bool LoadHiddenTracks { get; set; } = false; + + private bool _quadChannel; + private bool _isDataTrack; + private bool _copyAllowed; + private bool _trackHasEmphasis; #endregion @@ -171,6 +249,11 @@ namespace RedBookPlayer.Discs /// private ulong _currentSector = 0; + /// + /// Indicate if a TOC should be generated if missing + /// + private readonly bool _generateMissingToc = false; + /// /// Current disc table of contents /// @@ -178,8 +261,21 @@ namespace RedBookPlayer.Discs #endregion + /// + /// Constructor + /// + /// Generate a TOC if the disc is missing one + /// Load hidden tracks for playback + /// Load data tracks for playback + public CompactDisc(bool generateMissingToc, bool loadHiddenTracks, bool loadDataTracks) + { + _generateMissingToc = generateMissingToc; + LoadHiddenTracks = loadHiddenTracks; + LoadDataTracks = loadDataTracks; + } + /// - public override void Init(IOpticalMediaImage image, bool autoPlay = false) + public override void Init(IOpticalMediaImage image, bool autoPlay) { // If the image is null, we can't do anything if(image == null) @@ -200,7 +296,7 @@ namespace RedBookPlayer.Discs TotalIndexes = 0; // Set the internal disc state - TotalTracks = _image.Tracks.Count; + TotalTracks = (int)_image.Tracks.Max(t => t.TrackSequence); TrackDataDescriptor firstTrack = _toc.TrackDescriptors.First(d => d.ADR == 1 && d.POINT == 1); TimeOffset = (ulong)((firstTrack.PMIN * 60 * 75) + (firstTrack.PSEC * 75) + firstTrack.PFRAME); TotalTime = TimeOffset + _image.Tracks.Last().TrackEndSector; @@ -211,24 +307,63 @@ namespace RedBookPlayer.Discs #region Seeking + /// + public override void NextTrack() + { + if(_image == null) + return; + + CurrentTrackNumber++; + LoadTrack(CurrentTrackNumber); + } + + /// + public override void PreviousTrack() + { + if(_image == null) + return; + + CurrentTrackNumber--; + LoadTrack(CurrentTrackNumber); + } + /// public override bool NextIndex(bool changeTrack) { if(_image == null) return false; - if(CurrentTrackIndex + 1 > _image.Tracks[CurrentTrackNumber].Indexes.Keys.Max()) + // Cache the current track for easy access + Track track = GetTrack(CurrentTrackNumber); + if(track == null) + return false; + + // If the index is greater than the highest index, change tracks if needed + if(CurrentTrackIndex + 1 > track.Indexes.Keys.Max()) { if(changeTrack) { NextTrack(); - CurrentSector = (ulong)_image.Tracks[CurrentTrackNumber].Indexes.Values.Min(); + CurrentSector = (ulong)GetTrack(CurrentTrackNumber).Indexes.Values.Min(); return true; } } + + // If the next index has an invalid offset, change tracks if needed + else if(track.Indexes[(ushort)(CurrentTrackIndex + 1)] < 0) + { + if(changeTrack) + { + NextTrack(); + CurrentSector = (ulong)GetTrack(CurrentTrackNumber).Indexes.Values.Min(); + return true; + } + } + + // Otherwise, just move to the next index else { - CurrentSector = (ulong)_image.Tracks[CurrentTrackNumber].Indexes[++CurrentTrackIndex]; + CurrentSector = (ulong)track.Indexes[++CurrentTrackIndex]; } return false; @@ -240,18 +375,37 @@ namespace RedBookPlayer.Discs if(_image == null) return false; - if(CurrentTrackIndex - 1 < _image.Tracks[CurrentTrackNumber].Indexes.Keys.Min()) + // Cache the current track for easy access + Track track = GetTrack(CurrentTrackNumber); + if(track == null) + return false; + + // If the index is less than the lowest index, change tracks if needed + if(CurrentTrackIndex - 1 < track.Indexes.Keys.Min()) { if(changeTrack) { PreviousTrack(); - CurrentSector = (ulong)_image.Tracks[CurrentTrackNumber].Indexes.Values.Max(); + CurrentSector = (ulong)GetTrack(CurrentTrackNumber).Indexes.Values.Max(); return true; } } + + // If the previous index has an invalid offset, change tracks if needed + else if (track.Indexes[(ushort)(CurrentTrackIndex - 1)] < 0) + { + if(changeTrack) + { + PreviousTrack(); + CurrentSector = (ulong)GetTrack(CurrentTrackNumber).Indexes.Values.Max(); + return true; + } + } + + // Otherwise, just move to the previous index else { - CurrentSector = (ulong)_image.Tracks[CurrentTrackNumber].Indexes[--CurrentTrackIndex]; + CurrentSector = (ulong)track.Indexes[--CurrentTrackIndex]; } return false; @@ -264,7 +418,7 @@ namespace RedBookPlayer.Discs /// public override void LoadFirstTrack() { - CurrentTrackNumber = 0; + CurrentTrackNumber = 1; LoadTrack(CurrentTrackNumber); } @@ -274,21 +428,41 @@ namespace RedBookPlayer.Discs if(_image == null) return; - TotalIndexes = _image.Tracks[CurrentTrackNumber].Indexes.Keys.Max(); + TotalIndexes = GetTrack(CurrentTrackNumber)?.Indexes.Keys.Max() ?? 0; } /// - protected override void LoadTrack(int track) + protected override void LoadTrack(int trackNumber) { if(_image == null) return; - if(track < 0 || track >= _image.Tracks.Count) + // If the track number is invalid, just return + if(trackNumber < _image.Tracks.Min(t => t.TrackSequence) || trackNumber > _image.Tracks.Max(t => t.TrackSequence)) return; - ushort firstIndex = _image.Tracks[track].Indexes.Keys.Min(); - int firstSector = _image.Tracks[track].Indexes[firstIndex]; - CurrentSector = (ulong)(firstSector >= 0 ? firstSector : _image.Tracks[track].Indexes[1]); + // Cache the current track for easy access + Track track = GetTrack(trackNumber); + + // Select the first index that has a sector offset greater than or equal to 0 + CurrentSector = (ulong)(track?.Indexes.OrderBy(kvp => kvp.Key).First(kvp => kvp.Value >= 0).Value ?? 0); + } + + /// + /// Get the track with the given sequence value, if possible + /// + /// Track number to retrieve + /// Track object for the requested sequence, null on error + private Track GetTrack(int trackNumber) + { + try + { + return _image.Tracks.FirstOrDefault(t => t.TrackSequence == trackNumber); + } + catch + { + return null; + } } /// @@ -304,7 +478,7 @@ namespace RedBookPlayer.Discs if(_image.Info.ReadableMediaTags?.Contains(MediaTagType.CD_FullTOC) != true) { // Only generate the TOC if we have it set - if(!App.Settings.GenerateMissingTOC) + if(!_generateMissingToc) { Console.WriteLine("Full TOC not found"); return false; @@ -369,8 +543,9 @@ namespace RedBookPlayer.Discs /// Track object to read from private void SetDefaultTrackFlags(Track track) { - QuadChannel = false; TrackType = track.TrackType; + QuadChannel = false; + IsDataTrack = track.TrackType != TrackType.Audio; CopyAllowed = false; TrackHasEmphasis = false; } @@ -390,9 +565,11 @@ namespace RedBookPlayer.Discs byte flags = (byte)(descriptor.CONTROL & 0x0D); TrackHasEmphasis = (flags & (byte)TocControl.TwoChanPreEmph) == (byte)TocControl.TwoChanPreEmph; CopyAllowed = (flags & (byte)TocControl.CopyPermissionMask) == (byte)TocControl.CopyPermissionMask; - TrackType = (flags & (byte)TocControl.DataTrack) == (byte)TocControl.DataTrack ? TrackType.Data : TrackType.Audio; + IsDataTrack = (flags & (byte)TocControl.DataTrack) == (byte)TocControl.DataTrack; QuadChannel = (flags & (byte)TocControl.FourChanNoPreEmph) == (byte)TocControl.FourChanNoPreEmph; + TrackType = IsDataTrack ? TrackType.Data : TrackType.Audio; + return; } catch(Exception) diff --git a/RedBookPlayer/Discs/OpticalDisc.cs b/RedBookPlayer.Models/Discs/OpticalDiscBase.cs similarity index 80% rename from RedBookPlayer/Discs/OpticalDisc.cs rename to RedBookPlayer.Models/Discs/OpticalDiscBase.cs index 817c626..f2ce155 100644 --- a/RedBookPlayer/Discs/OpticalDisc.cs +++ b/RedBookPlayer.Models/Discs/OpticalDiscBase.cs @@ -1,9 +1,10 @@ using Aaru.CommonTypes.Enums; using Aaru.CommonTypes.Interfaces; +using ReactiveUI; -namespace RedBookPlayer.Discs +namespace RedBookPlayer.Models.Discs { - public abstract class OpticalDisc + public abstract class OpticalDiscBase : ReactiveObject { #region Public Fields @@ -25,17 +26,21 @@ namespace RedBookPlayer.Discs /// /// Current sector number /// - public abstract ulong CurrentSector { get; set; } + public abstract ulong CurrentSector { get; protected set; } /// /// Represents the sector starting the section /// - public ulong SectionStartSector { get; protected set; } + public ulong SectionStartSector + { + get => _sectionStartSector; + protected set => this.RaiseAndSetIfChanged(ref _sectionStartSector, value); + } /// /// Number of bytes per sector for the current track /// - public int BytesPerSector => _image.Tracks[CurrentTrackNumber].TrackBytesPerSector; + public abstract int BytesPerSector { get; } /// /// Represents the track type @@ -55,7 +60,7 @@ namespace RedBookPlayer.Discs /// /// Total sectors in the image /// - public ulong TotalSectors => _image.Info.Sectors; + public ulong TotalSectors => _image?.Info.Sectors ?? 0; /// /// Represents the time adjustment offset for the disc @@ -67,6 +72,8 @@ namespace RedBookPlayer.Discs /// public ulong TotalTime { get; protected set; } = 0; + private ulong _sectionStartSector; + #endregion #region Protected State Variables @@ -83,42 +90,19 @@ namespace RedBookPlayer.Discs /// /// Aaruformat image to load /// True if playback should begin immediately, false otherwise - public abstract void Init(IOpticalMediaImage image, bool autoPlay = false); + public abstract void Init(IOpticalMediaImage image, bool autoPlay); #region Seeking /// /// Try to move to the next track, wrapping around if necessary /// - public void NextTrack() - { - if(_image == null) - return; - - CurrentTrackNumber++; - LoadTrack(CurrentTrackNumber); - } + public abstract void NextTrack(); /// /// Try to move to the previous track, wrapping around if necessary /// - public void PreviousTrack() - { - if(_image == null) - return; - - if(CurrentSector < (ulong)_image.Tracks[CurrentTrackNumber].Indexes[1] + 75) - { - if(App.Settings.AllowSkipHiddenTrack && CurrentTrackNumber == 0 && CurrentSector >= 75) - CurrentSector = 0; - else - CurrentTrackNumber--; - } - else - CurrentTrackNumber--; - - LoadTrack(CurrentTrackNumber); - } + public abstract void PreviousTrack(); /// /// Try to move to the next track index @@ -155,6 +139,12 @@ namespace RedBookPlayer.Discs /// public abstract void SetTotalIndexes(); + /// + /// Set the current sector + /// + /// New sector number to use + public void SetCurrentSector(ulong sector) => CurrentSector = sector; + /// /// Load the desired track, if possible /// diff --git a/RedBookPlayer/Discs/OpticalDiscFactory.cs b/RedBookPlayer.Models/Factories/OpticalDiscFactory.cs similarity index 51% rename from RedBookPlayer/Discs/OpticalDiscFactory.cs rename to RedBookPlayer.Models/Factories/OpticalDiscFactory.cs index 99d9257..d09beaf 100644 --- a/RedBookPlayer/Discs/OpticalDiscFactory.cs +++ b/RedBookPlayer.Models/Factories/OpticalDiscFactory.cs @@ -1,31 +1,72 @@ +using System.IO; using Aaru.CommonTypes.Interfaces; using Aaru.CommonTypes.Metadata; +using Aaru.DiscImages; +using Aaru.Filters; +using RedBookPlayer.Models.Discs; -namespace RedBookPlayer.Discs +namespace RedBookPlayer.Models.Factories { public static class OpticalDiscFactory { + /// + /// Generate an OpticalDisc from an input path + /// + /// Path to load the image from + /// 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 the image should be playable immediately, false otherwise + /// Instantiated OpticalDisc, if possible + public static OpticalDiscBase GenerateFromPath(string path, bool generateMissingToc, bool loadHiddenTracks, bool loadDataTracks, bool autoPlay) + { + try + { + // Validate the image exists + if(string.IsNullOrWhiteSpace(path) || !File.Exists(path)) + return null; + + // Load the disc image to memory + // TODO: Assumes Aaruformat right now for all + var image = new AaruFormat(); + var filter = new ZZZNoFilter(); + filter.Open(path); + image.Open(filter); + + // Generate and instantiate the disc + return GenerateFromImage(image, generateMissingToc, loadHiddenTracks, loadDataTracks, autoPlay); + } + catch + { + // All errors mean an invalid image in some way + return null; + } + } + /// /// Generate an OpticalDisc from an input IOpticalMediaImage /// /// IOpticalMediaImage to create from + /// 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 the image should be playable immediately, false otherwise /// Instantiated OpticalDisc, if possible - public static OpticalDisc GenerateFromImage(IOpticalMediaImage image, bool autoPlay) + public static OpticalDiscBase GenerateFromImage(IOpticalMediaImage image, bool generateMissingToc, bool loadHiddenTracks, bool loadDataTracks, bool autoPlay) { // If the image is not usable, we don't do anything if(!IsUsableImage(image)) return null; // Create the output object - OpticalDisc opticalDisc; + OpticalDiscBase opticalDisc; // Create the proper disc type switch(GetMediaType(image)) { case "Compact Disc": case "GD": - opticalDisc = new CompactDisc(); + opticalDisc = new CompactDisc(generateMissingToc, loadHiddenTracks, loadDataTracks); break; default: opticalDisc = null; diff --git a/RedBookPlayer/Hardware/DeEmphasisFilter.cs b/RedBookPlayer.Models/Hardware/DeEmphasisFilter.cs similarity index 97% rename from RedBookPlayer/Hardware/DeEmphasisFilter.cs rename to RedBookPlayer.Models/Hardware/DeEmphasisFilter.cs index 546cbb3..5b74b1d 100644 --- a/RedBookPlayer/Hardware/DeEmphasisFilter.cs +++ b/RedBookPlayer.Models/Hardware/DeEmphasisFilter.cs @@ -1,7 +1,7 @@ using System; using NWaves.Filters.BiQuad; -namespace RedBookPlayer.Hardware +namespace RedBookPlayer.Models.Hardware { /// /// Filter for applying de-emphasis to audio diff --git a/RedBookPlayer.Models/Hardware/Player.cs b/RedBookPlayer.Models/Hardware/Player.cs new file mode 100644 index 0000000..7db9f0c --- /dev/null +++ b/RedBookPlayer.Models/Hardware/Player.cs @@ -0,0 +1,518 @@ +using System; +using System.ComponentModel; +using Aaru.CommonTypes.Enums; +using ReactiveUI; +using RedBookPlayer.Models.Discs; +using RedBookPlayer.Models.Factories; + +namespace RedBookPlayer.Models.Hardware +{ + public class Player : ReactiveObject + { + /// + /// Indicate if the player is ready to be used + /// + public bool Initialized { get; private set; } = false; + + #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; + protected 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 => _opticalDisc.TotalTracks; + + /// + /// Represents the total indices on the disc + /// + public int TotalIndexes => _opticalDisc.TotalIndexes; + + /// + /// Total sectors in the image + /// + public ulong TotalSectors => _opticalDisc.TotalSectors; + + /// + /// Represents the time adjustment offset for the disc + /// + public ulong TimeOffset => _opticalDisc.TimeOffset; + + /// + /// Represents the total playing time for the disc + /// + public ulong TotalTime => _opticalDisc.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 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 + + #region Private State Variables + + /// + /// Sound output handling class + /// + private readonly SoundOutput _soundOutput; + + /// + /// OpticalDisc object + /// + private readonly OpticalDiscBase _opticalDisc; + + /// + /// Last volume for mute toggling + /// + private int? _lastVolume = null; + + #endregion + + /// + /// Create a new Player from 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 Player(string path, bool generateMissingToc, bool loadHiddenTracks, bool loadDataTracks, bool autoPlay, int defaultVolume) + { + // Set the internal state for initialization + Initialized = false; + _soundOutput = new SoundOutput(); + _soundOutput.SetDeEmphasis(false); + + // Initalize the disc + _opticalDisc = OpticalDiscFactory.GenerateFromPath(path, generateMissingToc, loadHiddenTracks, loadDataTracks, autoPlay); + if(_opticalDisc == null || !_opticalDisc.Initialized) + return; + + // Add event handling for the optical disc + _opticalDisc.PropertyChanged += OpticalDiscStateChanged; + + // Initialize the sound output + _soundOutput.Init(_opticalDisc, autoPlay, defaultVolume); + if(_soundOutput == null || !_soundOutput.Initialized) + return; + + // Add event handling for the sound output + _soundOutput.PropertyChanged += SoundOutputStateChanged; + + // Mark the player as ready + Initialized = true; + + // Force a refresh of the state information + OpticalDiscStateChanged(this, null); + SoundOutputStateChanged(this, null); + } + + #region Playback + + /// + /// Begin playback + /// + public void Play() + { + if(_opticalDisc == null || !_opticalDisc.Initialized) + return; + else if(_soundOutput == null) + return; + else if(_soundOutput.Playing) + return; + + _soundOutput.Play(); + _opticalDisc.SetTotalIndexes(); + Playing = true; + } + + /// + /// Pause current playback + /// + public void Pause() + { + if(_opticalDisc == null || !_opticalDisc.Initialized) + return; + else if(_soundOutput == null) + return; + else if(!_soundOutput.Playing) + return; + + _soundOutput?.Stop(); + Playing = false; + } + + /// + /// Toggle current playback + /// + public void TogglePlayback() + { + if(Playing == true) + Pause(); + else + Play(); + } + + /// + /// Stop current playback + /// + public void Stop() + { + if(_opticalDisc == null || !_opticalDisc.Initialized) + return; + else if(_soundOutput == null) + return; + else if(!_soundOutput.Playing) + return; + + _soundOutput?.Stop(); + _opticalDisc.LoadFirstTrack(); + Playing = null; + } + + /// + /// Move to the next playable track + /// + public void NextTrack() + { + if(_opticalDisc == null || !_opticalDisc.Initialized) + return; + + bool? wasPlaying = Playing; + if(wasPlaying == true) Pause(); + + _opticalDisc.NextTrack(); + if(_opticalDisc is CompactDisc compactDisc) + _soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis); + + if(wasPlaying == true) Play(); + } + + /// + /// Move to the previous playable track + /// + public void PreviousTrack() + { + if(_opticalDisc == null || !_opticalDisc.Initialized) + return; + + bool? wasPlaying = Playing; + if(wasPlaying == true) Pause(); + + _opticalDisc.PreviousTrack(); + if(_opticalDisc is CompactDisc compactDisc) + _soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis); + + if(wasPlaying == true) Play(); + } + + /// + /// Move to the next index + /// + /// True if index changes can trigger a track change, false otherwise + public void NextIndex(bool changeTrack) + { + if(_opticalDisc == null || !_opticalDisc.Initialized) + return; + + bool? wasPlaying = Playing; + if(wasPlaying == true) Pause(); + + _opticalDisc.NextIndex(changeTrack); + if(_opticalDisc is CompactDisc compactDisc) + _soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis); + + if(wasPlaying == true) Play(); + } + + /// + /// Move to the previous index + /// + /// True if index changes can trigger a track change, false otherwise + public void PreviousIndex(bool changeTrack) + { + if(_opticalDisc == null || !_opticalDisc.Initialized) + return; + + bool? wasPlaying = Playing; + if(wasPlaying == true) Pause(); + + _opticalDisc.PreviousIndex(changeTrack); + if(_opticalDisc is CompactDisc compactDisc) + _soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis); + + if(wasPlaying == true) Play(); + } + + /// + /// Fast-forward playback by 75 sectors, if possible + /// + public void FastForward() + { + if(_opticalDisc == null || !_opticalDisc.Initialized) + return; + + _opticalDisc.SetCurrentSector(Math.Min(_opticalDisc.TotalSectors, _opticalDisc.CurrentSector + 75)); + } + + /// + /// Rewind playback by 75 sectors, if possible + /// + public void Rewind() + { + if(_opticalDisc == null || !_opticalDisc.Initialized) + return; + + if(_opticalDisc.CurrentSector >= 75) + _opticalDisc.SetCurrentSector(_opticalDisc.CurrentSector - 75); + } + + #endregion + + #region Volume + + /// + /// Increment the volume value + /// + public void VolumeUp() => SetVolume(Volume + 1); + + /// + /// Decrement the volume value + /// + public void VolumeDown() => SetVolume(Volume + 1); + + /// + /// Set the value for the volume + /// + /// New volume value + public void SetVolume(int volume) => _soundOutput?.SetVolume(volume); + + /// + /// Temporarily mute playback + /// + public void ToggleMute() + { + if(_lastVolume == null) + { + _lastVolume = Volume; + SetVolume(0); + } + else + { + SetVolume(_lastVolume.Value); + _lastVolume = null; + } + } + + #endregion + + #region Emphasis + + /// + /// Enable de-emphasis + /// + public void EnableDeEmphasis() => SetDeEmphasis(true); + + /// + /// Disable de-emphasis + /// + public void DisableDeEmphasis() => SetDeEmphasis(false); + + /// + /// Toggle de-emphasis + /// + public void ToggleDeEmphasis() => SetDeEmphasis(!ApplyDeEmphasis); + + /// + /// Set de-emphasis status + /// + /// + private void SetDeEmphasis(bool apply) => _soundOutput?.SetDeEmphasis(apply); + + #endregion + + #region Helpers + + /// + /// Set the value for loading data tracks [CompactDisc only] + /// + /// True to enable loading data tracks, false otherwise + public void SetLoadDataTracks(bool load) + { + if(_opticalDisc is CompactDisc compactDisc) + compactDisc.LoadDataTracks = load; + } + + /// + /// Set the value for loading hidden tracks [CompactDisc only] + /// + /// True to enable loading hidden tracks, false otherwise + public void SetLoadHiddenTracks(bool load) + { + if(_opticalDisc is CompactDisc compactDisc) + compactDisc.LoadHiddenTracks = load; + } + + /// + /// Update the player from the current OpticalDisc + /// + private void OpticalDiscStateChanged(object sender, PropertyChangedEventArgs e) + { + CurrentTrackNumber = _opticalDisc.CurrentTrackNumber; + CurrentTrackIndex = _opticalDisc.CurrentTrackIndex; + CurrentSector = _opticalDisc.CurrentSector; + SectionStartSector = _opticalDisc.SectionStartSector; + + HiddenTrack = TimeOffset > 150; + + if(_opticalDisc is CompactDisc compactDisc) + { + QuadChannel = compactDisc.QuadChannel; + IsDataTrack = compactDisc.IsDataTrack; + CopyAllowed = compactDisc.CopyAllowed; + TrackHasEmphasis = compactDisc.TrackHasEmphasis; + } + else + { + QuadChannel = false; + IsDataTrack = _opticalDisc.TrackType != TrackType.Audio; + CopyAllowed = false; + TrackHasEmphasis = false; + } + } + + /// + /// Update the player from the current SoundOutput + /// + private void SoundOutputStateChanged(object sender, PropertyChangedEventArgs e) + { + Playing = _soundOutput.Playing; + ApplyDeEmphasis = _soundOutput.ApplyDeEmphasis; + Volume = _soundOutput.Volume; + } + + #endregion + } +} \ No newline at end of file diff --git a/RedBookPlayer/Hardware/PlayerSource.cs b/RedBookPlayer.Models/Hardware/PlayerSource.cs similarity index 96% rename from RedBookPlayer/Hardware/PlayerSource.cs rename to RedBookPlayer.Models/Hardware/PlayerSource.cs index 17b4a5c..203ff4f 100644 --- a/RedBookPlayer/Hardware/PlayerSource.cs +++ b/RedBookPlayer.Models/Hardware/PlayerSource.cs @@ -2,7 +2,7 @@ using System; using CSCore; using WaveFormat = CSCore.WaveFormat; -namespace RedBookPlayer.Hardware +namespace RedBookPlayer.Models.Hardware { public class PlayerSource : IWaveSource { diff --git a/RedBookPlayer/Hardware/SoundOutput.cs b/RedBookPlayer.Models/Hardware/SoundOutput.cs similarity index 68% rename from RedBookPlayer/Hardware/SoundOutput.cs rename to RedBookPlayer.Models/Hardware/SoundOutput.cs index d31b68c..743c35b 100644 --- a/RedBookPlayer/Hardware/SoundOutput.cs +++ b/RedBookPlayer.Models/Hardware/SoundOutput.cs @@ -4,11 +4,12 @@ using System.Threading.Tasks; using CSCore.SoundOut; using NWaves.Audio; using NWaves.Filters.BiQuad; -using RedBookPlayer.Discs; +using ReactiveUI; +using RedBookPlayer.Models.Discs; -namespace RedBookPlayer.Hardware +namespace RedBookPlayer.Models.Hardware { - public class SoundOutput + public class SoundOutput : ReactiveObject { #region Public Fields @@ -17,15 +18,23 @@ namespace RedBookPlayer.Hardware /// public bool Initialized { get; private set; } = false; - /// - /// Indicates if de-emphasis should be applied - /// - public bool ApplyDeEmphasis { get; set; } = false; - /// /// Indicate if the output is playing /// - public bool Playing => _soundOut.PlaybackState == PlaybackState.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 @@ -33,38 +42,33 @@ namespace RedBookPlayer.Hardware public int Volume { get => _volume; - set + private set { + int tempVolume = value; if(value > 100) - _volume = 100; + tempVolume = 100; else if(value < 0) - _volume = 0; - else - _volume = value; + tempVolume = 0; + + this.RaiseAndSetIfChanged(ref _volume, tempVolume); } } + private bool _playing; + private bool _applyDeEmphasis; + private int _volume; + #endregion #region Private State Variables - /// - /// Current position in the sector - /// - private int _currentSectorReadPosition = 0; - /// /// OpticalDisc from the parent player for easy access /// /// /// TODO: Can we remove the need for a local reference to OpticalDisc? /// - private OpticalDisc _opticalDisc; - - /// - /// Internal value for the volume - /// - private int _volume; + private OpticalDiscBase _opticalDisc; /// /// Data provider for sound output @@ -86,6 +90,11 @@ namespace RedBookPlayer.Hardware /// private BiQuadFilter _deEmphasisFilterRight; + /// + /// Current position in the sector + /// + private int _currentSectorReadPosition = 0; + /// /// Lock object for reading track data /// @@ -99,7 +108,7 @@ namespace RedBookPlayer.Hardware /// OpticalDisc to load from /// True if playback should begin immediately, false otherwise /// Default volume between 0 and 100 to use when starting playback - public void Init(OpticalDisc opticalDisc, bool autoPlay = false, int defaultVolume = 100) + public void Init(OpticalDiscBase opticalDisc, bool autoPlay = false, int defaultVolume = 100) { // If we have an unusable disc, just return if(opticalDisc == null || !opticalDisc.Initialized) @@ -144,32 +153,15 @@ namespace RedBookPlayer.Hardware // Set the current volume _soundOut.Volume = (float)Volume / 100; - // Determine how many sectors we can read - ulong sectorsToRead; - ulong zeroSectorsAmount; - do + // If we have an unreadable track, just return + if(_opticalDisc.BytesPerSector <= 0) { - // Attempt to read 2 more sectors than requested - sectorsToRead = ((ulong)count / (ulong)_opticalDisc.BytesPerSector) + 2; - zeroSectorsAmount = 0; + Array.Clear(buffer, offset, count); + return count; + } - // Avoid overreads by padding with 0-byte data at the end - if(_opticalDisc.CurrentSector + sectorsToRead > _opticalDisc.TotalSectors) - { - ulong oldSectorsToRead = sectorsToRead; - sectorsToRead = _opticalDisc.TotalSectors - _opticalDisc.CurrentSector; - - int tempZeroSectorCount = (int)(oldSectorsToRead - sectorsToRead); - zeroSectorsAmount = (ulong)(tempZeroSectorCount < 0 ? 0 : tempZeroSectorCount); - } - - // TODO: Figure out when this value could be negative - if(sectorsToRead <= 0) - { - _opticalDisc.LoadFirstTrack(); - _currentSectorReadPosition = 0; - } - } while(sectorsToRead <= 0); + // Determine how many sectors we can read + DetermineReadAmount(count, out ulong sectorsToRead, out ulong zeroSectorsAmount); // Create padding data for overreads byte[] zeroSectors = new byte[(int)zeroSectorsAmount * _opticalDisc.BytesPerSector]; @@ -220,20 +212,7 @@ namespace RedBookPlayer.Hardware // Apply de-emphasis filtering, only if enabled if(ApplyDeEmphasis) - { - float[][] floatAudioData = new float[2][]; - floatAudioData[0] = new float[audioDataSegment.Length / 4]; - floatAudioData[1] = new float[audioDataSegment.Length / 4]; - ByteConverter.ToFloats16Bit(audioDataSegment, floatAudioData); - - for(int i = 0; i < floatAudioData[0].Length; i++) - { - floatAudioData[0][i] = _deEmphasisFilterLeft.Process(floatAudioData[0][i]); - floatAudioData[1][i] = _deEmphasisFilterRight.Process(floatAudioData[1][i]); - } - - ByteConverter.FromFloats16Bit(floatAudioData, audioDataSegment); - } + ProcessDeEmphasis(audioDataSegment); // Write out the audio data to the buffer Array.Copy(audioDataSegment, 0, buffer, offset, count); @@ -242,7 +221,7 @@ namespace RedBookPlayer.Hardware _currentSectorReadPosition += count; if(_currentSectorReadPosition >= _opticalDisc.BytesPerSector) { - _opticalDisc.CurrentSector += (ulong)(_currentSectorReadPosition / _opticalDisc.BytesPerSector); + _opticalDisc.SetCurrentSector(_opticalDisc.CurrentSector + (ulong)(_currentSectorReadPosition / _opticalDisc.BytesPerSector)); _currentSectorReadPosition %= _opticalDisc.BytesPerSector; } @@ -254,22 +233,105 @@ namespace RedBookPlayer.Hardware /// /// Start audio playback /// - public void Play() => _soundOut.Play(); + public void Play() + { + if (_soundOut.PlaybackState != PlaybackState.Playing) + _soundOut.Play(); + + Playing = _soundOut.PlaybackState == PlaybackState.Playing; + } + + /// + /// Pause audio playback + /// + public void Pause() + { + if(_soundOut.PlaybackState != PlaybackState.Paused) + _soundOut.Pause(); + + Playing = _soundOut.PlaybackState == PlaybackState.Playing; + } /// /// Stop audio playback /// - public void Stop() => _soundOut.Stop(); + public void Stop() + { + if(_soundOut.PlaybackState != PlaybackState.Stopped) + _soundOut.Stop(); + + Playing = _soundOut.PlaybackState == PlaybackState.Playing; + } #endregion #region Helpers /// - /// Toggle de-emphasis processing + /// Set de-emphasis status /// - /// True to apply de-emphasis, false otherwise - public void SetDeEmphasis(bool enable) => ApplyDeEmphasis = enable; + /// + public void SetDeEmphasis(bool apply) => ApplyDeEmphasis = apply; + + /// + /// Set the value for the volume + /// + /// New volume value + public void SetVolume(int volume) => Volume = volume; + + /// + /// Determine the number of real and zero sectors to read + /// + /// Number of requested bytes to read + /// Number of sectors to read + /// Number of zeroed sectors to concatenate + private void DetermineReadAmount(int count, out ulong sectorsToRead, out ulong zeroSectorsAmount) + { + do + { + // Attempt to read 2 more sectors than requested + sectorsToRead = ((ulong)count / (ulong)_opticalDisc.BytesPerSector) + 2; + zeroSectorsAmount = 0; + + // Avoid overreads by padding with 0-byte data at the end + if(_opticalDisc.CurrentSector + sectorsToRead > _opticalDisc.TotalSectors) + { + ulong oldSectorsToRead = sectorsToRead; + sectorsToRead = _opticalDisc.TotalSectors - _opticalDisc.CurrentSector; + + int tempZeroSectorCount = (int)(oldSectorsToRead - sectorsToRead); + zeroSectorsAmount = (ulong)(tempZeroSectorCount < 0 ? 0 : tempZeroSectorCount); + } + + // If we're reading past the last sector of the disc, wrap around + // TODO: Have past-end reads looping back controlled by a flag instead (Repeat? Repeat All?) + if(sectorsToRead <= 0) + { + _opticalDisc.LoadFirstTrack(); + _currentSectorReadPosition = 0; + } + } while(sectorsToRead <= 0); + } + + /// + /// Process de-emphasis of audio data + /// + /// Audio data to process + private void ProcessDeEmphasis(byte[] audioData) + { + float[][] floatAudioData = new float[2][]; + floatAudioData[0] = new float[audioData.Length / 4]; + floatAudioData[1] = new float[audioData.Length / 4]; + ByteConverter.ToFloats16Bit(audioData, floatAudioData); + + for(int i = 0; i < floatAudioData[0].Length; i++) + { + floatAudioData[0][i] = _deEmphasisFilterLeft.Process(floatAudioData[0][i]); + floatAudioData[1][i] = _deEmphasisFilterRight.Process(floatAudioData[1][i]); + } + + ByteConverter.FromFloats16Bit(floatAudioData, audioData); + } /// /// Sets or resets the de-emphasis filters diff --git a/RedBookPlayer.Models/RedBookPlayer.Models.csproj b/RedBookPlayer.Models/RedBookPlayer.Models.csproj new file mode 100644 index 0000000..bfbe9f2 --- /dev/null +++ b/RedBookPlayer.Models/RedBookPlayer.Models.csproj @@ -0,0 +1,19 @@ + + + + netcoreapp3.1 + true + + + + + + + + + + + + + + diff --git a/RedBookPlayer.Models/nuget.config b/RedBookPlayer.Models/nuget.config new file mode 100644 index 0000000..7c07e22 --- /dev/null +++ b/RedBookPlayer.Models/nuget.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/RedBookPlayer.sln b/RedBookPlayer.sln index a622fe1..3b35228 100644 --- a/RedBookPlayer.sln +++ b/RedBookPlayer.sln @@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.31321.278 MinimumVisualStudioVersion = 15.0.26124.0 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RedBookPlayer", "RedBookPlayer\RedBookPlayer.csproj", "{94944959-0352-4ABF-9C5C-19FF33747ECE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RedBookPlayer.GUI", "RedBookPlayer.GUI\RedBookPlayer.GUI.csproj", "{94944959-0352-4ABF-9C5C-19FF33747ECE}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "cscore", "cscore", "{9A371299-4C59-4E46-9C3B-4FE024017491}" EndProject @@ -40,6 +40,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.md = README.md EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RedBookPlayer.Models", "RedBookPlayer.Models\RedBookPlayer.Models.csproj", "{462A3B8E-A5D4-4539-8469-1647B47AB2A8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -194,6 +196,18 @@ Global {ED8E11B7-786F-4EFF-9E4C-B937B7A2DE89}.Release|x64.Build.0 = Release|Any CPU {ED8E11B7-786F-4EFF-9E4C-B937B7A2DE89}.Release|x86.ActiveCfg = Release|Any CPU {ED8E11B7-786F-4EFF-9E4C-B937B7A2DE89}.Release|x86.Build.0 = Release|Any CPU + {462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Debug|x64.ActiveCfg = Debug|Any CPU + {462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Debug|x64.Build.0 = Debug|Any CPU + {462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Debug|x86.ActiveCfg = Debug|Any CPU + {462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Debug|x86.Build.0 = Debug|Any CPU + {462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Release|Any CPU.Build.0 = Release|Any CPU + {462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Release|x64.ActiveCfg = Release|Any CPU + {462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Release|x64.Build.0 = Release|Any CPU + {462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Release|x86.ActiveCfg = Release|Any CPU + {462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/RedBookPlayer/GUI/PlayerView.xaml.cs b/RedBookPlayer/GUI/PlayerView.xaml.cs deleted file mode 100644 index 7dabeae..0000000 --- a/RedBookPlayer/GUI/PlayerView.xaml.cs +++ /dev/null @@ -1,248 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using System.Timers; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Interactivity; -using Avalonia.Markup.Xaml; -using Avalonia.Media.Imaging; -using Avalonia.Platform; -using Avalonia.Threading; - -namespace RedBookPlayer.GUI -{ - public class PlayerView : UserControl - { - /// - /// Read-only access to the view model - /// - public PlayerViewModel PlayerViewModel => DataContext as PlayerViewModel; - - /// - /// Set of images representing the digits for the UI - /// - /// - /// TODO: Does it make sense to have this as an array? - /// - private Image[] _digits; - - /// - /// Timer for performing UI updates - /// - private Timer _updateTimer; - - public PlayerView() => InitializeComponent(null); - - public PlayerView(string xaml) => InitializeComponent(xaml); - - #region Helpers - - /// - /// Generate a path selection dialog box - /// - /// User-selected path, if possible - public async Task GetPath() - { - 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((Window)Parent.Parent))?.FirstOrDefault(); - } - - /// - /// Load an image from the path - /// - /// Path to the image to load - public async Task LoadImage(string path) - { - bool result = await Dispatcher.UIThread.InvokeAsync(() => - { - PlayerViewModel.Init(path, App.Settings.AutoPlay, App.Settings.Volume); - return PlayerViewModel.Initialized; - }); - - if(result) - { - await Dispatcher.UIThread.InvokeAsync(() => - { - MainWindow.Instance.Title = "RedBookPlayer - " + path.Split('/').Last().Split('\\').Last(); - }); - } - - return result; - } - - /// - /// Load the png image for a given character based on the theme - /// - /// Character to load the image for - /// Bitmap representing the loaded image - /// - /// TODO: Currently assumes that an image must always exist - /// - private Bitmap GetBitmap(char character) - { - 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); - } - } - - /// - /// Initialize the UI based on the currently selected theme - /// - /// XAML data representing the theme, null for default - private void InitializeComponent(string xaml) - { - DataContext = new PlayerViewModel(); - - // Load the theme - try - { - if(xaml != null) - new AvaloniaXamlLoader().Load(xaml, null, this); - else - AvaloniaXamlLoader.Load(this); - } - catch - { - AvaloniaXamlLoader.Load(this); - } - - InitializeDigits(); - - _updateTimer = new Timer(1000 / 60); - - _updateTimer.Elapsed += (sender, e) => - { - try - { - UpdateView(sender, e); - } - catch(Exception ex) - { - Console.WriteLine(ex); - } - }; - - _updateTimer.AutoReset = true; - _updateTimer.Start(); - } - - /// - /// Initialize the displayed digits array - /// - private void InitializeDigits() - { - _digits = new Image[] - { - this.FindControl("TrackDigit1"), - this.FindControl("TrackDigit2"), - - this.FindControl("IndexDigit1"), - this.FindControl("IndexDigit2"), - - this.FindControl("TimeDigit1"), - this.FindControl("TimeDigit2"), - this.FindControl("TimeDigit3"), - this.FindControl("TimeDigit4"), - this.FindControl("TimeDigit5"), - this.FindControl("TimeDigit6"), - - this.FindControl("TotalTracksDigit1"), - this.FindControl("TotalTracksDigit2"), - - this.FindControl("TotalIndexesDigit1"), - this.FindControl("TotalIndexesDigit2"), - - this.FindControl("TotalTimeDigit1"), - this.FindControl("TotalTimeDigit2"), - this.FindControl("TotalTimeDigit3"), - this.FindControl("TotalTimeDigit4"), - this.FindControl("TotalTimeDigit5"), - this.FindControl("TotalTimeDigit6"), - }; - } - - /// - /// Update the UI with the most recent information from the Player - /// - private void UpdateView(object sender, ElapsedEventArgs e) - { - Dispatcher.UIThread.InvokeAsync(() => - { - string digitString = PlayerViewModel.GenerateDigitString(); - for (int i = 0; i < _digits.Length; i++) - { - if (_digits[i] != null) - _digits[i].Source = GetBitmap(digitString[i]); - } - - PlayerViewModel?.UpdateView(); - }); - } - - #endregion - - #region Event Handlers - - public async void LoadButton_Click(object sender, RoutedEventArgs e) - { - string path = await GetPath(); - if (path == null) - return; - - await LoadImage(path); - } - - public void PlayButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.Playing = true; - - public void PauseButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.Playing = false; - - public void PlayPauseButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.Playing = !(PlayerViewModel.Playing ?? false); - - public void StopButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.Playing = null; - - public void NextTrackButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.NextTrack(); - - public void PreviousTrackButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.PreviousTrack(); - - public void NextIndexButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.NextIndex(App.Settings.IndexButtonChangeTrack); - - public void PreviousIndexButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.PreviousIndex(App.Settings.IndexButtonChangeTrack); - - public void FastForwardButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.FastForward(); - - public void RewindButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.Rewind(); - - public void VolumeUpButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.Volume++; - - public void VolumeDownButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.Volume--; - - public void MuteToggleButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.ToggleMute(); - - public void EnableDeEmphasisButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.ApplyDeEmphasis = true; - - public void DisableDeEmphasisButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.ApplyDeEmphasis = false; - - public void EnableDisableDeEmphasisButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.ApplyDeEmphasis = !PlayerViewModel.ApplyDeEmphasis; - - #endregion - } -} \ No newline at end of file diff --git a/RedBookPlayer/GUI/PlayerViewModel.cs b/RedBookPlayer/GUI/PlayerViewModel.cs deleted file mode 100644 index 4030ba5..0000000 --- a/RedBookPlayer/GUI/PlayerViewModel.cs +++ /dev/null @@ -1,270 +0,0 @@ -using System.Linq; -using Aaru.CommonTypes.Enums; -using ReactiveUI; -using RedBookPlayer.Discs; -using RedBookPlayer.Hardware; - -namespace RedBookPlayer.GUI -{ - public class PlayerViewModel : ReactiveObject - { - /// - /// Player representing the internal state - /// - private Player _player; - - /// - /// Last volume for mute toggling - /// - private int? _lastVolume = null; - - #region Player Status - - /// - /// Indicate if the model is ready to be used - /// - public bool Initialized => _player?.Initialized ?? false; - - /// - /// Indicate the player state - /// - public bool? Playing - { - get => _player?.Playing ?? false; - set - { - if(_player != null) - _player.Playing = value; - } - } - - /// - /// Indicate the current playback volume - /// - public int Volume - { - get => _player?.Volume ?? 100; - set - { - if(_player != null) - _player.Volume = value; - } - } - - /// - /// Indicates if de-emphasis should be applied - /// - public bool ApplyDeEmphasis - { - get => _player?.ApplyDeEmphasis ?? false; - set - { - if(_player != null) - _player.ApplyDeEmphasis = value; - } - } - - #endregion - - #region Model-Provided Playback Information - - private ulong _currentSector; - public ulong CurrentSector - { - get => _currentSector; - set => this.RaiseAndSetIfChanged(ref _currentSector, value); - } - - public int CurrentFrame => (int)(_currentSector / (75 * 60)); - public int CurrentSecond => (int)(_currentSector / 75 % 60); - public int CurrentMinute => (int)(_currentSector % 75); - - private ulong _totalSectors; - public ulong TotalSectors - { - get => _totalSectors; - set => this.RaiseAndSetIfChanged(ref _totalSectors, value); - } - - public int TotalFrames => (int)(_totalSectors / (75 * 60)); - public int TotalSeconds => (int)(_totalSectors / 75 % 60); - public int TotalMinutes => (int)(_totalSectors % 75); - - #endregion - - #region Disc Flags - - private bool _quadChannel; - public bool QuadChannel - { - get => _quadChannel; - set => this.RaiseAndSetIfChanged(ref _quadChannel, value); - } - - private bool _isDataTrack; - public bool IsDataTrack - { - get => _isDataTrack; - set => this.RaiseAndSetIfChanged(ref _isDataTrack, value); - } - - private bool _copyAllowed; - public bool CopyAllowed - { - get => _copyAllowed; - set => this.RaiseAndSetIfChanged(ref _copyAllowed, value); - } - - private bool _trackHasEmphasis; - public bool TrackHasEmphasis - { - get => _trackHasEmphasis; - set => this.RaiseAndSetIfChanged(ref _trackHasEmphasis, value); - } - - private bool _hiddenTrack; - public bool HiddenTrack - { - get => _hiddenTrack; - set => this.RaiseAndSetIfChanged(ref _hiddenTrack, value); - } - - #endregion - - /// - /// Initialize the view model with a given image path - /// - /// Path to the disc image - /// 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 autoPlay, int defaultVolume) - { - // Stop current playback, if necessary - if(Playing != null) Playing = null; - - // Create and attempt to initialize new Player - _player = new Player(path, autoPlay, defaultVolume); - if(Initialized) - UpdateView(); - } - - #region Playback - - /// - /// Move to the next playable track - /// - public void NextTrack() => _player?.NextTrack(); - - /// - /// Move to the previous playable track - /// - public void PreviousTrack() => _player?.PreviousTrack(); - - /// - /// Move to the next index - /// - /// True if index changes can trigger a track change, false otherwise - public void NextIndex(bool changeTrack) => _player?.NextIndex(changeTrack); - - /// - /// Move to the previous index - /// - /// True if index changes can trigger a track change, false otherwise - public void PreviousIndex(bool changeTrack) => _player?.PreviousIndex(changeTrack); - - /// - /// Fast-forward playback by 75 sectors, if possible - /// - public void FastForward() => _player?.FastForward(); - - /// - /// Rewind playback by 75 sectors, if possible - /// - public void Rewind() => _player?.Rewind(); - - #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(_player?.OpticalDisc == null || !_player.OpticalDisc.Initialized) - return string.Empty.PadLeft(20, '-'); - - // Otherwise, take the current time into account - ulong sectorTime = _player.GetCurrentSectorTime(); - - int[] numbers = new int[] - { - _player.OpticalDisc.CurrentTrackNumber + 1, - _player.OpticalDisc.CurrentTrackIndex, - - (int)(sectorTime / (75 * 60)), - (int)(sectorTime / 75 % 60), - (int)(sectorTime % 75), - - _player.OpticalDisc.TotalTracks, - _player.OpticalDisc.TotalIndexes, - - (int)(_player.OpticalDisc.TotalTime / (75 * 60)), - (int)(_player.OpticalDisc.TotalTime / 75 % 60), - (int)(_player.OpticalDisc.TotalTime % 75), - }; - - return string.Join("", numbers.Select(i => i.ToString().PadLeft(2, '0').Substring(0, 2))); - } - - /// - /// Temporarily mute playback - /// - public void ToggleMute() - { - if(_lastVolume == null) - { - _lastVolume = Volume; - Volume = 0; - } - else - { - Volume = _lastVolume.Value; - _lastVolume = null; - } - } - - /// - /// Update the UI from the internal player - /// - public void UpdateView() - { - if(_player?.Initialized != true) - return; - - CurrentSector = _player.GetCurrentSectorTime(); - TotalSectors = _player.OpticalDisc.TotalTime; - - HiddenTrack = _player.OpticalDisc.TimeOffset > 150; - - if(_player.OpticalDisc is CompactDisc compactDisc) - { - QuadChannel = compactDisc.QuadChannel; - IsDataTrack = compactDisc.IsDataTrack; - CopyAllowed = compactDisc.CopyAllowed; - TrackHasEmphasis = compactDisc.TrackHasEmphasis; - } - else - { - QuadChannel = false; - IsDataTrack = _player.OpticalDisc.TrackType != TrackType.Audio; - CopyAllowed = false; - TrackHasEmphasis = false; - } - } - - #endregion - } -} \ No newline at end of file diff --git a/RedBookPlayer/Hardware/Player.cs b/RedBookPlayer/Hardware/Player.cs deleted file mode 100644 index 2850f89..0000000 --- a/RedBookPlayer/Hardware/Player.cs +++ /dev/null @@ -1,258 +0,0 @@ -using System; -using System.IO; -using Aaru.DiscImages; -using Aaru.Filters; -using RedBookPlayer.Discs; - -namespace RedBookPlayer.Hardware -{ - public class Player - { - #region Public Fields - - /// - /// Indicate if the player is ready to be used - /// - public bool Initialized { get; private set; } = false; - - /// - /// OpticalDisc object - /// - public OpticalDisc OpticalDisc { get; private set; } - - /// - /// Indicate if the disc is playing - /// - public bool? Playing - { - get => _soundOutput?.Playing; - set - { - if(OpticalDisc == null || !OpticalDisc.Initialized) - return; - - // If the playing state has not changed, do nothing - if(value == _soundOutput?.Playing) - return; - - if(value == true) - { - _soundOutput.Play(); - OpticalDisc.SetTotalIndexes(); - } - else if(value == false) - { - _soundOutput.Stop(); - } - else - { - _soundOutput.Stop(); - OpticalDisc.LoadFirstTrack(); - } - } - } - - /// - /// Indicate the current playback volume - /// - public int Volume - { - get => _soundOutput?.Volume ?? 100; - set - { - if(_soundOutput != null) - _soundOutput.Volume = value; - } - } - - /// - /// Indicates if de-emphasis should be applied - /// - public bool ApplyDeEmphasis - { - get => _soundOutput?.ApplyDeEmphasis ?? false; - set => _soundOutput?.SetDeEmphasis(value); - } - - #endregion - - #region Private State Variables - - /// - /// Sound output handling class - /// - public SoundOutput _soundOutput; - - #endregion - - /// - /// Create a new Player from a given image path - /// - /// Path to the disc image - /// True if playback should begin immediately, false otherwise - /// Default volume between 0 and 100 to use when starting playback - public Player(string path, bool autoPlay = false, int defaultVolume = 100) - { - // Set the internal state for initialization - Initialized = false; - _soundOutput = new SoundOutput(); - _soundOutput.ApplyDeEmphasis = false; - OpticalDisc = null; - - try - { - // Validate the image exists - if(string.IsNullOrWhiteSpace(path) || !File.Exists(path)) - return; - - // Load the disc image to memory - var image = new AaruFormat(); - var filter = new ZZZNoFilter(); - filter.Open(path); - image.Open(filter); - - // Generate and instantiate the disc - OpticalDisc = OpticalDiscFactory.GenerateFromImage(image, autoPlay); - } - catch - { - // All errors mean an invalid image in some way - return; - } - - // Initialize the sound output - _soundOutput.Init(OpticalDisc, autoPlay, defaultVolume); - if(_soundOutput == null || !_soundOutput.Initialized) - return; - - // Mark the player as ready - Initialized = true; - } - - #region Playback - - /// - /// Move to the next playable track - /// - public void NextTrack() - { - if(OpticalDisc == null || !OpticalDisc.Initialized) - return; - - bool? wasPlaying = Playing; - if(wasPlaying == true) Playing = false; - - OpticalDisc.NextTrack(); - if(OpticalDisc is CompactDisc compactDisc) - _soundOutput.ApplyDeEmphasis = compactDisc.TrackHasEmphasis; - - if(wasPlaying == true) Playing = true; - } - - /// - /// Move to the previous playable track - /// - public void PreviousTrack() - { - if(OpticalDisc == null || !OpticalDisc.Initialized) - return; - - bool? wasPlaying = Playing; - if(wasPlaying == true) Playing = false; - - OpticalDisc.PreviousTrack(); - if(OpticalDisc is CompactDisc compactDisc) - _soundOutput.ApplyDeEmphasis = compactDisc.TrackHasEmphasis; - - if(wasPlaying == true) Playing = true; - } - - /// - /// Move to the next index - /// - /// True if index changes can trigger a track change, false otherwise - public void NextIndex(bool changeTrack) - { - if(OpticalDisc == null || !OpticalDisc.Initialized) - return; - - bool? wasPlaying = Playing; - if(wasPlaying == true) Playing = false; - - OpticalDisc.NextIndex(changeTrack); - if(OpticalDisc is CompactDisc compactDisc) - _soundOutput.ApplyDeEmphasis = compactDisc.TrackHasEmphasis; - - if(wasPlaying == true) Playing = true; - } - - /// - /// Move to the previous index - /// - /// True if index changes can trigger a track change, false otherwise - public void PreviousIndex(bool changeTrack) - { - if(OpticalDisc == null || !OpticalDisc.Initialized) - return; - - bool? wasPlaying = Playing; - if(wasPlaying == true) Playing = false; - - OpticalDisc.PreviousIndex(changeTrack); - if(OpticalDisc is CompactDisc compactDisc) - _soundOutput.ApplyDeEmphasis = compactDisc.TrackHasEmphasis; - - if(wasPlaying == true) Playing = true; - } - - /// - /// Fast-forward playback by 75 sectors, if possible - /// - public void FastForward() - { - if(OpticalDisc == null || !OpticalDisc.Initialized) - return; - - OpticalDisc.CurrentSector = Math.Min(OpticalDisc.TotalSectors, OpticalDisc.CurrentSector + 75); - } - - /// - /// Rewind playback by 75 sectors, if possible - /// - public void Rewind() - { - if(OpticalDisc == null || !OpticalDisc.Initialized) - return; - - if(OpticalDisc.CurrentSector >= 75) - OpticalDisc.CurrentSector -= 75; - } - - #endregion - - #region Helpers - - /// - /// Get current sector time, accounting for offsets - /// - /// ulong representing the current sector time - public ulong GetCurrentSectorTime() - { - ulong sectorTime = OpticalDisc.CurrentSector; - if (OpticalDisc.SectionStartSector != 0) - sectorTime -= OpticalDisc.SectionStartSector; - else - sectorTime += OpticalDisc.TimeOffset; - - return sectorTime; - } - - /// - /// Set if de-emphasis should be applied - /// - /// True to enable, false to disable - public void SetDeEmphasis(bool apply) => _soundOutput?.SetDeEmphasis(apply); - - #endregion - } -} \ No newline at end of file