diff --git a/README.md b/README.md index 6ccbb4b..7b0ec36 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,27 @@ [Audio CD](https://en.wikipedia.org/wiki/Compact_Disc_Digital_Audio) player for [Aaru format](https://github.com/aaru-dps/Aaru). * This project is fully sponsored by the [Game Preservation Society](https://www.gamepres.org/en/). + +## Default Player Controls + +| Key | Action | +| --- | ------ | +| **F1** | Open Settings Window | +| **F2** | Load New Image | +| **Space** | Toggle Play / Pause | +| **Esc** | Stop Playback | +| **→** | Next Track | +| **←** | Previous Track | +| **]** | Next Index | +| **[** | Previous Index | +| **.** | Fast Forward | +| **,** | Rewind | +| **Numpad +** | Volume Up | +| **Numpad -** | Volume Down | +| **M** | Mute | +| **E** | Toggle Emphasis | + +For both Volume Up and Volume Down: +- Holding **Ctrl** will move in increments of 2 +- Holding **Shift** will move in increments of 5 +- Holding both will move in increments of 10 \ No newline at end of file diff --git a/RedBookPlayer/Discs/CompactDisc.cs b/RedBookPlayer/Discs/CompactDisc.cs index 87b670a..21e8fc8 100644 --- a/RedBookPlayer/Discs/CompactDisc.cs +++ b/RedBookPlayer/Discs/CompactDisc.cs @@ -79,8 +79,8 @@ namespace RedBookPlayer.Discs // Ensure that the value is valid, wrapping around if necessary if(value > track.Indexes.Keys.Max()) - _currentTrackIndex = 0; - else if(value < 0) + _currentTrackIndex = track.Indexes.Keys.Min(); + else if(value < track.Indexes.Keys.Min()) _currentTrackIndex = track.Indexes.Keys.Max(); else _currentTrackIndex = value; diff --git a/RedBookPlayer/GUI/MainWindow.xaml.cs b/RedBookPlayer/GUI/MainWindow.xaml.cs index 474037b..9c46297 100644 --- a/RedBookPlayer/GUI/MainWindow.xaml.cs +++ b/RedBookPlayer/GUI/MainWindow.xaml.cs @@ -90,7 +90,7 @@ namespace RedBookPlayer.GUI Closing += (e, f) => { - PlayerView.Player.Stop(); + ((PlayerView)ContentControl.Content).StopButton_Click(this, null); }; AddHandler(DragDrop.DropEvent, MainWindow_Drop); @@ -115,11 +115,106 @@ namespace RedBookPlayer.GUI public void OnKeyDown(object sender, KeyEventArgs e) { - if(e.Key == Key.F1) + PlayerView playerView = ContentControl.Content as PlayerView; + + // Open settings window + if(e.Key == App.Settings.OpenSettingsKey) { settingsWindow = new SettingsWindow(App.Settings); settingsWindow.Show(); } + + // Load image + else if (e.Key == App.Settings.LoadImageKey) + { + playerView?.LoadButton_Click(this, null); + } + + // Toggle playback + else if(e.Key == App.Settings.TogglePlaybackKey || e.Key == Key.MediaPlayPause) + { + playerView?.PlayPauseButton_Click(this, null); + } + + // Stop playback + else if(e.Key == App.Settings.StopPlaybackKey || e.Key == Key.MediaStop) + { + playerView?.StopButton_Click(this, null); + } + + // Next Track + else if(e.Key == App.Settings.NextTrackKey || e.Key == Key.MediaNextTrack) + { + playerView?.NextTrackButton_Click(this, null); + } + + // Previous Track + else if(e.Key == App.Settings.PreviousTrackKey || e.Key == Key.MediaPreviousTrack) + { + playerView?.PreviousTrackButton_Click(this, null); + } + + // Next Index + else if(e.Key == App.Settings.NextIndexKey) + { + playerView?.NextIndexButton_Click(this, null); + } + + // Previous Index + else if(e.Key == App.Settings.PreviousIndexKey) + { + playerView?.PreviousIndexButton_Click(this, null); + } + + // Fast Foward + else if(e.Key == App.Settings.FastForwardPlaybackKey) + { + playerView?.FastForwardButton_Click(this, null); + } + + // Rewind + else if(e.Key == App.Settings.RewindPlaybackKey) + { + playerView?.RewindButton_Click(this, null); + } + + // Volume Up + else if(e.Key == App.Settings.VolumeUpKey || e.Key == Key.VolumeUp) + { + int increment = 1; + if(e.KeyModifiers.HasFlag(KeyModifiers.Control)) + increment *= 2; + if(e.KeyModifiers.HasFlag(KeyModifiers.Shift)) + increment *= 5; + + if(playerView?.PlayerViewModel?.Volume != null) + playerView.PlayerViewModel.Volume += increment; + } + + // Volume Down + else if(e.Key == App.Settings.VolumeDownKey || e.Key == Key.VolumeDown) + { + int decrement = 1; + if(e.KeyModifiers.HasFlag(KeyModifiers.Control)) + decrement *= 2; + if(e.KeyModifiers.HasFlag(KeyModifiers.Shift)) + decrement *= 5; + + if (playerView?.PlayerViewModel?.Volume != null) + playerView.PlayerViewModel.Volume -= decrement; + } + + // Mute Toggle + else if(e.Key == App.Settings.ToggleMuteKey || e.Key == Key.VolumeMute) + { + playerView?.MuteToggleButton_Click(this, null); + } + + // Emphasis Toggle + else if(e.Key == App.Settings.ToggleDeEmphasisKey) + { + playerView?.EnableDisableDeEmphasisButton_Click(this, null); + } } #endregion diff --git a/RedBookPlayer/GUI/PlayerView.xaml.cs b/RedBookPlayer/GUI/PlayerView.xaml.cs index 533e076..7dabeae 100644 --- a/RedBookPlayer/GUI/PlayerView.xaml.cs +++ b/RedBookPlayer/GUI/PlayerView.xaml.cs @@ -11,16 +11,15 @@ using Avalonia.Markup.Xaml; using Avalonia.Media.Imaging; using Avalonia.Platform; using Avalonia.Threading; -using RedBookPlayer.Hardware; namespace RedBookPlayer.GUI { public class PlayerView : UserControl { /// - /// Player representing the internal state + /// Read-only access to the view model /// - public static Player Player = new Player(); + public PlayerViewModel PlayerViewModel => DataContext as PlayerViewModel; /// /// Set of images representing the digits for the UI @@ -64,10 +63,10 @@ namespace RedBookPlayer.GUI /// Path to the image to load public async Task LoadImage(string path) { - bool result = await Task.Run(() => + bool result = await Dispatcher.UIThread.InvokeAsync(() => { - Player.Init(path, App.Settings.AutoPlay); - return Player.Initialized; + PlayerViewModel.Init(path, App.Settings.AutoPlay, App.Settings.Volume); + return PlayerViewModel.Initialized; }); if(result) @@ -114,10 +113,17 @@ namespace RedBookPlayer.GUI DataContext = new PlayerViewModel(); // Load the theme - if (xaml != null) - new AvaloniaXamlLoader().Load(xaml, null, this); - else + try + { + if(xaml != null) + new AvaloniaXamlLoader().Load(xaml, null, this); + else + AvaloniaXamlLoader.Load(this); + } + catch + { AvaloniaXamlLoader.Load(this); + } InitializeDigits(); @@ -181,14 +187,14 @@ namespace RedBookPlayer.GUI { Dispatcher.UIThread.InvokeAsync(() => { - string digitString = Player.GenerateDigitString(); + string digitString = PlayerViewModel.GenerateDigitString(); for (int i = 0; i < _digits.Length; i++) { if (_digits[i] != null) _digits[i].Source = GetBitmap(digitString[i]); } - Player.UpdateDataContext(DataContext as PlayerViewModel); + PlayerViewModel?.UpdateView(); }); } @@ -202,32 +208,40 @@ namespace RedBookPlayer.GUI if (path == null) return; - LoadImage(path); + await LoadImage(path); } - public void PlayButton_Click(object sender, RoutedEventArgs e) => Player.TogglePlayPause(true); + public void PlayButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.Playing = true; - public void PauseButton_Click(object sender, RoutedEventArgs e) => Player.TogglePlayPause(false); + public void PauseButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.Playing = false; - public void PlayPauseButton_Click(object sender, RoutedEventArgs e) => Player.TogglePlayPause(!Player.Playing); + public void PlayPauseButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.Playing = !(PlayerViewModel.Playing ?? false); - public void StopButton_Click(object sender, RoutedEventArgs e) => Player.Stop(); + public void StopButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.Playing = null; - public void NextTrackButton_Click(object sender, RoutedEventArgs e) => Player.NextTrack(); + public void NextTrackButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.NextTrack(); - public void PreviousTrackButton_Click(object sender, RoutedEventArgs e) => Player.PreviousTrack(); + public void PreviousTrackButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.PreviousTrack(); - public void NextIndexButton_Click(object sender, RoutedEventArgs e) => Player.NextIndex(App.Settings.IndexButtonChangeTrack); + public void NextIndexButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.NextIndex(App.Settings.IndexButtonChangeTrack); - public void PreviousIndexButton_Click(object sender, RoutedEventArgs e) => Player.PreviousIndex(App.Settings.IndexButtonChangeTrack); + public void PreviousIndexButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.PreviousIndex(App.Settings.IndexButtonChangeTrack); - public void FastForwardButton_Click(object sender, RoutedEventArgs e) => Player.FastForward(); + public void FastForwardButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.FastForward(); - public void RewindButton_Click(object sender, RoutedEventArgs e) => Player.Rewind(); + public void RewindButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.Rewind(); - public void EnableDeEmphasisButton_Click(object sender, RoutedEventArgs e) => Player.ToggleDeEmphasis(true); + public void VolumeUpButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.Volume++; - public void DisableDeEmphasisButton_Click(object sender, RoutedEventArgs e) => Player.ToggleDeEmphasis(false); + 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 } diff --git a/RedBookPlayer/GUI/PlayerViewModel.cs b/RedBookPlayer/GUI/PlayerViewModel.cs index 5090377..4030ba5 100644 --- a/RedBookPlayer/GUI/PlayerViewModel.cs +++ b/RedBookPlayer/GUI/PlayerViewModel.cs @@ -1,16 +1,99 @@ +using System.Linq; +using Aaru.CommonTypes.Enums; using ReactiveUI; +using RedBookPlayer.Discs; +using RedBookPlayer.Hardware; namespace RedBookPlayer.GUI { public class PlayerViewModel : ReactiveObject { - private bool _applyDeEmphasis; + /// + /// 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 => _applyDeEmphasis; - set => this.RaiseAndSetIfChanged(ref _applyDeEmphasis, value); + 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 { @@ -45,5 +128,143 @@ namespace RedBookPlayer.GUI 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/GUI/SettingsWindow.xaml b/RedBookPlayer/GUI/SettingsWindow.xaml index 7f5c54a..ba416ba 100644 --- a/RedBookPlayer/GUI/SettingsWindow.xaml +++ b/RedBookPlayer/GUI/SettingsWindow.xaml @@ -1,40 +1,122 @@ - - Themes - - - - Auto-play CD on load - - - - Index navigation can change track - - - - Treat index 0 of track 1 as track 0 (hidden track) - - - - Play data tracks like old, non-compliant players - - - - Generate a TOC if the disc is missing one - - - Volume - - - - - - - - + d:DesignHeight="450" x:Class="RedBookPlayer.GUI.SettingsWindow" Title="Settings" SizeToContent="WidthAndHeight"> + + + + + Themes + + + + Auto-play CD on load + + + + Index navigation can change track + + + + Treat index 0 of track 1 as track 0 (hidden track) + + + + Play data tracks like old, non-compliant players + + + + Generate a TOC if the disc is missing one + + + Default Volume + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Load Image + + + + Toggle Play/Pause + + + + Stop Playback + + + + Next Track + + + + Previous Track + + + + Next Index + + + + Previous Index + + + + Fast-Forward + + + + Rewind + + + + Volume Up + + + + Volume Down + + + + Toggle Mute + + + + Toggle De-Emphasis + + + + + + \ No newline at end of file diff --git a/RedBookPlayer/GUI/SettingsWindow.xaml.cs b/RedBookPlayer/GUI/SettingsWindow.xaml.cs index cfe293e..3f1d226 100644 --- a/RedBookPlayer/GUI/SettingsWindow.xaml.cs +++ b/RedBookPlayer/GUI/SettingsWindow.xaml.cs @@ -1,6 +1,8 @@ +using System; using System.Collections.Generic; using System.IO; using Avalonia.Controls; +using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; @@ -36,6 +38,7 @@ namespace RedBookPlayer.GUI MainWindow.ApplyTheme(_selectedTheme); } + SaveKeyboardList(); _settings.Save(); } @@ -46,6 +49,7 @@ namespace RedBookPlayer.GUI AvaloniaXamlLoader.Load(this); PopulateThemes(); + PopulateKeyboardList(); this.FindControl public bool Initialized { get; private set; } = false; + /// + /// OpticalDisc object + /// + public OpticalDisc OpticalDisc { get; private set; } + /// /// Indicate if the disc is playing /// - public bool Playing => _soundOutput?.Playing ?? false; + 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 - /// - /// OpticalDisc object - /// - private OpticalDisc _opticalDisc; - /// /// Sound output handling class /// @@ -40,17 +86,18 @@ namespace RedBookPlayer.Hardware #endregion /// - /// Initialize the player with a given image path + /// Create a new Player from a given image path /// /// Path to the disc image /// True if playback should begin immediately, false otherwise - public void Init(string path, bool autoPlay = false) + /// Default volume between 0 and 100 to use when starting playback + public Player(string path, bool autoPlay = false, int defaultVolume = 100) { - // Reset the internal state for initialization + // Set the internal state for initialization Initialized = false; _soundOutput = new SoundOutput(); _soundOutput.ApplyDeEmphasis = false; - _opticalDisc = null; + OpticalDisc = null; try { @@ -65,7 +112,7 @@ namespace RedBookPlayer.Hardware image.Open(filter); // Generate and instantiate the disc - _opticalDisc = OpticalDiscFactory.GenerateFromImage(image, App.Settings.AutoPlay); + OpticalDisc = OpticalDiscFactory.GenerateFromImage(image, autoPlay); } catch { @@ -74,7 +121,7 @@ namespace RedBookPlayer.Hardware } // Initialize the sound output - _soundOutput.Init(_opticalDisc, autoPlay); + _soundOutput.Init(OpticalDisc, autoPlay, defaultVolume); if(_soundOutput == null || !_soundOutput.Initialized) return; @@ -84,54 +131,22 @@ namespace RedBookPlayer.Hardware #region Playback - /// - /// Toggle audio playback - /// - /// True to start playback, false to pause - public void TogglePlayPause(bool start) - { - if(_opticalDisc == null || !_opticalDisc.Initialized) - return; - - if(start) - { - _soundOutput.Play(); - _opticalDisc.SetTotalIndexes(); - } - else - { - _soundOutput.Stop(); - } - } - - /// - /// Stop the current audio playback - /// - public void Stop() - { - if(_opticalDisc == null || !_opticalDisc.Initialized) - return; - - _soundOutput.Stop(); - _opticalDisc.LoadFirstTrack(); - } - /// /// Move to the next playable track /// public void NextTrack() { - if(_opticalDisc == null || !_opticalDisc.Initialized) + if(OpticalDisc == null || !OpticalDisc.Initialized) return; - bool wasPlaying = Playing; - if(wasPlaying) TogglePlayPause(false); + bool? wasPlaying = Playing; + if(wasPlaying == true) Playing = false; - _opticalDisc.NextTrack(); - if(_opticalDisc is CompactDisc compactDisc) + OpticalDisc.NextTrack(); + if(OpticalDisc is CompactDisc compactDisc) _soundOutput.ApplyDeEmphasis = compactDisc.TrackHasEmphasis; - if(wasPlaying) TogglePlayPause(true); + if(wasPlaying == true) Playing = true; } /// @@ -139,17 +154,17 @@ namespace RedBookPlayer.Hardware /// public void PreviousTrack() { - if(_opticalDisc == null || !_opticalDisc.Initialized) + if(OpticalDisc == null || !OpticalDisc.Initialized) return; - bool wasPlaying = Playing; - if(wasPlaying) TogglePlayPause(false); + bool? wasPlaying = Playing; + if(wasPlaying == true) Playing = false; - _opticalDisc.PreviousTrack(); - if(_opticalDisc is CompactDisc compactDisc) + OpticalDisc.PreviousTrack(); + if(OpticalDisc is CompactDisc compactDisc) _soundOutput.ApplyDeEmphasis = compactDisc.TrackHasEmphasis; - if(wasPlaying) TogglePlayPause(true); + if(wasPlaying == true) Playing = true; } /// @@ -158,17 +173,17 @@ namespace RedBookPlayer.Hardware /// True if index changes can trigger a track change, false otherwise public void NextIndex(bool changeTrack) { - if(_opticalDisc == null || !_opticalDisc.Initialized) + if(OpticalDisc == null || !OpticalDisc.Initialized) return; - bool wasPlaying = Playing; - if(wasPlaying) TogglePlayPause(false); + bool? wasPlaying = Playing; + if(wasPlaying == true) Playing = false; - _opticalDisc.NextIndex(changeTrack); - if(_opticalDisc is CompactDisc compactDisc) + OpticalDisc.NextIndex(changeTrack); + if(OpticalDisc is CompactDisc compactDisc) _soundOutput.ApplyDeEmphasis = compactDisc.TrackHasEmphasis; - if(wasPlaying) TogglePlayPause(true); + if(wasPlaying == true) Playing = true; } /// @@ -177,17 +192,17 @@ namespace RedBookPlayer.Hardware /// True if index changes can trigger a track change, false otherwise public void PreviousIndex(bool changeTrack) { - if(_opticalDisc == null || !_opticalDisc.Initialized) + if(OpticalDisc == null || !OpticalDisc.Initialized) return; - bool wasPlaying = Playing; - if(wasPlaying) TogglePlayPause(false); + bool? wasPlaying = Playing; + if(wasPlaying == true) Playing = false; - _opticalDisc.PreviousIndex(changeTrack); - if(_opticalDisc is CompactDisc compactDisc) + OpticalDisc.PreviousIndex(changeTrack); + if(OpticalDisc is CompactDisc compactDisc) _soundOutput.ApplyDeEmphasis = compactDisc.TrackHasEmphasis; - if(wasPlaying) TogglePlayPause(true); + if(wasPlaying == true) Playing = true; } /// @@ -195,10 +210,10 @@ namespace RedBookPlayer.Hardware /// public void FastForward() { - if(_opticalDisc == null || !_opticalDisc.Initialized) + if(OpticalDisc == null || !OpticalDisc.Initialized) return; - _opticalDisc.CurrentSector = Math.Min(_opticalDisc.TotalSectors, _opticalDisc.CurrentSector + 75); + OpticalDisc.CurrentSector = Math.Min(OpticalDisc.TotalSectors, OpticalDisc.CurrentSector + 75); } /// @@ -206,11 +221,11 @@ namespace RedBookPlayer.Hardware /// public void Rewind() { - if(_opticalDisc == null || !_opticalDisc.Initialized) + if(OpticalDisc == null || !OpticalDisc.Initialized) return; - if(_opticalDisc.CurrentSector >= 75) - _opticalDisc.CurrentSector -= 75; + if(OpticalDisc.CurrentSector >= 75) + OpticalDisc.CurrentSector -= 75; } #endregion @@ -218,75 +233,25 @@ namespace RedBookPlayer.Hardware #region Helpers /// - /// Generate the digit string to be interpreted by the frontend + /// Get current sector time, accounting for offsets /// - /// String representing the digits for the frontend - public string GenerateDigitString() + /// ulong representing the current sector time + public ulong GetCurrentSectorTime() { - // If the disc isn't initialized, return all '-' characters - if(_opticalDisc == null || !_opticalDisc.Initialized) - return string.Empty.PadLeft(20, '-'); - - // Otherwise, take the current time into account - ulong sectorTime = _opticalDisc.CurrentSector; - if(_opticalDisc.SectionStartSector != 0) - sectorTime -= _opticalDisc.SectionStartSector; + ulong sectorTime = OpticalDisc.CurrentSector; + if (OpticalDisc.SectionStartSector != 0) + sectorTime -= OpticalDisc.SectionStartSector; else - sectorTime += _opticalDisc.TimeOffset; + sectorTime += OpticalDisc.TimeOffset; - int[] numbers = new int[] - { - _opticalDisc.CurrentTrackNumber + 1, - _opticalDisc.CurrentTrackIndex, - - (int)(sectorTime / (75 * 60)), - (int)(sectorTime / 75 % 60), - (int)(sectorTime % 75), - - _opticalDisc.TotalTracks, - _opticalDisc.TotalIndexes, - - (int)(_opticalDisc.TotalTime / (75 * 60)), - (int)(_opticalDisc.TotalTime / 75 % 60), - (int)(_opticalDisc.TotalTime % 75), - }; - - return string.Join("", numbers.Select(i => i.ToString().PadLeft(2, '0').Substring(0, 2))); + return sectorTime; } /// - /// Toggle de-emphasis processing + /// Set if de-emphasis should be applied /// - /// True to apply de-emphasis, false otherwise - public void ToggleDeEmphasis(bool enable) => _soundOutput.ToggleDeEmphasis(enable); - - /// - /// Update the data context for the frontend - /// - /// Data context to be updated - public void UpdateDataContext(PlayerViewModel dataContext) - { - if(!Initialized || dataContext == null) - return; - - dataContext.HiddenTrack = _opticalDisc.TimeOffset > 150; - dataContext.ApplyDeEmphasis = _soundOutput.ApplyDeEmphasis; - - if(_opticalDisc is CompactDisc compactDisc) - { - dataContext.QuadChannel = compactDisc.QuadChannel; - dataContext.IsDataTrack = compactDisc.IsDataTrack; - dataContext.CopyAllowed = compactDisc.CopyAllowed; - dataContext.TrackHasEmphasis = compactDisc.TrackHasEmphasis; - } - else - { - dataContext.QuadChannel = false; - dataContext.IsDataTrack = _opticalDisc.TrackType != TrackType.Audio; - dataContext.CopyAllowed = false; - dataContext.TrackHasEmphasis = false; - } - } + /// True to enable, false to disable + public void SetDeEmphasis(bool apply) => _soundOutput?.SetDeEmphasis(apply); #endregion } diff --git a/RedBookPlayer/Hardware/SoundOutput.cs b/RedBookPlayer/Hardware/SoundOutput.cs index a88dab7..d31b68c 100644 --- a/RedBookPlayer/Hardware/SoundOutput.cs +++ b/RedBookPlayer/Hardware/SoundOutput.cs @@ -27,6 +27,23 @@ namespace RedBookPlayer.Hardware /// public bool Playing => _soundOut.PlaybackState == PlaybackState.Playing; + /// + /// Current playback volume + /// + public int Volume + { + get => _volume; + set + { + if(value > 100) + _volume = 100; + else if(value < 0) + _volume = 0; + else + _volume = value; + } + } + #endregion #region Private State Variables @@ -44,6 +61,11 @@ namespace RedBookPlayer.Hardware /// private OpticalDisc _opticalDisc; + /// + /// Internal value for the volume + /// + private int _volume; + /// /// Data provider for sound output /// @@ -76,7 +98,8 @@ namespace RedBookPlayer.Hardware /// /// OpticalDisc to load from /// True if playback should begin immediately, false otherwise - public void Init(OpticalDisc opticalDisc, bool autoPlay = false) + /// Default volume between 0 and 100 to use when starting playback + public void Init(OpticalDisc opticalDisc, bool autoPlay = false, int defaultVolume = 100) { // If we have an unusable disc, just return if(opticalDisc == null || !opticalDisc.Initialized) @@ -85,6 +108,9 @@ namespace RedBookPlayer.Hardware // Save a reference to the disc _opticalDisc = opticalDisc; + // Set the initial playback volume + Volume = defaultVolume; + // Enable de-emphasis for CDs, if necessary if(opticalDisc is CompactDisc compactDisc) ApplyDeEmphasis = compactDisc.TrackHasEmphasis; @@ -116,7 +142,7 @@ namespace RedBookPlayer.Hardware public int ProviderRead(byte[] buffer, int offset, int count) { // Set the current volume - _soundOut.Volume = (float)App.Settings.Volume / 100; + _soundOut.Volume = (float)Volume / 100; // Determine how many sectors we can read ulong sectorsToRead; @@ -243,7 +269,7 @@ namespace RedBookPlayer.Hardware /// Toggle de-emphasis processing /// /// True to apply de-emphasis, false otherwise - public void ToggleDeEmphasis(bool enable) => ApplyDeEmphasis = enable; + public void SetDeEmphasis(bool enable) => ApplyDeEmphasis = enable; /// /// Sets or resets the de-emphasis filters diff --git a/RedBookPlayer/Settings.cs b/RedBookPlayer/Settings.cs index fe144a0..36e95fb 100644 --- a/RedBookPlayer/Settings.cs +++ b/RedBookPlayer/Settings.cs @@ -1,12 +1,15 @@ using System; using System.IO; using System.Text.Json; +using Avalonia.Input; using RedBookPlayer.GUI; namespace RedBookPlayer { public class Settings { + #region Player Settings + /// /// Indicates if discs should start playing on load /// @@ -35,18 +38,111 @@ namespace RedBookPlayer /// /// Indicates the default playback volume /// - public int Volume { get; set; } = 100; + public int Volume + { + get => _volume; + set + { + if(value > 100) + _volume = 100; + else if(value < 0) + _volume = 0; + else + _volume = value; + } + } /// /// Indicates the currently selected theme /// public string SelectedTheme { get; set; } = "default"; + #endregion + + #region Key Mappings + + /// + /// Key assigned to open settings + /// + public Key OpenSettingsKey { get; set; } = Key.F1; + + /// + /// Key assigned to load a new image + /// + public Key LoadImageKey { get; set; } = Key.F2; + + /// + /// Key assigned to toggle play and pause + /// + public Key TogglePlaybackKey { get; set; } = Key.Space; + + /// + /// Key assigned to stop playback + /// + public Key StopPlaybackKey { get; set; } = Key.Escape; + + /// + /// Key assigned to move to the next track + /// + public Key NextTrackKey { get; set; } = Key.Right; + + /// + /// Key assigned to move to the previous track + /// + public Key PreviousTrackKey { get; set; } = Key.Left; + + /// + /// Key assigned to move to the next index + /// + public Key NextIndexKey { get; set; } = Key.OemCloseBrackets; + + /// + /// Key assigned to move to the previous index + /// + public Key PreviousIndexKey { get; set; } = Key.OemOpenBrackets; + + /// + /// Key assigned to fast forward playback + /// + public Key FastForwardPlaybackKey { get; set; } = Key.OemPeriod; + + /// + /// Key assigned to rewind playback + /// + public Key RewindPlaybackKey { get; set; } = Key.OemComma; + + /// + /// Key assigned to raise volume + /// + public Key VolumeUpKey { get; set; } = Key.Add; + + /// + /// Key assigned to lower volume + /// + public Key VolumeDownKey { get; set; } = Key.Subtract; + + /// + /// Key assigned to toggle mute + /// + public Key ToggleMuteKey { get; set; } = Key.M; + + /// + /// Key assigned to toggle de-emphasis + /// + public Key ToggleDeEmphasisKey { get; set; } = Key.E; + + #endregion + /// /// Path to the settings file /// private string _filePath; + /// + /// Internal value for the volume + /// + private int _volume = 100; + public Settings() {} public Settings(string filePath) => _filePath = filePath;