diff --git a/README.md b/README.md index a8c710e..75cdedd 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,11 @@ | **Space** | Toggle Play / Pause | | **Esc** | Stop Playback | | **~** | Eject | +| **Page Up** | Next Disc | +| **Page Down** | Previous Disc | | **→** | Next Track | | **←** | Previous Track | +| **R** | Shuffle Tracks | | **]** | Next Index | | **[** | Previous Index | | **.** | Fast Forward | @@ -31,6 +34,13 @@ For Save Track(s): - Holding no modifying keys will prompt to save the current track - Holding **Shift** will prompt to save all tracks (including hidden) +For Disc Switching: +- If you change the number of discs in the internal changer, you must restart the program for it to take effect + +For Shuffling: +- Shuffling only works on the current set of playable tracks +- If you are in single disc mode and switch discs, it will not automatically shuffle the new tracks + For both Volume Up and Volume Down: - Holding **Ctrl** will move in increments of 2 - Holding **Shift** will move in increments of 5 diff --git a/RedBookPlayer.GUI/RedBookPlayer.GUI.csproj b/RedBookPlayer.GUI/RedBookPlayer.GUI.csproj index 22e946e..c0be0a2 100644 --- a/RedBookPlayer.GUI/RedBookPlayer.GUI.csproj +++ b/RedBookPlayer.GUI/RedBookPlayer.GUI.csproj @@ -9,6 +9,7 @@ WindowsDebug + %(Filename) diff --git a/RedBookPlayer.GUI/ViewModels/MainViewModel.cs b/RedBookPlayer.GUI/ViewModels/MainViewModel.cs index c3a2a0e..aaac01b 100644 --- a/RedBookPlayer.GUI/ViewModels/MainViewModel.cs +++ b/RedBookPlayer.GUI/ViewModels/MainViewModel.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Linq; using Avalonia.Controls; using Avalonia.Input; using RedBookPlayer.GUI.Views; @@ -75,6 +76,18 @@ namespace RedBookPlayer.GUI.ViewModels PlayerView?.ViewModel?.ExecuteEject(); } + // Next Disc + else if(e.Key == App.Settings.NextDiscKey) + { + PlayerView?.ViewModel?.ExecuteNextDisc(); + } + + // Previous Disc + else if(e.Key == App.Settings.PreviousDiscKey) + { + PlayerView?.ViewModel?.ExecutePreviousDisc(); + } + // Next Track else if(e.Key == App.Settings.NextTrackKey || e.Key == Key.MediaNextTrack) { @@ -87,6 +100,12 @@ namespace RedBookPlayer.GUI.ViewModels PlayerView?.ViewModel?.ExecutePreviousTrack(); } + // Shuffle Track List + else if(e.Key == App.Settings.ShuffleTracksKey) + { + PlayerView?.ViewModel?.ExecuteShuffle(); + } + // Next Index else if(e.Key == App.Settings.NextIndexKey) { @@ -112,7 +131,7 @@ namespace RedBookPlayer.GUI.ViewModels } // Volume Up - else if(e.Key == App.Settings.VolumeUpKey || e.Key == Key.VolumeUp) + else if(e.Key == App.Settings.VolumeUpKey) { int increment = 1; if(e.KeyModifiers.HasFlag(KeyModifiers.Control)) @@ -125,7 +144,7 @@ namespace RedBookPlayer.GUI.ViewModels } // Volume Down - else if(e.Key == App.Settings.VolumeDownKey || e.Key == Key.VolumeDown) + else if(e.Key == App.Settings.VolumeDownKey) { int decrement = 1; if(e.KeyModifiers.HasFlag(KeyModifiers.Control)) @@ -138,7 +157,7 @@ namespace RedBookPlayer.GUI.ViewModels } // Mute Toggle - else if(e.Key == App.Settings.ToggleMuteKey || e.Key == Key.VolumeMute) + else if(e.Key == App.Settings.ToggleMuteKey) { PlayerView?.ViewModel?.ExecuteToggleMute(); } @@ -151,19 +170,35 @@ namespace RedBookPlayer.GUI.ViewModels } /// - /// Load the first valid drag-and-dropped disc image + /// Load the all valid drag-and-dropped disc images /// + /// If more than the number of discs in the changer are added, it will begin to overwrite public async void ExecuteLoadDragDrop(object sender, DragEventArgs e) { if(PlayerView?.ViewModel == null) return; IEnumerable fileNames = e.Data.GetFileNames(); - foreach(string filename in fileNames) + if(fileNames == null || fileNames.Count() == 0) { - bool loaded = await PlayerView.ViewModel.LoadImage(filename); - if(loaded) - break; + return; + } + else if(fileNames.Count() == 1) + { + await PlayerView.ViewModel.LoadImage(fileNames.FirstOrDefault()); + } + else + { + int lastDisc = PlayerView.ViewModel.CurrentDisc; + foreach(string path in fileNames) + { + await PlayerView.ViewModel.LoadImage(path); + + if(PlayerView.ViewModel.Initialized) + PlayerView.ViewModel.ExecuteNextDisc(); + } + + PlayerView.ViewModel.SelectDisc(lastDisc); } } diff --git a/RedBookPlayer.GUI/ViewModels/PlayerViewModel.cs b/RedBookPlayer.GUI/ViewModels/PlayerViewModel.cs index 1750063..21610ba 100644 --- a/RedBookPlayer.GUI/ViewModels/PlayerViewModel.cs +++ b/RedBookPlayer.GUI/ViewModels/PlayerViewModel.cs @@ -31,8 +31,28 @@ namespace RedBookPlayer.GUI.ViewModels #region Player Passthrough + /// + /// Currently selected disc + /// + public int CurrentDisc + { + get => _currentDisc; + private set => this.RaiseAndSetIfChanged(ref _currentDisc, value); + } + + private int _currentDisc; + #region OpticalDisc Passthrough + /// + /// Path to the disc image + /// + public string ImagePath + { + get => _imagePath; + private set => this.RaiseAndSetIfChanged(ref _imagePath, value); + } + /// /// Current track number /// @@ -148,6 +168,7 @@ namespace RedBookPlayer.GUI.ViewModels /// public ulong TotalTime => _player.TotalTime; + private string _imagePath; private int _currentTrackNumber; private ushort _currentTrackIndex; private ushort _currentTrackSession; @@ -263,6 +284,16 @@ namespace RedBookPlayer.GUI.ViewModels /// public ReactiveCommand EjectCommand { get; } + /// + /// Command for moving to the next disc + /// + public ReactiveCommand NextDiscCommand { get; } + + /// + /// Command for moving to the previous disc + /// + public ReactiveCommand PreviousDiscCommand { get; } + /// /// Command for moving to the next track /// @@ -348,6 +379,8 @@ namespace RedBookPlayer.GUI.ViewModels TogglePlayPauseCommand = ReactiveCommand.Create(ExecuteTogglePlayPause); StopCommand = ReactiveCommand.Create(ExecuteStop); EjectCommand = ReactiveCommand.Create(ExecuteEject); + NextDiscCommand = ReactiveCommand.Create(ExecuteNextDisc); + PreviousDiscCommand = ReactiveCommand.Create(ExecutePreviousDisc); NextTrackCommand = ReactiveCommand.Create(ExecuteNextTrack); PreviousTrackCommand = ReactiveCommand.Create(ExecutePreviousTrack); NextIndexCommand = ReactiveCommand.Create(ExecuteNextIndex); @@ -364,7 +397,9 @@ namespace RedBookPlayer.GUI.ViewModels ToggleDeEmphasisCommand = ReactiveCommand.Create(ExecuteToggleDeEmphasis); // Initialize Player - _player = new Player(App.Settings.Volume); + _player = new Player(App.Settings.NumberOfDiscs, App.Settings.Volume); + _player.PropertyChanged += PlayerStateChanged; + PlayerStateChanged(this, null); PlayerState = PlayerState.NoDisc; } @@ -372,25 +407,22 @@ namespace RedBookPlayer.GUI.ViewModels /// Initialize the view model with a given image path /// /// Path to the disc image - /// Options to pass to the optical disc factory - /// RepeatMode for sound output + /// Options to pass to the player + /// Options to pass to the optical disc factory /// True if playback should begin immediately, false otherwise - public void Init(string path, OpticalDiscOptions options, RepeatMode repeatMode, bool autoPlay) + public void Init(string path, PlayerOptions playerOptions, OpticalDiscOptions opticalDiscOptions, bool autoPlay) { // Stop current playback, if necessary if(PlayerState != PlayerState.NoDisc) ExecuteStop(); // Attempt to initialize Player - _player.Init(path, options, repeatMode, autoPlay); + _player.Init(path, playerOptions, opticalDiscOptions, autoPlay); if(_player.Initialized) - { - _player.PropertyChanged += PlayerStateChanged; PlayerStateChanged(this, null); - } } - #region Playback + #region Playback (UI) /// /// Begin playback @@ -407,6 +439,11 @@ namespace RedBookPlayer.GUI.ViewModels /// public void ExecuteTogglePlayPause() => _player?.TogglePlayback(); + /// + /// Shuffle the current track list + /// + public void ExecuteShuffle() => _player?.ShuffleTracks(); + /// /// Stop current playback /// @@ -417,6 +454,16 @@ namespace RedBookPlayer.GUI.ViewModels /// public void ExecuteEject() => _player?.Eject(); + /// + /// Move to the next disc + /// + public void ExecuteNextDisc() => _player?.NextDisc(); + + /// + /// Move to the previous disc + /// + public void ExecutePreviousDisc() => _player?.PreviousDisc(); + /// /// Move to the next playable track /// @@ -449,6 +496,29 @@ namespace RedBookPlayer.GUI.ViewModels #endregion + #region Playback (Internal) + + /// + /// Select a particular disc by number + /// + /// Disc number to attempt to load + public void SelectDisc(int discNumber) => _player?.SelectDisc(discNumber); + + /// + /// Select a particular index by number + /// + /// Track index to attempt to load + /// True if index changes can trigger a track change, false otherwise + public void SelectIndex(ushort index, bool changeTrack) => _player?.SelectIndex(index, changeTrack); + + /// + /// Select a particular track by number + /// + /// Track number to attempt to load + public void SelectTrack(int trackNumber) => _player?.SelectTrack(trackNumber); + + #endregion + #region Volume /// @@ -493,6 +563,111 @@ namespace RedBookPlayer.GUI.ViewModels #endregion + #region Extraction + + /// + /// Extract a single track from the image to WAV + /// + /// + /// Output path to write data to + public void ExtractSingleTrackToWav(uint trackNumber, string outputDirectory) => _player?.ExtractSingleTrackToWav(trackNumber, outputDirectory); + + /// + /// Extract all tracks from the image to WAV + /// + /// Output path to write data to + public void ExtractAllTracksToWav(string outputDirectory) => _player?.ExtractAllTracksToWav(outputDirectory); + + #endregion + + #region Setters + + /// + /// Set data playback method [CompactDisc only] + /// + /// New playback value + public void SetDataPlayback(DataPlayback dataPlayback) => _player?.SetDataPlayback(dataPlayback); + + /// + /// Set disc handling method + /// + /// New playback value + public void SetDiscHandling(DiscHandling discHandling) => _player?.SetDiscHandling(discHandling); + + /// + /// Set the value for loading hidden tracks [CompactDisc only] + /// + /// True to enable loading hidden tracks, false otherwise + public void SetLoadHiddenTracks(bool load) => _player?.SetLoadHiddenTracks(load); + + /// + /// Set repeat mode + /// + /// New repeat mode value + public void SetRepeatMode(RepeatMode repeatMode) => _player?.SetRepeatMode(repeatMode); + + /// + /// Set session handling + /// + /// New session handling value + public void SetSessionHandling(SessionHandling sessionHandling) => _player?.SetSessionHandling(sessionHandling); + + #endregion + + #region State Change Event Handlers + + /// + /// Update the view-model from the Player + /// + private void PlayerStateChanged(object sender, PropertyChangedEventArgs e) + { + if(_player == null) + return; + + if(!_player.Initialized) + { + Dispatcher.UIThread.InvokeAsync(() => + { + App.MainWindow.Title = "RedBookPlayer"; + }); + } + + ImagePath = _player.ImagePath; + Initialized = _player.Initialized; + + if(!string.IsNullOrWhiteSpace(ImagePath) && Initialized) + { + Dispatcher.UIThread.InvokeAsync(() => + { + App.MainWindow.Title = "RedBookPlayer - " + ImagePath.Split('/').Last().Split('\\').Last(); + }); + } + + CurrentDisc = _player.CurrentDisc + 1; + CurrentTrackNumber = _player.CurrentTrackNumber; + CurrentTrackIndex = _player.CurrentTrackIndex; + CurrentTrackSession = _player.CurrentTrackSession; + CurrentSector = _player.CurrentSector; + SectionStartSector = _player.SectionStartSector; + + HiddenTrack = _player.HiddenTrack; + + QuadChannel = _player.QuadChannel; + IsDataTrack = _player.IsDataTrack; + CopyAllowed = _player.CopyAllowed; + TrackHasEmphasis = _player.TrackHasEmphasis; + + PlayerState = _player.PlayerState; + DataPlayback = _player.DataPlayback; + RepeatMode = _player.RepeatMode; + ApplyDeEmphasis = _player.ApplyDeEmphasis; + Volume = _player.Volume; + + UpdateDigits(); + } + + #endregion + #region Helpers /// @@ -540,11 +715,28 @@ namespace RedBookPlayer.GUI.ViewModels /// public async void ExecuteLoad() { - string path = await GetPath(); - if(path == null) + string[] paths = await GetPaths(); + if(paths == null || paths.Length == 0) + { return; + } + else if(paths.Length == 1) + { + await LoadImage(paths[0]); + } + else + { + int lastDisc = CurrentDisc; + foreach(string path in paths) + { + await LoadImage(path); + + if(Initialized) + ExecuteNextDisc(); + } - await LoadImage(path); + SelectDisc(lastDisc); + } } /// @@ -593,19 +785,25 @@ namespace RedBookPlayer.GUI.ViewModels { return await Dispatcher.UIThread.InvokeAsync(() => { - OpticalDiscOptions options = new OpticalDiscOptions + PlayerOptions playerOptions = new PlayerOptions { DataPlayback = App.Settings.DataPlayback, - GenerateMissingToc = App.Settings.GenerateMissingTOC, + DiscHandling = App.Settings.DiscHandling, LoadHiddenTracks = App.Settings.PlayHiddenTracks, + RepeatMode = App.Settings.RepeatMode, SessionHandling = App.Settings.SessionHandling, }; + OpticalDiscOptions opticalDiscOptions = new OpticalDiscOptions + { + GenerateMissingToc = App.Settings.GenerateMissingTOC, + }; + // Ensure the context and view model are set App.PlayerView.DataContext = this; App.PlayerView.ViewModel = this; - Init(path, options, App.Settings.RepeatMode, App.Settings.AutoPlay); + Init(path, playerOptions, opticalDiscOptions, App.Settings.AutoPlay); if(Initialized) App.MainWindow.Title = "RedBookPlayer - " + path.Split('/').Last().Split('\\').Last(); @@ -619,48 +817,12 @@ namespace RedBookPlayer.GUI.ViewModels public void RefreshFromSettings() { SetDataPlayback(App.Settings.DataPlayback); + SetDiscHandling(App.Settings.DiscHandling); SetLoadHiddenTracks(App.Settings.PlayHiddenTracks); SetRepeatMode(App.Settings.RepeatMode); SetSessionHandling(App.Settings.SessionHandling); } - /// - /// Extract a single track from the image to WAV - /// - /// - /// Output path to write data to - public void ExtractSingleTrackToWav(uint trackNumber, string outputDirectory) => _player?.ExtractSingleTrackToWav(trackNumber, outputDirectory); - - /// - /// Extract all tracks from the image to WAV - /// - /// Output path to write data to - public void ExtractAllTracksToWav(string outputDirectory) => _player?.ExtractAllTracksToWav(outputDirectory); - - /// - /// Set data playback method [CompactDisc only] - /// - /// New playback value - public void SetDataPlayback(DataPlayback dataPlayback) => _player?.SetDataPlayback(dataPlayback); - - /// - /// Set the value for loading hidden tracks [CompactDisc only] - /// - /// True to enable loading hidden tracks, false otherwise - public void SetLoadHiddenTracks(bool load) => _player?.SetLoadHiddenTracks(load); - - /// - /// Set repeat mode - /// - /// New repeat mode value - public void SetRepeatMode(RepeatMode repeatMode) => _player?.SetRepeatMode(repeatMode); - - /// - /// Set session handling - /// - /// New session handling value - public void SetSessionHandling(SessionHandling sessionHandling) => _player?.SetSessionHandling(sessionHandling); - /// /// Generate the digit string to be interpreted by the frontend /// @@ -737,12 +899,12 @@ namespace RedBookPlayer.GUI.ViewModels /// /// Generate a path selection dialog box /// - /// User-selected path, if possible - private async Task GetPath() + /// User-selected paths, if possible + private async Task GetPaths() { return await Dispatcher.UIThread.InvokeAsync(async () => { - var dialog = new OpenFileDialog { AllowMultiple = false }; + var dialog = new OpenFileDialog { AllowMultiple = true }; List knownExtensions = new Aaru.DiscImages.AaruFormat().KnownExtensions.ToList(); dialog.Filters.Add(new FileDialogFilter() { @@ -750,7 +912,7 @@ namespace RedBookPlayer.GUI.ViewModels Extensions = knownExtensions.ConvertAll(e => e.TrimStart('.')) }); - return (await dialog.ShowAsync(App.MainWindow))?.FirstOrDefault(); + return (await dialog.ShowAsync(App.MainWindow)); }); } @@ -782,46 +944,6 @@ namespace RedBookPlayer.GUI.ViewModels UpdateDigits(); } - /// - /// Update the view-model from the Player - /// - private void PlayerStateChanged(object sender, PropertyChangedEventArgs e) - { - if(_player == null) - return; - - if(!_player.Initialized) - { - Dispatcher.UIThread.InvokeAsync(() => - { - App.MainWindow.Title = "RedBookPlayer"; - }); - } - - Initialized = _player.Initialized; - - CurrentTrackNumber = _player.CurrentTrackNumber; - CurrentTrackIndex = _player.CurrentTrackIndex; - CurrentTrackSession = _player.CurrentTrackSession; - CurrentSector = _player.CurrentSector; - SectionStartSector = _player.SectionStartSector; - - HiddenTrack = _player.HiddenTrack; - - QuadChannel = _player.QuadChannel; - IsDataTrack = _player.IsDataTrack; - CopyAllowed = _player.CopyAllowed; - TrackHasEmphasis = _player.TrackHasEmphasis; - - PlayerState = _player.PlayerState; - DataPlayback = _player.DataPlayback; - RepeatMode = _player.RepeatMode; - ApplyDeEmphasis = _player.ApplyDeEmphasis; - Volume = _player.Volume; - - UpdateDigits(); - } - /// /// Update UI /// diff --git a/RedBookPlayer.GUI/ViewModels/SettingsViewModel.cs b/RedBookPlayer.GUI/ViewModels/SettingsViewModel.cs index 6473f7c..fad9566 100644 --- a/RedBookPlayer.GUI/ViewModels/SettingsViewModel.cs +++ b/RedBookPlayer.GUI/ViewModels/SettingsViewModel.cs @@ -21,6 +21,12 @@ namespace RedBookPlayer.GUI.ViewModels [JsonIgnore] public List DataPlaybackValues => GenerateDataPlaybackList(); + /// + /// List of all disc handling values + /// + [JsonIgnore] + public List DiscHandlingValues => GenerateDiscHandlingList(); + /// /// List of all repeat mode values /// @@ -44,6 +50,16 @@ namespace RedBookPlayer.GUI.ViewModels /// public bool AutoPlay { get; set; } = false; + /// + /// Indicates the number of discs to allow loading and changing + /// + public int NumberOfDiscs { get; set; } = 1; + + /// + /// Indicates how to deal with multiple discs + /// + public DiscHandling DiscHandling { get; set; } = DiscHandling.SingleDisc; + /// /// Indicates if an index change can trigger a track change /// @@ -144,6 +160,16 @@ namespace RedBookPlayer.GUI.ViewModels /// public Key EjectKey { get; set; } = Key.OemTilde; + /// + /// Key assigned to move to the next disc + /// + public Key NextDiscKey { get; set; } = Key.PageUp; + + /// + /// Key assigned to move to the previous disc + /// + public Key PreviousDiscKey { get; set; } = Key.PageDown; + /// /// Key assigned to move to the next track /// @@ -154,6 +180,11 @@ namespace RedBookPlayer.GUI.ViewModels /// public Key PreviousTrackKey { get; set; } = Key.Left; + /// + /// Key assigned to shuffling the track list + /// + public Key ShuffleTracksKey { get; set; } = Key.R; + /// /// Key assigned to move to the next index /// @@ -275,6 +306,11 @@ namespace RedBookPlayer.GUI.ViewModels /// private List GenerateDataPlaybackList() => Enum.GetValues(typeof(DataPlayback)).Cast().ToList(); + /// + /// Generate the list of DiscHandling values + /// + private List GenerateDiscHandlingList() => Enum.GetValues(typeof(DiscHandling)).Cast().ToList(); + /// /// Generate the list of Key values /// diff --git a/RedBookPlayer.GUI/Views/PlayerView.xaml b/RedBookPlayer.GUI/Views/PlayerView.xaml index c599c86..d954853 100644 --- a/RedBookPlayer.GUI/Views/PlayerView.xaml +++ b/RedBookPlayer.GUI/Views/PlayerView.xaml @@ -8,19 +8,19 @@ - + - - - + + + - - - - - Rewind - Fast Forward + + + + + Rewind + Fast Forward @@ -81,12 +81,12 @@ - - @@ -103,7 +103,8 @@ 4CH HIDDEN HIDDEN - + + \ No newline at end of file diff --git a/RedBookPlayer.GUI/Views/SettingsWindow.xaml b/RedBookPlayer.GUI/Views/SettingsWindow.xaml index 4000a8a..de5fa6b 100644 --- a/RedBookPlayer.GUI/Views/SettingsWindow.xaml +++ b/RedBookPlayer.GUI/Views/SettingsWindow.xaml @@ -26,20 +26,30 @@ Play hidden tracks - Data Track Playback - Data Track Playback + - Session Handling - Session Handling + - Repeat Mode - Repeat Mode + + + Discs in Changer + + + + Disc Handling + + Generate a TOC if the disc is missing one @@ -58,120 +68,152 @@ - - - - - - - - - - - - - - - - - - - - - - - + - Load Image - + + Load Image + + - Save Track(s) - + + Save Track(s) + + - Toggle Play/Pause - + + Toggle Play/Pause + + - Stop Playback - + + Stop Playback + + - Eject Disc - + + Eject Disc + + + + + + Next Disc + + + + + + Previous Disc + + - Next Track - + + Next Track + + - Previous Track - + + Previous Track + + + + + + Shuffle Tracks + + - Next Index - + + Next Index + + - Previous Index - + + Previous Index + + - Fast-Forward - + + Fast-Forward + + - Rewind - + + Rewind + + - Volume Up - + + Volume Up + + - Volume Down - + + Volume Down + + - Toggle Mute - + + Toggle Mute + + - Toggle De-Emphasis - - + + Toggle De-Emphasis + + + diff --git a/RedBookPlayer.GUI/themes/Default/view.xaml b/RedBookPlayer.GUI/themes/Default/view.xaml index 83d1b6f..e3d9e4d 100644 --- a/RedBookPlayer.GUI/themes/Default/view.xaml +++ b/RedBookPlayer.GUI/themes/Default/view.xaml @@ -8,19 +8,19 @@ - + - - - + + + - - - - - Rewind - Fast Forward + + + + + Rewind + Fast Forward @@ -81,12 +81,12 @@ - - @@ -103,7 +103,8 @@ 4CH HIDDEN HIDDEN - + + \ No newline at end of file diff --git a/RedBookPlayer.Models/Hardware/DeEmphasisFilter.cs b/RedBookPlayer.Models/Audio/DeEmphasisFilter.cs similarity index 97% rename from RedBookPlayer.Models/Hardware/DeEmphasisFilter.cs rename to RedBookPlayer.Models/Audio/DeEmphasisFilter.cs index 5b74b1d..3ee46f7 100644 --- a/RedBookPlayer.Models/Hardware/DeEmphasisFilter.cs +++ b/RedBookPlayer.Models/Audio/DeEmphasisFilter.cs @@ -1,7 +1,7 @@ using System; using NWaves.Filters.BiQuad; -namespace RedBookPlayer.Models.Hardware +namespace RedBookPlayer.Models.Audio { /// /// Filter for applying de-emphasis to audio diff --git a/RedBookPlayer.Models/Hardware/FilterStage.cs b/RedBookPlayer.Models/Audio/FilterStage.cs similarity index 97% rename from RedBookPlayer.Models/Hardware/FilterStage.cs rename to RedBookPlayer.Models/Audio/FilterStage.cs index b9189ec..f53a8b5 100644 --- a/RedBookPlayer.Models/Hardware/FilterStage.cs +++ b/RedBookPlayer.Models/Audio/FilterStage.cs @@ -1,7 +1,7 @@ using NWaves.Audio; using NWaves.Filters.BiQuad; -namespace RedBookPlayer.Models.Hardware +namespace RedBookPlayer.Models.Audio { /// /// Output stage that represents all filters on the audio diff --git a/RedBookPlayer.Models/Audio/IAudioBackend.cs b/RedBookPlayer.Models/Audio/IAudioBackend.cs new file mode 100644 index 0000000..b8417dc --- /dev/null +++ b/RedBookPlayer.Models/Audio/IAudioBackend.cs @@ -0,0 +1,30 @@ +namespace RedBookPlayer.Models.Audio +{ + public interface IAudioBackend + { + /// + /// Pauses the audio playback + /// + void Pause(); + + /// + /// Starts the playback. + /// + void Play(); + + /// + /// Stops the audio playback + /// + void Stop(); + + /// + /// Get the current playback state + /// + PlayerState GetPlayerState(); + + /// + /// Set the new volume value + /// + void SetVolume(float volume); + } +} \ No newline at end of file diff --git a/RedBookPlayer.Models/Audio/Linux/AudioBackend.cs b/RedBookPlayer.Models/Audio/Linux/AudioBackend.cs new file mode 100644 index 0000000..9ee5c12 --- /dev/null +++ b/RedBookPlayer.Models/Audio/Linux/AudioBackend.cs @@ -0,0 +1,52 @@ +using CSCore.SoundOut; + +namespace RedBookPlayer.Models.Audio.Linux +{ + public class AudioBackend : IAudioBackend + { + /// + /// Sound output instance + /// + private readonly ALSoundOut _soundOut; + + public AudioBackend() { } + + public AudioBackend(PlayerSource source) + { + _soundOut = new ALSoundOut(100); + _soundOut.Initialize(source); + } + + #region IAudioBackend Implementation + + /// + public void Pause() => _soundOut.Pause(); + + /// + public void Play() => _soundOut.Play(); + + /// + public void Stop() => _soundOut.Stop(); + + /// + public PlayerState GetPlayerState() + { + return (_soundOut?.PlaybackState) switch + { + PlaybackState.Paused => PlayerState.Paused, + PlaybackState.Playing => PlayerState.Playing, + PlaybackState.Stopped => PlayerState.Stopped, + _ => PlayerState.NoDisc, + }; + } + + /// + public void SetVolume(float volume) + { + if(_soundOut != null) + _soundOut.Volume = volume; + } + + #endregion + } +} \ No newline at end of file diff --git a/RedBookPlayer.Models/Hardware/PlayerSource.cs b/RedBookPlayer.Models/Audio/PlayerSource.cs similarity index 96% rename from RedBookPlayer.Models/Hardware/PlayerSource.cs rename to RedBookPlayer.Models/Audio/PlayerSource.cs index 203ff4f..41c8360 100644 --- a/RedBookPlayer.Models/Hardware/PlayerSource.cs +++ b/RedBookPlayer.Models/Audio/PlayerSource.cs @@ -2,7 +2,7 @@ using System; using CSCore; using WaveFormat = CSCore.WaveFormat; -namespace RedBookPlayer.Models.Hardware +namespace RedBookPlayer.Models.Audio { public class PlayerSource : IWaveSource { diff --git a/RedBookPlayer.Models/Audio/SoundOutput.cs b/RedBookPlayer.Models/Audio/SoundOutput.cs new file mode 100644 index 0000000..4c526ae --- /dev/null +++ b/RedBookPlayer.Models/Audio/SoundOutput.cs @@ -0,0 +1,177 @@ +using System.Runtime.InteropServices; +using ReactiveUI; + +namespace RedBookPlayer.Models.Audio +{ + public class SoundOutput : ReactiveObject + { + #region Public Fields + + /// + /// Indicate if the output is ready to be used + /// + public bool Initialized + { + get => _initialized; + private set => this.RaiseAndSetIfChanged(ref _initialized, value); + } + + /// + /// Indicates the current player state + /// + public PlayerState PlayerState + { + get => _playerState; + private set => this.RaiseAndSetIfChanged(ref _playerState, value); + } + + /// + /// Current playback volume + /// + public int Volume + { + get => _volume; + private set + { + int tempVolume = value; + if(value > 100) + tempVolume = 100; + else if(value < 0) + tempVolume = 0; + + this.RaiseAndSetIfChanged(ref _volume, tempVolume); + } + } + + private bool _initialized; + private PlayerState _playerState; + private int _volume; + + #endregion + + #region Private State Variables + + /// + /// Data provider for sound output + /// + private PlayerSource _source; + + /// + /// Sound output instance + /// + private IAudioBackend _soundOut; + + #endregion + + /// + /// Constructor + /// + /// ReadFunction to use during decoding + /// Default volume between 0 and 100 to use when starting playback + public SoundOutput(PlayerSource.ReadFunction read, int defaultVolume = 100) + { + Volume = defaultVolume; + SetupAudio(read); + } + + /// + /// Initialize the output with a given image + /// + /// True if playback should begin immediately, false otherwise + public void Init(bool autoPlay) + { + // Reset initialization + Initialized = false; + + // Initialize playback, if necessary + if(autoPlay) + _soundOut.Play(); + + // Mark the output as ready + Initialized = true; + PlayerState = PlayerState.Stopped; + + // Begin loading data + _source.Start(); + } + + /// + /// Reset the current internal state + /// + public void Reset() + { + _soundOut.Stop(); + Initialized = false; + PlayerState = PlayerState.NoDisc; + } + + #region Playback + + /// + /// Start audio playback + /// + public void Play() + { + if(_soundOut.GetPlayerState() != PlayerState.Playing) + _soundOut.Play(); + + PlayerState = PlayerState.Playing; + } + + /// + /// Pause audio playback + /// + public void Pause() + { + if(_soundOut.GetPlayerState() != PlayerState.Paused) + _soundOut.Pause(); + + PlayerState = PlayerState.Paused; + } + + /// + /// Stop audio playback + /// + public void Stop() + { + if(_soundOut.GetPlayerState() != PlayerState.Stopped) + _soundOut.Stop(); + + PlayerState = PlayerState.Stopped; + } + + /// + /// Eject the currently loaded disc + /// + public void Eject() => Reset(); + + #endregion + + #region Helpers + + /// + /// Set the value for the volume + /// + /// New volume value + public void SetVolume(int volume) + { + Volume = volume; + _soundOut?.SetVolume((float)Volume / 100); + } + + /// + /// Sets or resets the audio playback objects + /// + /// ReadFunction to use during decoding + private void SetupAudio(PlayerSource.ReadFunction read) + { + _source = new PlayerSource(read); + if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + _soundOut = new Linux.AudioBackend(_source); + else if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + _soundOut = new Windows.AudioBackend(_source); + } + + #endregion + } +} \ No newline at end of file diff --git a/RedBookPlayer.Models/Audio/Windows/AudioBackend.cs b/RedBookPlayer.Models/Audio/Windows/AudioBackend.cs new file mode 100644 index 0000000..3f01aea --- /dev/null +++ b/RedBookPlayer.Models/Audio/Windows/AudioBackend.cs @@ -0,0 +1,52 @@ +using CSCore.SoundOut; + +namespace RedBookPlayer.Models.Audio.Windows +{ + public class AudioBackend : IAudioBackend + { + /// + /// Sound output instance + /// + private readonly ALSoundOut _soundOut; + + public AudioBackend() { } + + public AudioBackend(PlayerSource source) + { + _soundOut = new ALSoundOut(100); + _soundOut.Initialize(source); + } + + #region IAudioBackend Implementation + + /// + public void Pause() => _soundOut.Pause(); + + /// + public void Play() => _soundOut.Play(); + + /// + public void Stop() => _soundOut.Stop(); + + /// + public PlayerState GetPlayerState() + { + return (_soundOut?.PlaybackState) switch + { + PlaybackState.Paused => PlayerState.Paused, + PlaybackState.Playing => PlayerState.Playing, + PlaybackState.Stopped => PlayerState.Stopped, + _ => PlayerState.NoDisc, + }; + } + + /// + public void SetVolume(float volume) + { + if(_soundOut != null) + _soundOut.Volume = volume; + } + + #endregion + } +} \ No newline at end of file diff --git a/RedBookPlayer.Models/Discs/CompactDisc.cs b/RedBookPlayer.Models/Discs/CompactDisc.cs index 9759b6f..7c97058 100644 --- a/RedBookPlayer.Models/Discs/CompactDisc.cs +++ b/RedBookPlayer.Models/Discs/CompactDisc.cs @@ -27,85 +27,25 @@ namespace RedBookPlayer.Models.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) && DataPlayback == DataPlayback.Skip) + // Invalid value means we can't do anything + if(value > _image.Tracks.Max(t => t.TrackSequence)) + return; + else if(value < _image.Tracks.Min(t => t.TrackSequence)) return; - // Cache the value and the current track number - int cachedValue = value; - int cachedTrackNumber; - - // Check if we're incrementing or decrementing the track - bool increment = cachedValue >= _currentTrackNumber; - - do - { - // 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++; - } - - // 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 = GetTrack(cachedTrackNumber); - if(track == null) - return; - - // Set track flags from subchannel data, if possible - SetTrackFlags(track); - - // If the track is playable, just return - if((TrackType == TrackType.Audio || DataPlayback != DataPlayback.Skip) - && (SessionHandling == SessionHandling.AllSessions || track.TrackSession == 1)) - { - break; - } - - // If we're not playing the track, skip - if(increment) - cachedValue++; - else - cachedValue--; - } - 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) + // Cache the current track for easy access + Track track = GetTrack(value); + if(track == null) return; - TotalIndexes = cachedTrack.Indexes.Keys.Max(); - CurrentTrackIndex = cachedTrack.Indexes.Keys.Min(); - CurrentTrackSession = cachedTrack.TrackSession; + // Set all track flags and values + SetTrackFlags(track); + TotalIndexes = track.Indexes.Keys.Max(); + CurrentTrackIndex = track.Indexes.Keys.Min(); + CurrentTrackSession = track.TrackSession; + + // Mark the track as changed + this.RaiseAndSetIfChanged(ref _currentTrackNumber, value); } } @@ -124,14 +64,13 @@ namespace RedBookPlayer.Models.Discs if(track == null) return; - // Ensure that the value is valid, wrapping around if necessary - ushort fixedValue = value; + // Invalid value means we can't do anything if(value > track.Indexes.Keys.Max()) - fixedValue = track.Indexes.Keys.Min(); + return; else if(value < track.Indexes.Keys.Min()) - fixedValue = track.Indexes.Keys.Max(); + return; - this.RaiseAndSetIfChanged(ref _currentTrackIndex, fixedValue); + this.RaiseAndSetIfChanged(ref _currentTrackIndex, value); // Set new index-specific data SectionStartSector = (ulong)track.Indexes[CurrentTrackIndex]; @@ -156,19 +95,18 @@ namespace RedBookPlayer.Models.Discs if(_image == null) return; - // If the sector is over the end of the image, then loop - ulong tempSector = value; - if(tempSector > _image.Info.Sectors) - tempSector = 0; - else if(tempSector < 0) - tempSector = _image.Info.Sectors - 1; + // Invalid value means we can't do anything + if(value > _image.Info.Sectors) + return; + else if(value < 0) + return; // Cache the current track for easy access Track track = GetTrack(CurrentTrackNumber); if(track == null) return; - this.RaiseAndSetIfChanged(ref _currentSector, tempSector); + this.RaiseAndSetIfChanged(ref _currentSector, value); // If the current sector is outside of the last known track, seek to the right one if(CurrentSector < track.TrackStartSector || CurrentSector > track.TrackEndSector) @@ -194,6 +132,11 @@ namespace RedBookPlayer.Models.Discs /// public override int BytesPerSector => GetTrack(CurrentTrackNumber)?.TrackRawBytesPerSector ?? 0; + /// + /// Readonly list of all tracks in the image + /// + public List Tracks => _image?.Tracks; + /// /// Represents the 4CH flag /// @@ -230,21 +173,6 @@ namespace RedBookPlayer.Models.Discs private set => this.RaiseAndSetIfChanged(ref _trackHasEmphasis, value); } - /// - /// Indicate how data tracks should be handled - /// - public DataPlayback DataPlayback { get; set; } = DataPlayback.Skip; - - /// - /// Indicate if hidden tracks should be loaded - /// - public bool LoadHiddenTracks { get; set; } = false; - - /// - /// Indicates how tracks on different session should be handled - /// - public SessionHandling SessionHandling { get; set; } = SessionHandling.AllSessions; - private bool _quadChannel; private bool _isDataTrack; private bool _copyAllowed; @@ -290,30 +218,26 @@ namespace RedBookPlayer.Models.Discs /// Constructor /// /// Set of options for a new disc - public CompactDisc(OpticalDiscOptions options) - { - DataPlayback = options.DataPlayback; - _generateMissingToc = options.GenerateMissingToc; - LoadHiddenTracks = options.LoadHiddenTracks; - SessionHandling = options.SessionHandling; - } + public CompactDisc(OpticalDiscOptions options) => _generateMissingToc = options.GenerateMissingToc; /// - public override void Init(IOpticalMediaImage image, bool autoPlay) + public override void Init(string path, IOpticalMediaImage image, bool autoPlay) { // If the image is null, we can't do anything if(image == null) return; // Set the current disc image + ImagePath = path; _image = image; // Attempt to load the TOC if(!LoadTOC()) return; - // Load the first track - LoadFirstTrack(); + // Load the first track by default + CurrentTrackNumber = 1; + LoadTrack(CurrentTrackNumber); // Reset total indexes if not in autoplay if(!autoPlay) @@ -329,134 +253,32 @@ namespace RedBookPlayer.Models.Discs Initialized = true; } - #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; - - // 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)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)track.Indexes[++CurrentTrackIndex]; - } - - return false; - } - - /// - public override bool PreviousIndex(bool changeTrack) - { - if(_image == null) - return false; - - // 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)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)track.Indexes[--CurrentTrackIndex]; - } - - return false; - } - - #endregion - #region Helpers /// - public override void ExtractTrackToWav(uint trackNumber, string outputDirectory) + public override void ExtractTrackToWav(uint trackNumber, string outputDirectory) => ExtractTrackToWav(trackNumber, outputDirectory, DataPlayback.Skip); + + /// + /// Extract a track to WAV + /// + /// Track number to extract + /// Output path to write data to + /// DataPlayback value indicating how to handle data tracks + public void ExtractTrackToWav(uint trackNumber, string outputDirectory, DataPlayback dataPlayback) { if(_image == null) return; // Get the track with that value, if possible Track track = _image.Tracks.FirstOrDefault(t => t.TrackSequence == trackNumber); - - // If the track isn't valid, we can't do anything - if(track == null || !(DataPlayback != DataPlayback.Skip || track.TrackType == TrackType.Audio)) + if(track == null) return; // Get the number of sectors to read uint length = (uint)(track.TrackEndSector - track.TrackStartSector); // Read in the track data to a buffer - byte[] buffer = ReadSectors(track.TrackStartSector, length); + byte[] buffer = ReadSectors(track.TrackStartSector, length, dataPlayback); // Build the WAV output string filename = Path.Combine(outputDirectory, $"Track {trackNumber.ToString().PadLeft(2, '0')}.wav"); @@ -470,15 +292,20 @@ namespace RedBookPlayer.Models.Discs } } - /// - public override void ExtractAllTracksToWav(string outputDirectory) + /// + /// Get the track with the given sequence value, if possible + /// + /// Track number to retrieve + /// Track object for the requested sequence, null on error + public Track GetTrack(int trackNumber) { - if(_image == null) - return; - - foreach(Track track in _image.Tracks) + try { - ExtractTrackToWav(track.TrackSequence, outputDirectory); + return _image.Tracks.FirstOrDefault(t => t.TrackSequence == trackNumber); + } + catch + { + return null; } } @@ -497,31 +324,65 @@ namespace RedBookPlayer.Models.Discs // 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); + + // Load and debug output + uint sectorCount = (uint)(track.TrackEndSector - track.TrackStartSector); + byte[] trackData = ReadSectors(sectorCount); + Console.WriteLine($"DEBUG: Track {trackNumber} - {sectorCount} sectors / {trackData.Length} bytes"); } /// - public override void LoadFirstTrack() + public override void LoadIndex(ushort index) { - CurrentTrackNumber = 1; - LoadTrack(CurrentTrackNumber); + if(_image == null) + return; + + // Cache the current track for easy access + Track track = GetTrack(CurrentTrackNumber); + if(track == null) + return; + + // If the index is invalid, just return + if(index < track.Indexes.Keys.Min() || index > track.Indexes.Keys.Max()) + return; + + // Select the first index that has a sector offset greater than or equal to 0 + CurrentSector = (ulong)track.Indexes[index]; } /// - public override byte[] ReadSectors(uint sectorsToRead) => ReadSectors(CurrentSector, sectorsToRead); + public override byte[] ReadSectors(uint sectorsToRead) => ReadSectors(CurrentSector, sectorsToRead, DataPlayback.Skip); + + /// + /// Read sector data from the base image starting from the specified sector + /// + /// Current number of sectors to read + /// DataPlayback value indicating how to handle data tracks + /// Byte array representing the read sectors, if possible + public byte[] ReadSectors(uint sectorsToRead, DataPlayback dataPlayback) => ReadSectors(CurrentSector, sectorsToRead, dataPlayback); + + /// + /// Read subchannel data from the base image starting from the specified sector + /// + /// Current number of sectors to read + /// Byte array representing the read subchannels, if possible + public byte[] ReadSubchannels(uint sectorsToRead) => ReadSubchannels(CurrentSector, sectorsToRead); /// /// Read sector data from the base image starting from the specified sector /// /// Sector to start at for reading /// Current number of sectors to read + /// DataPlayback value indicating how to handle data tracks /// Byte array representing the read sectors, if possible - private byte[] ReadSectors(ulong startSector, uint sectorsToRead) + /// Should be a multiple of 96 bytes + private byte[] ReadSectors(ulong startSector, uint sectorsToRead, DataPlayback dataPlayback) { - if(TrackType == TrackType.Audio || DataPlayback == DataPlayback.Play) + if(TrackType == TrackType.Audio || dataPlayback == DataPlayback.Play) { return _image.ReadSectors(startSector, sectorsToRead); } - else if(DataPlayback == DataPlayback.Blank) + else if(dataPlayback == DataPlayback.Blank) { byte[] sectors = _image.ReadSectors(startSector, sectorsToRead); Array.Clear(sectors, 0, sectors.Length); @@ -533,6 +394,16 @@ namespace RedBookPlayer.Models.Discs } } + /// + /// Read subchannel data from the base image starting from the specified sector + /// + /// Sector to start at for reading + /// Current number of sectors to read + /// Byte array representing the read subchannels, if possible + /// Should be a multiple of 96 bytes + private byte[] ReadSubchannels(ulong startSector, uint sectorsToRead) + => _image.ReadSectorsTag(startSector, sectorsToRead, SectorTagType.CdSectorSubchannel); + /// public override void SetTotalIndexes() { @@ -542,23 +413,6 @@ namespace RedBookPlayer.Models.Discs TotalIndexes = GetTrack(CurrentTrackNumber)?.Indexes.Keys.Max() ?? 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; - } - } - /// /// Load TOC for the current disc image /// diff --git a/RedBookPlayer.Models/Discs/OpticalDiscBase.cs b/RedBookPlayer.Models/Discs/OpticalDiscBase.cs index 0a31b9b..5dcd612 100644 --- a/RedBookPlayer.Models/Discs/OpticalDiscBase.cs +++ b/RedBookPlayer.Models/Discs/OpticalDiscBase.cs @@ -8,6 +8,11 @@ namespace RedBookPlayer.Models.Discs { #region Public Fields + /// + /// Path to the disc image + /// + public string ImagePath { get; protected set; } + /// /// Indicate if the disc is ready to be used /// @@ -93,37 +98,10 @@ namespace RedBookPlayer.Models.Discs /// /// Initialize the disc with a given image /// + /// Path of the image /// Aaruformat image to load /// True if playback should begin immediately, false otherwise - public abstract void Init(IOpticalMediaImage image, bool autoPlay); - - #region Seeking - - /// - /// Try to move to the next track, wrapping around if necessary - /// - public abstract void NextTrack(); - - /// - /// Try to move to the previous track, wrapping around if necessary - /// - public abstract void PreviousTrack(); - - /// - /// Try to move to the next track index - /// - /// True if index changes can trigger a track change, false otherwise - /// True if the track was changed, false otherwise - public abstract bool NextIndex(bool changeTrack); - - /// - /// Try to move to the previous track index - /// - /// True if index changes can trigger a track change, false otherwise - /// True if the track was changed, false otherwise - public abstract bool PreviousIndex(bool changeTrack); - - #endregion + public abstract void Init(string path, IOpticalMediaImage image, bool autoPlay); #region Helpers @@ -134,12 +112,6 @@ namespace RedBookPlayer.Models.Discs /// Output path to write data to public abstract void ExtractTrackToWav(uint trackNumber, string outputDirectory); - /// - /// Extract all tracks to WAV - /// - /// Output path to write data to - public abstract void ExtractAllTracksToWav(string outputDirectory); - /// /// Load the desired track, if possible /// @@ -147,9 +119,10 @@ namespace RedBookPlayer.Models.Discs public abstract void LoadTrack(int track); /// - /// Load the first valid track in the image + /// Load the desired index, if possible /// - public abstract void LoadFirstTrack(); + /// Index number to load + public abstract void LoadIndex(ushort index); /// /// Read sector data from the base image starting from the current sector diff --git a/RedBookPlayer.Models/Discs/OpticalDiscOptions.cs b/RedBookPlayer.Models/Discs/OpticalDiscOptions.cs index a636c39..d814919 100644 --- a/RedBookPlayer.Models/Discs/OpticalDiscOptions.cs +++ b/RedBookPlayer.Models/Discs/OpticalDiscOptions.cs @@ -4,26 +4,11 @@ namespace RedBookPlayer.Models.Discs { #region CompactDisc - /// - /// Indicate how data tracks should be handled - /// - public DataPlayback DataPlayback { get; set; } = DataPlayback.Skip; - /// /// Indicate if a TOC should be generated if missing /// public bool GenerateMissingToc { get; set; } = false; - /// - /// Indicate if hidden tracks should be loaded - /// - public bool LoadHiddenTracks { get; set; } = false; - - /// - /// Indicates how tracks on different session should be handled - /// - public SessionHandling SessionHandling { get; set; } = SessionHandling.AllSessions; - #endregion } } \ No newline at end of file diff --git a/RedBookPlayer.Models/Enums.cs b/RedBookPlayer.Models/Enums.cs index 4066c37..5619e11 100644 --- a/RedBookPlayer.Models/Enums.cs +++ b/RedBookPlayer.Models/Enums.cs @@ -21,6 +21,23 @@ namespace RedBookPlayer.Models Play = 2, } + /// + /// Determine how to handle multiple discs + /// + /// Used with both repeat and shuffle + public enum DiscHandling + { + /// + /// Only deal with tracks on the current disc + /// + SingleDisc = 0, + + /// + /// Deal with tracks on all loaded discs + /// + MultiDisc = 1, + } + /// /// Current player state /// diff --git a/RedBookPlayer.Models/Factories/OpticalDiscFactory.cs b/RedBookPlayer.Models/Factories/OpticalDiscFactory.cs index 5023a6b..fc0e0f0 100644 --- a/RedBookPlayer.Models/Factories/OpticalDiscFactory.cs +++ b/RedBookPlayer.Models/Factories/OpticalDiscFactory.cs @@ -32,7 +32,7 @@ namespace RedBookPlayer.Models.Factories image.Open(filter); // Generate and instantiate the disc - return GenerateFromImage(image, options, autoPlay); + return GenerateFromImage(path, image, options, autoPlay); } catch { @@ -44,11 +44,12 @@ namespace RedBookPlayer.Models.Factories /// /// Generate an OpticalDisc from an input IOpticalMediaImage /// + /// Path of the image /// IOpticalMediaImage to create from /// Options to pass to the optical disc factory /// True if the image should be playable immediately, false otherwise /// Instantiated OpticalDisc, if possible - public static OpticalDiscBase GenerateFromImage(IOpticalMediaImage image, OpticalDiscOptions options, bool autoPlay) + public static OpticalDiscBase GenerateFromImage(string path, IOpticalMediaImage image, OpticalDiscOptions options, bool autoPlay) { // If the image is not usable, we don't do anything if(!IsUsableImage(image)) @@ -74,7 +75,7 @@ namespace RedBookPlayer.Models.Factories return opticalDisc; // Instantiate the disc and return - opticalDisc.Init(image, autoPlay); + opticalDisc.Init(path, image, autoPlay); return opticalDisc; } diff --git a/RedBookPlayer.Models/Hardware/Player.cs b/RedBookPlayer.Models/Hardware/Player.cs index 8651e95..639cc6c 100644 --- a/RedBookPlayer.Models/Hardware/Player.cs +++ b/RedBookPlayer.Models/Hardware/Player.cs @@ -1,6 +1,13 @@ +using System; +using System.Collections.Generic; using System.ComponentModel; +using System.Linq; +using System.Threading.Tasks; using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Structs; +using Avalonia.Threading; using ReactiveUI; +using RedBookPlayer.Models.Audio; using RedBookPlayer.Models.Discs; using RedBookPlayer.Models.Factories; @@ -17,10 +24,113 @@ namespace RedBookPlayer.Models.Hardware private set => this.RaiseAndSetIfChanged(ref _initialized, value); } + #region Playback Passthrough + + /// + /// Currently selected disc + /// + public int CurrentDisc + { + get => _currentDisc; + private set + { + int temp = value; + if(temp < 0) + temp = _numberOfDiscs - 1; + else if(temp >= _numberOfDiscs) + temp = 0; + + this.RaiseAndSetIfChanged(ref _currentDisc, temp); + } + } + + /// + /// Indicates how to deal with multiple discs + /// + public DiscHandling DiscHandling + { + get => _discHandling; + private set => this.RaiseAndSetIfChanged(ref _discHandling, value); + } + + /// + /// Indicates how to handle playback of data tracks + /// + public DataPlayback DataPlayback + { + get => _dataPlayback; + private set => this.RaiseAndSetIfChanged(ref _dataPlayback, value); + } + + /// + /// Indicate if hidden tracks should be loaded + /// + public bool LoadHiddenTracks + { + get => _loadHiddenTracks; + private set => this.RaiseAndSetIfChanged(ref _loadHiddenTracks, value); + } + + /// + /// Indicates the repeat mode + /// + public RepeatMode RepeatMode + { + get => _repeatMode; + private set => this.RaiseAndSetIfChanged(ref _repeatMode, value); + } + + /// + /// Indicates how tracks on different session should be handled + /// + public SessionHandling SessionHandling + { + get => _sessionHandling; + private set => this.RaiseAndSetIfChanged(ref _sessionHandling, value); + } + + /// + /// Indicates if de-emphasis should be applied + /// + public bool ApplyDeEmphasis + { + get => _applyDeEmphasis; + private set => this.RaiseAndSetIfChanged(ref _applyDeEmphasis, value); + } + + /// + /// Should invoke playback mode changes + /// + private bool ShouldInvokePlaybackModes + { + get => _shouldInvokePlaybackModes; + set => this.RaiseAndSetIfChanged(ref _shouldInvokePlaybackModes, value); + } + private bool _initialized; + private int _numberOfDiscs; + private int _currentDisc; + private DiscHandling _discHandling; + private bool _loadHiddenTracks; + private DataPlayback _dataPlayback; + private RepeatMode _repeatMode; + private SessionHandling _sessionHandling; + private bool _applyDeEmphasis; + private bool _shouldInvokePlaybackModes; + + #endregion #region OpticalDisc Passthrough + /// + /// Path to the disc image + /// + public string ImagePath + { + get => _imagePath; + private set => this.RaiseAndSetIfChanged(ref _imagePath, value); + } + /// /// Current track number /// @@ -114,28 +224,29 @@ namespace RedBookPlayer.Models.Hardware /// /// Represents the total tracks on the disc /// - public int TotalTracks => _opticalDisc?.TotalTracks ?? 0; + public int TotalTracks => _opticalDiscs[CurrentDisc]?.TotalTracks ?? 0; /// /// Represents the total indices on the disc /// - public int TotalIndexes => _opticalDisc?.TotalIndexes ?? 0; + public int TotalIndexes => _opticalDiscs[CurrentDisc]?.TotalIndexes ?? 0; /// /// Total sectors in the image /// - public ulong TotalSectors => _opticalDisc?.TotalSectors ?? 0; + public ulong TotalSectors => _opticalDiscs[CurrentDisc]?.TotalSectors ?? 0; /// /// Represents the time adjustment offset for the disc /// - public ulong TimeOffset => _opticalDisc?.TimeOffset ?? 0; + public ulong TimeOffset => _opticalDiscs[CurrentDisc]?.TimeOffset ?? 0; /// /// Represents the total playing time for the disc /// - public ulong TotalTime => _opticalDisc?.TotalTime ?? 0; + public ulong TotalTime => _opticalDiscs[CurrentDisc]?.TotalTime ?? 0; + private string _imagePath; private int _currentTrackNumber; private ushort _currentTrackIndex; private ushort _currentTrackSession; @@ -161,33 +272,6 @@ namespace RedBookPlayer.Models.Hardware private set => this.RaiseAndSetIfChanged(ref _playerState, value); } - /// - /// Indicates how to handle playback of data tracks - /// - public DataPlayback DataPlayback - { - get => _dataPlayback; - private set => this.RaiseAndSetIfChanged(ref _dataPlayback, value); - } - - /// - /// Indicates the repeat mode - /// - public RepeatMode RepeatMode - { - get => _repeatMode; - private set => this.RaiseAndSetIfChanged(ref _repeatMode, value); - } - - /// - /// Indicates if de-emphasis should be applied - /// - public bool ApplyDeEmphasis - { - get => _applyDeEmphasis; - private set => this.RaiseAndSetIfChanged(ref _applyDeEmphasis, value); - } - /// /// Current playback volume /// @@ -198,9 +282,6 @@ namespace RedBookPlayer.Models.Hardware } private PlayerState _playerState; - private DataPlayback _dataPlayback; - private RepeatMode _repeatMode; - private bool _applyDeEmphasis; private int _volume; #endregion @@ -213,55 +294,119 @@ namespace RedBookPlayer.Models.Hardware private readonly SoundOutput _soundOutput; /// - /// OpticalDisc object + /// OpticalDisc objects /// - private OpticalDiscBase _opticalDisc; + private readonly OpticalDiscBase[] _opticalDiscs; + + /// + /// List of available tracks organized by disc + /// + private Dictionary> _availableTrackList; + + /// + /// Current track playback order + /// + private List> _trackPlaybackOrder; + + /// + /// Current track in playback order list + /// + private int _currentTrackInOrder; /// /// Last volume for mute toggling /// private int? _lastVolume = null; + /// + /// Filtering stage for audio output + /// + private readonly FilterStage _filterStage; + + /// + /// Current position in the sector for reading + /// + private int _currentSectorReadPosition = 0; + + /// + /// Lock object for reading track data + /// + private readonly object _readingImage = new object(); + #endregion /// /// Constructor /// + /// Number of discs to allow loading /// Default volume between 0 and 100 to use when starting playback - public Player(int defaultVolume) + public Player(int numberOfDiscs, int defaultVolume) { Initialized = false; - _soundOutput = new SoundOutput(defaultVolume); - _soundOutput.SetDeEmphasis(false); + + if(numberOfDiscs <= 0) + numberOfDiscs = 1; + + _numberOfDiscs = numberOfDiscs; + _opticalDiscs = new OpticalDiscBase[numberOfDiscs]; + _currentDisc = 0; + + _filterStage = new FilterStage(); + _soundOutput = new SoundOutput(ProviderRead, defaultVolume); + _soundOutput.PropertyChanged += SoundOutputStateChanged; + + _availableTrackList = new Dictionary>(); + for(int i = 0; i < _numberOfDiscs; i++) + { + _availableTrackList.Add(i, new List()); + } + + _trackPlaybackOrder = new List>(); + _currentTrackInOrder = 0; + + PropertyChanged += HandlePlaybackModes; + + // Force a refresh of the state information + SoundOutputStateChanged(this, null); } /// /// Initializes player from a given image path /// /// Path to the disc image - /// Options to pass to the optical disc factory - /// RepeatMode for sound output + /// Options to pass to the player + /// Options to pass to the optical disc factory /// True if playback should begin immediately, false otherwise - public void Init(string path, OpticalDiscOptions options, RepeatMode repeatMode, bool autoPlay) + public void Init(string path, PlayerOptions playerOptions, OpticalDiscOptions opticalDiscOptions, bool autoPlay) { // Reset initialization Initialized = false; + // Set player options + DataPlayback = playerOptions.DataPlayback; + DiscHandling = playerOptions.DiscHandling; + LoadHiddenTracks = playerOptions.LoadHiddenTracks; + RepeatMode = playerOptions.RepeatMode; + SessionHandling = playerOptions.SessionHandling; + // Initalize the disc - _opticalDisc = OpticalDiscFactory.GenerateFromPath(path, options, autoPlay); - if(_opticalDisc == null || !_opticalDisc.Initialized) + _opticalDiscs[CurrentDisc] = OpticalDiscFactory.GenerateFromPath(path, opticalDiscOptions, autoPlay); + if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized) return; // Add event handling for the optical disc - _opticalDisc.PropertyChanged += OpticalDiscStateChanged; + _opticalDiscs[CurrentDisc].PropertyChanged += OpticalDiscStateChanged; + + // Setup de-emphasis filters + _filterStage.SetupFilters(); // Initialize the sound output - _soundOutput.Init(_opticalDisc, repeatMode, autoPlay); + _soundOutput.Init(autoPlay); if(_soundOutput == null || !_soundOutput.Initialized) return; - // Add event handling for the sound output - _soundOutput.PropertyChanged += SoundOutputStateChanged; + // Load in the track list for the current disc + LoadTrackList(); // Mark the player as ready Initialized = true; @@ -271,14 +416,86 @@ namespace RedBookPlayer.Models.Hardware SoundOutputStateChanged(this, null); } - #region Playback + /// + /// Load the track list into the track dictionary + /// + private void LoadTrackList() + { + OpticalDiscBase opticalDisc = _opticalDiscs[CurrentDisc]; + + // If the disc exists, add it to the dictionary + if(_opticalDiscs[CurrentDisc] != null) + { + if(opticalDisc is CompactDisc compactDisc) + _availableTrackList[CurrentDisc] = compactDisc.Tracks.Select(t => (int)t.TrackSequence).OrderBy(s => s).ToList(); + else + _availableTrackList[CurrentDisc] = Enumerable.Range(1, opticalDisc.TotalTracks).ToList(); + } + + // If the disc is null, then make sure it's removed + else + { + _availableTrackList[CurrentDisc] = new List(); + } + + // Repopulate the playback order + _trackPlaybackOrder = new List>(); + if(DiscHandling == DiscHandling.SingleDisc) + { + List availableTracks = _availableTrackList[CurrentDisc]; + if(availableTracks != null && availableTracks.Count > 0) + _trackPlaybackOrder.AddRange(availableTracks.Select(t => new KeyValuePair(CurrentDisc, t))); + } + else if(DiscHandling == DiscHandling.MultiDisc) + { + for(int i = 0; i < _numberOfDiscs; i++) + { + List availableTracks = _availableTrackList[i]; + if(availableTracks != null && availableTracks.Count > 0) + _trackPlaybackOrder.AddRange(availableTracks.Select(t => new KeyValuePair(i, t))); + } + } + + // Try to get back to the last loaded track + SetTrackOrderIndex(); + } + + /// + /// Set the current track order index, if possible + /// + private void SetTrackOrderIndex() + { + int currentFoundTrack = 0; + if(_trackPlaybackOrder == null || _trackPlaybackOrder.Count == 0) + { + _currentTrackInOrder = 0; + return; + } + else if(_trackPlaybackOrder.Any(kvp => kvp.Key == CurrentDisc)) + { + currentFoundTrack = _trackPlaybackOrder.FindIndex(kvp => kvp.Key == CurrentDisc && kvp.Value == CurrentTrackNumber); + if(currentFoundTrack == -1) + currentFoundTrack = _trackPlaybackOrder.FindIndex(kvp => kvp.Key == CurrentDisc && kvp.Value == _trackPlaybackOrder.Min(kvp => kvp.Value)); + } + else + { + int lowestDiscNumber = _trackPlaybackOrder.Min(kvp => kvp.Key); + currentFoundTrack = _trackPlaybackOrder.FindIndex(kvp => kvp.Key == lowestDiscNumber && kvp.Value == _trackPlaybackOrder.Min(kvp => kvp.Value)); + } + + CurrentDisc = _trackPlaybackOrder[currentFoundTrack].Key; + CurrentTrackNumber = _trackPlaybackOrder[currentFoundTrack].Value; + _currentTrackInOrder = currentFoundTrack; + } + + #region Playback (UI) /// /// Begin playback /// public void Play() { - if(_opticalDisc == null || !_opticalDisc.Initialized) + if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized) return; else if(_soundOutput == null) return; @@ -286,7 +503,7 @@ namespace RedBookPlayer.Models.Hardware return; _soundOutput.Play(); - _opticalDisc.SetTotalIndexes(); + _opticalDiscs[CurrentDisc].SetTotalIndexes(); PlayerState = PlayerState.Playing; } @@ -295,14 +512,14 @@ namespace RedBookPlayer.Models.Hardware /// public void Pause() { - if(_opticalDisc == null || !_opticalDisc.Initialized) + if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized) return; else if(_soundOutput == null) return; else if(_soundOutput.PlayerState != PlayerState.Playing) return; - _soundOutput?.Pause(); + _soundOutput.Pause(); PlayerState = PlayerState.Paused; } @@ -327,20 +544,48 @@ namespace RedBookPlayer.Models.Hardware } } + /// + /// Shuffle the current track order + /// + public void ShuffleTracks() + { + List> newPlaybackOrder = new List>(); + Random random = new Random(); + + while(_trackPlaybackOrder.Count > 0) + { + int next = random.Next(0, _trackPlaybackOrder.Count - 1); + newPlaybackOrder.Add(_trackPlaybackOrder[next]); + _trackPlaybackOrder.RemoveAt(next); + } + + _trackPlaybackOrder = newPlaybackOrder; + switch(PlayerState) + { + case PlayerState.Stopped: + _currentTrackInOrder = 0; + break; + case PlayerState.Paused: + case PlayerState.Playing: + SetTrackOrderIndex(); + break; + } + } + /// /// Stop current playback /// public void Stop() { - if(_opticalDisc == null || !_opticalDisc.Initialized) + if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized) return; else if(_soundOutput == null) return; else if(_soundOutput.PlayerState != PlayerState.Playing && _soundOutput.PlayerState != PlayerState.Paused) - return; + return; _soundOutput.Stop(); - _opticalDisc.LoadFirstTrack(); + SelectRelativeTrack(0); PlayerState = PlayerState.Stopped; } @@ -349,109 +594,75 @@ namespace RedBookPlayer.Models.Hardware /// public void Eject() { - if(_opticalDisc == null || !_opticalDisc.Initialized) + if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized) return; else if(_soundOutput == null) return; Stop(); - _soundOutput.Eject(); - _opticalDisc = null; - PlayerState = PlayerState.NoDisc; - Initialized = false; + _opticalDiscs[CurrentDisc] = null; + LoadTrackList(); + + // Force a refresh of the state information + OpticalDiscStateChanged(this, null); + SoundOutputStateChanged(this, null); + + // Only de-initialize the player if all discs are ejected + if(_opticalDiscs.All(d => d == null || !d.Initialized)) + { + _soundOutput.Eject(); + PlayerState = PlayerState.NoDisc; + Initialized = false; + } + else + { + PlayerState = PlayerState.Stopped; + } } + /// + /// Move to the next disc + /// + public void NextDisc() => SelectDisc(CurrentDisc + 1); + + /// + /// Move to the previous disc + /// + public void PreviousDisc() => SelectDisc(CurrentDisc - 1); + /// /// Move to the next playable track /// - public void NextTrack() - { - if(_opticalDisc == null || !_opticalDisc.Initialized) - return; - - PlayerState wasPlaying = PlayerState; - if(wasPlaying == PlayerState.Playing) - Pause(); - - _opticalDisc.NextTrack(); - if(_opticalDisc is CompactDisc compactDisc) - _soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis); - - if(wasPlaying == PlayerState.Playing) - Play(); - } + /// TODO: This should follow the track playback order + public void NextTrack() => SelectRelativeTrack(_currentTrackInOrder + 1); /// /// Move to the previous playable track /// - public void PreviousTrack() - { - if(_opticalDisc == null || !_opticalDisc.Initialized) - return; - - PlayerState wasPlaying = PlayerState; - if(wasPlaying == PlayerState.Playing) - Pause(); - - _opticalDisc.PreviousTrack(); - if(_opticalDisc is CompactDisc compactDisc) - _soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis); - - if(wasPlaying == PlayerState.Playing) - Play(); - } + /// TODO: This should follow the track playback order + public void PreviousTrack() => SelectRelativeTrack(_currentTrackInOrder - 1); /// /// 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; - - PlayerState wasPlaying = PlayerState; - if(wasPlaying == PlayerState.Playing) - Pause(); - - _opticalDisc.NextIndex(changeTrack); - if(_opticalDisc is CompactDisc compactDisc) - _soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis); - - if(wasPlaying == PlayerState.Playing) - Play(); - } + public void NextIndex(bool changeTrack) => SelectIndex((ushort)(CurrentTrackIndex + 1), changeTrack); /// /// 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; - - PlayerState wasPlaying = PlayerState; - if(wasPlaying == PlayerState.Playing) - Pause(); - - _opticalDisc.PreviousIndex(changeTrack); - if(_opticalDisc is CompactDisc compactDisc) - _soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis); - - if(wasPlaying == PlayerState.Playing) - Play(); - } + public void PreviousIndex(bool changeTrack) => SelectIndex((ushort)(CurrentTrackIndex - 1), changeTrack); /// /// Fast-forward playback by 75 sectors /// public void FastForward() { - if(_opticalDisc == null || !_opticalDisc.Initialized) + if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized) return; - _opticalDisc.SetCurrentSector(_opticalDisc.CurrentSector + 75); + _opticalDiscs[CurrentDisc].SetCurrentSector(_opticalDiscs[CurrentDisc].CurrentSector + 75); } /// @@ -459,10 +670,424 @@ namespace RedBookPlayer.Models.Hardware /// public void Rewind() { - if(_opticalDisc == null || !_opticalDisc.Initialized) + if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized) return; - _opticalDisc.SetCurrentSector(_opticalDisc.CurrentSector - 75); + _opticalDiscs[CurrentDisc].SetCurrentSector(_opticalDiscs[CurrentDisc].CurrentSector - 75); + } + + #endregion + + #region Playback (Internal) + + /// + /// Fill the current byte buffer with playable data + /// + /// Buffer to load data into + /// Offset in the buffer to load at + /// Number of bytes to load + /// Number of bytes read + public int ProviderRead(byte[] buffer, int offset, int count) + { + // If we have an unreadable amount + if(count <= 0) + { + Array.Clear(buffer, offset, count); + return count; + } + + // If we have an unreadable track, just return + if(_opticalDiscs[CurrentDisc].BytesPerSector <= 0) + { + Array.Clear(buffer, offset, count); + return count; + } + + // Determine how many sectors we can read + DetermineReadAmount(out ulong sectorsToRead, out ulong zeroSectorsAmount); + + // Get data to return + byte[] audioDataSegment = ReadData(count, sectorsToRead, zeroSectorsAmount); + if(audioDataSegment == null) + { + Array.Clear(buffer, offset, count); + return count; + } + + // Write out the audio data to the buffer + Array.Copy(audioDataSegment, 0, buffer, offset, count); + + // Set the read position in the sector for easier access + _currentSectorReadPosition += count; + if(_currentSectorReadPosition >= _opticalDiscs[CurrentDisc].BytesPerSector) + { + int previousTrack = CurrentTrackNumber; + ulong newSectorValue = _opticalDiscs[CurrentDisc].CurrentSector + (ulong)(_currentSectorReadPosition / _opticalDiscs[CurrentDisc].BytesPerSector); + if(newSectorValue >= _opticalDiscs[CurrentDisc].TotalSectors) + { + ShouldInvokePlaybackModes = true; + } + else if(RepeatMode == RepeatMode.Single && _opticalDiscs[CurrentDisc] is CompactDisc compactDisc) + { + ulong trackEndSector = compactDisc.GetTrack(CurrentTrackNumber).TrackEndSector; + if(newSectorValue > trackEndSector) + { + ShouldInvokePlaybackModes = true; + } + else + { + _opticalDiscs[CurrentDisc].SetCurrentSector(newSectorValue); + _currentSectorReadPosition %= _opticalDiscs[CurrentDisc].BytesPerSector; + } + } + else + { + _opticalDiscs[CurrentDisc].SetCurrentSector(newSectorValue); + _currentSectorReadPosition %= _opticalDiscs[CurrentDisc].BytesPerSector; + } + + // If we are supposed to change tracks, get the next one from the list + if(CurrentTrackNumber != previousTrack && !ShouldInvokePlaybackModes) + Dispatcher.UIThread.InvokeAsync(NextTrack).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + return count; + } + + /// + /// Select a disc by number + /// + /// Disc number to attempt to load + public void SelectDisc(int discNumber) + { + // If the disc didn't change, don't do anything + if(_currentDisc == discNumber) + return; + + PlayerState wasPlaying = PlayerState; + if(wasPlaying == PlayerState.Playing) + Stop(); + + if(discNumber >= _numberOfDiscs) + discNumber = 0; + else if(discNumber < 0) + discNumber = _numberOfDiscs - 1; + + _currentSectorReadPosition = 0; + + CurrentDisc = discNumber; + + // If we're in single disc mode, we need to reload the full track list + if(DiscHandling == DiscHandling.SingleDisc) + LoadTrackList(); + + if(_opticalDiscs[CurrentDisc] != null && _opticalDiscs[CurrentDisc].Initialized) + { + Initialized = true; + SelectTrack(1); + OpticalDiscStateChanged(this, null); + SoundOutputStateChanged(this, null); + + if(wasPlaying == PlayerState.Playing) + Play(); + } + else + { + PlayerState = PlayerState.NoDisc; + Initialized = false; + } + } + + /// + /// Select a disc by number + /// + /// Track index to attempt to load + /// True if index changes can trigger a track change, false otherwise + public void SelectIndex(ushort index, bool changeTrack) + { + if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized) + return; + + // If the index didn't change, don't do anything + if(_currentTrackIndex == index) + return; + + PlayerState wasPlaying = PlayerState; + if(wasPlaying == PlayerState.Playing) + Pause(); + + // CompactDisc needs special handling of track wraparound + if(_opticalDiscs[CurrentDisc] is CompactDisc compactDisc) + { + // Cache the current track for easy access + Track track = compactDisc.GetTrack(CurrentTrackNumber); + if(track == null) + return; + + // Check if we're incrementing or decrementing the track + bool increment = (short)index >= (short)CurrentTrackIndex; + + // If the index is greater than the highest index, change tracks if needed + if((short)index > (short)track.Indexes.Keys.Max()) + { + if(changeTrack) + NextTrack(); + } + + // If the index is less than the lowest index, change tracks if needed + else if((short)index < (short)track.Indexes.Keys.Min()) + { + if(changeTrack) + { + PreviousTrack(); + compactDisc.SetCurrentSector((ulong)compactDisc.GetTrack(CurrentTrackNumber).Indexes.Values.Max()); + } + } + + // If the next index has an invalid offset, change tracks if needed + else if(track.Indexes[index] < 0) + { + if(changeTrack) + { + if(increment) + { + NextTrack(); + } + else + { + PreviousTrack(); + compactDisc.SetCurrentSector((ulong)compactDisc.GetTrack(CurrentTrackNumber).Indexes.Values.Min()); + } + } + } + + // Otherwise, just move to the next index + else + { + compactDisc.SetCurrentSector((ulong)track.Indexes[index]); + } + } + else + { + // TODO: Fill in for non-CD media + } + + if(wasPlaying == PlayerState.Playing) + Play(); + } + + /// + /// Select a track by number within a disc + /// + /// Track number to attempt to load + /// True if the track was changed, false otherwise + public bool SelectTrack(int trackNumber) + { + if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized) + return false; + + PlayerState wasPlaying = PlayerState; + if(wasPlaying == PlayerState.Playing) + Pause(); + + // CompactDisc needs special handling of track wraparound + if(_opticalDiscs[CurrentDisc] is CompactDisc compactDisc) + { + // Cache the value and the current track number + int cachedValue = trackNumber; + int cachedTrackNumber; + + // If we have an invalid current track number, set it to the minimum + if(!compactDisc.Tracks.Any(t => t.TrackSequence == _currentTrackNumber)) + _currentTrackNumber = (int)compactDisc.Tracks.Min(t => t.TrackSequence); + + // Check if we're incrementing or decrementing the track + bool increment = cachedValue >= _currentTrackNumber; + + do + { + // If we're over the last track, wrap around + if(cachedValue > compactDisc.Tracks.Max(t => t.TrackSequence)) + { + cachedValue = (int)compactDisc.Tracks.Min(t => t.TrackSequence); + if(cachedValue == 0 && !LoadHiddenTracks) + cachedValue++; + } + + // If we're under the first track and we're not loading hidden tracks, wrap around + else if(cachedValue < 1 && !LoadHiddenTracks) + { + cachedValue = (int)compactDisc.Tracks.Max(t => t.TrackSequence); + } + + // If we're under the first valid track, wrap around + else if(cachedValue < compactDisc.Tracks.Min(t => t.TrackSequence)) + { + cachedValue = (int)compactDisc.Tracks.Max(t => t.TrackSequence); + } + + cachedTrackNumber = cachedValue; + + // Cache the current track for easy access + Track track = compactDisc.GetTrack(cachedTrackNumber); + if(track == null) + return false; + + // If the track is playable, just return + if((track.TrackType == TrackType.Audio || DataPlayback != DataPlayback.Skip) + && (SessionHandling == SessionHandling.AllSessions || track.TrackSession == 1)) + { + break; + } + + // If we're not playing the track, skip + if(increment) + cachedValue++; + else + cachedValue--; + } + while(cachedValue != _currentTrackNumber); + + // Load the now-valid value + compactDisc.LoadTrack(cachedTrackNumber); + ApplyDeEmphasis = compactDisc.TrackHasEmphasis; + } + else + { + if(trackNumber >= _opticalDiscs[CurrentDisc].TotalTracks) + trackNumber = 1; + else if(trackNumber < 1) + trackNumber = _opticalDiscs[CurrentDisc].TotalTracks - 1; + + _opticalDiscs[CurrentDisc].LoadTrack(trackNumber); + } + + SetTrackOrderIndex(); + if(wasPlaying == PlayerState.Playing) + Play(); + + return true; + } + + /// + /// Determine the number of real and zero sectors to read + /// + /// Number of sectors to read + /// Number of zeroed sectors to concatenate + private void DetermineReadAmount(out ulong sectorsToRead, out ulong zeroSectorsAmount) + { + // Always attempt to read one second of data + sectorsToRead = 75; + zeroSectorsAmount = 0; + + // Avoid overreads by padding with 0-byte data at the end + if(_opticalDiscs[CurrentDisc].CurrentSector + sectorsToRead > _opticalDiscs[CurrentDisc].TotalSectors) + { + ulong oldSectorsToRead = sectorsToRead; + sectorsToRead = _opticalDiscs[CurrentDisc].TotalSectors - _opticalDiscs[CurrentDisc].CurrentSector; + + int tempZeroSectorCount = (int)(oldSectorsToRead - sectorsToRead); + zeroSectorsAmount = (ulong)(tempZeroSectorCount < 0 ? 0 : tempZeroSectorCount); + } + } + + /// + /// Read the requested amount of data from an input + /// + /// Number of bytes to load + /// Number of sectors to read + /// Number of zeroed sectors to concatenate + /// The requested amount of data, if possible + private byte[] ReadData(int count, ulong sectorsToRead, ulong zeroSectorsAmount) + { + // If the amount of zeroes being asked for is the same as the sectors, return null + if(sectorsToRead == zeroSectorsAmount) + return null; + + // Create padding data for overreads + byte[] zeroSectors = new byte[(int)zeroSectorsAmount * _opticalDiscs[CurrentDisc].BytesPerSector]; + byte[] audioData; + + // Attempt to read the required number of sectors + var readSectorTask = Task.Run(() => + { + lock(_readingImage) + { + try + { + if(_opticalDiscs[CurrentDisc] is CompactDisc compactDisc) + { + //byte[] subchannelData = compactDisc.ReadSubchannels((uint)sectorsToRead); + return compactDisc.ReadSectors((uint)sectorsToRead, DataPlayback).Concat(zeroSectors).ToArray(); + } + else + { + return _opticalDiscs[CurrentDisc].ReadSectors((uint)sectorsToRead).Concat(zeroSectors).ToArray(); + } + } + catch { } + + return zeroSectors; + } + }); + + // Wait 100ms at longest for the read to occur + if(readSectorTask.Wait(TimeSpan.FromMilliseconds(100))) + audioData = readSectorTask.Result; + else + return null; + + // Load only the requested audio segment + byte[] audioDataSegment = new byte[count]; + int copyAmount = Math.Min(count, audioData.Length - _currentSectorReadPosition); + if(Math.Max(0, copyAmount) == 0) + return null; + + Array.Copy(audioData, _currentSectorReadPosition, audioDataSegment, 0, copyAmount); + + // Apply de-emphasis filtering, only if enabled + if(ApplyDeEmphasis) + _filterStage.ProcessAudioData(audioDataSegment); + + return audioDataSegment; + } + + /// + /// Select a track in the relative track list by number + /// + /// Relative track number to attempt to load + private void SelectRelativeTrack(int relativeTrackNumber) + { + if(_trackPlaybackOrder == null || _trackPlaybackOrder.Count == 0) + return; + + PlayerState wasPlaying = PlayerState; + if(wasPlaying == PlayerState.Playing) + Pause(); + + if(relativeTrackNumber < 0) + relativeTrackNumber = _trackPlaybackOrder.Count - 1; + else if(relativeTrackNumber >= _trackPlaybackOrder.Count) + relativeTrackNumber = 0; + + do + { + _currentTrackInOrder = relativeTrackNumber; + KeyValuePair discTrackPair = _trackPlaybackOrder[relativeTrackNumber]; + SelectDisc(discTrackPair.Key); + if(SelectTrack(discTrackPair.Value)) + break; + + relativeTrackNumber++; + if(relativeTrackNumber < 0) + relativeTrackNumber = _trackPlaybackOrder.Count - 1; + else if(relativeTrackNumber >= _trackPlaybackOrder.Count) + relativeTrackNumber = 0; + } + while(true); + + if(wasPlaying == PlayerState.Playing) + Play(); } #endregion @@ -477,7 +1102,7 @@ namespace RedBookPlayer.Models.Hardware /// /// Decrement the volume value /// - public void VolumeDown() => SetVolume(Volume + 1); + public void VolumeDown() => SetVolume(Volume - 1); /// /// Set the value for the volume @@ -524,59 +1149,138 @@ namespace RedBookPlayer.Models.Hardware /// /// Set de-emphasis status /// - /// - private void SetDeEmphasis(bool apply) => _soundOutput?.SetDeEmphasis(apply); + /// + private void SetDeEmphasis(bool applyDeEmphasis) => ApplyDeEmphasis = applyDeEmphasis; #endregion - #region Helpers + #region Extraction /// /// Extract a single track from the image to WAV /// /// /// Output path to write data to - public void ExtractSingleTrackToWav(uint trackNumber, string outputDirectory) => _opticalDisc?.ExtractTrackToWav(trackNumber, outputDirectory); + public void ExtractSingleTrackToWav(uint trackNumber, string outputDirectory) + { + OpticalDiscBase opticalDisc = _opticalDiscs[CurrentDisc]; + if(opticalDisc == null || !opticalDisc.Initialized) + return; + + if(opticalDisc is CompactDisc compactDisc) + { + // Get the track with that value, if possible + Track track = compactDisc.Tracks.FirstOrDefault(t => t.TrackSequence == trackNumber); + + // If the track isn't valid, we can't do anything + if(track == null || !(DataPlayback != DataPlayback.Skip || track.TrackType == TrackType.Audio)) + return; + + // Extract the track if it's valid + compactDisc.ExtractTrackToWav(trackNumber, outputDirectory, DataPlayback); + } + else + { + opticalDisc?.ExtractTrackToWav(trackNumber, outputDirectory); + } + } /// /// Extract all tracks from the image to WAV + /// /// Output path to write data to - public void ExtractAllTracksToWav(string outputDirectory) => _opticalDisc?.ExtractAllTracksToWav(outputDirectory); + public void ExtractAllTracksToWav(string outputDirectory) + { + OpticalDiscBase opticalDisc = _opticalDiscs[CurrentDisc]; + if(opticalDisc == null || !opticalDisc.Initialized) + return; + + if(opticalDisc is CompactDisc compactDisc) + { + foreach(Track track in compactDisc.Tracks) + { + ExtractSingleTrackToWav(track.TrackSequence, outputDirectory); + } + } + else + { + for(uint i = 0; i < opticalDisc.TotalTracks; i++) + { + ExtractSingleTrackToWav(i, outputDirectory); + } + } + } + + #endregion + + #region Setters /// /// Set data playback method [CompactDisc only] /// /// New playback value - public void SetDataPlayback(DataPlayback dataPlayback) + public void SetDataPlayback(DataPlayback dataPlayback) => DataPlayback = dataPlayback; + + /// + /// Set disc handling method + /// + /// New playback value + public void SetDiscHandling(DiscHandling discHandling) { - if(_opticalDisc is CompactDisc compactDisc) - compactDisc.DataPlayback = dataPlayback; + DiscHandling = discHandling; + LoadTrackList(); } /// /// 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; - } + /// True to enable loading hidden tracks, false otherwise + public void SetLoadHiddenTracks(bool loadHiddenTracks) => LoadHiddenTracks = loadHiddenTracks; /// /// Set repeat mode /// /// New repeat mode value - public void SetRepeatMode(RepeatMode repeatMode) => _soundOutput?.SetRepeatMode(repeatMode); + public void SetRepeatMode(RepeatMode repeatMode) => RepeatMode = repeatMode; /// /// Set the value for session handling [CompactDisc only] /// /// New session handling value - public void SetSessionHandling(SessionHandling sessionHandling) + public void SetSessionHandling(SessionHandling sessionHandling) => SessionHandling = sessionHandling; + + #endregion + + #region State Change Event Handlers + + /// + /// Handle special playback modes if we get flagged to + /// + private async void HandlePlaybackModes(object sender, PropertyChangedEventArgs e) { - if(_opticalDisc is CompactDisc compactDisc) - compactDisc.SessionHandling = sessionHandling; + if(e.PropertyName != nameof(ShouldInvokePlaybackModes)) + return; + + // Always pause before doing anything else + PlayerState wasPlaying = PlayerState; + await Dispatcher.UIThread.InvokeAsync(Pause); + + switch(RepeatMode) + { + case RepeatMode.None: + await Dispatcher.UIThread.InvokeAsync(Stop); + break; + case RepeatMode.Single: + _opticalDiscs[CurrentDisc].LoadTrack(CurrentTrackNumber); + break; + case RepeatMode.All: + NextTrack(); + break; + } + + _shouldInvokePlaybackModes = false; + if(wasPlaying == PlayerState.Playing) + await Dispatcher.UIThread.InvokeAsync(Play); } /// @@ -584,14 +1288,22 @@ namespace RedBookPlayer.Models.Hardware /// private void OpticalDiscStateChanged(object sender, PropertyChangedEventArgs e) { - CurrentTrackNumber = _opticalDisc.CurrentTrackNumber; - CurrentTrackIndex = _opticalDisc.CurrentTrackIndex; - CurrentSector = _opticalDisc.CurrentSector; - SectionStartSector = _opticalDisc.SectionStartSector; + if(_opticalDiscs[CurrentDisc] == null) + { + ImagePath = null; + CurrentTrackNumber = 1; + return; + } + + ImagePath = _opticalDiscs[CurrentDisc].ImagePath; + CurrentTrackNumber = _opticalDiscs[CurrentDisc].CurrentTrackNumber; + CurrentTrackIndex = _opticalDiscs[CurrentDisc].CurrentTrackIndex; + CurrentSector = _opticalDiscs[CurrentDisc].CurrentSector; + SectionStartSector = _opticalDiscs[CurrentDisc].SectionStartSector; HiddenTrack = TimeOffset > 150; - if(_opticalDisc is CompactDisc compactDisc) + if(_opticalDiscs[CurrentDisc] is CompactDisc compactDisc) { QuadChannel = compactDisc.QuadChannel; IsDataTrack = compactDisc.IsDataTrack; @@ -601,7 +1313,7 @@ namespace RedBookPlayer.Models.Hardware else { QuadChannel = false; - IsDataTrack = _opticalDisc.TrackType != TrackType.Audio; + IsDataTrack = _opticalDiscs[CurrentDisc].TrackType != TrackType.Audio; CopyAllowed = false; TrackHasEmphasis = false; } @@ -613,11 +1325,58 @@ namespace RedBookPlayer.Models.Hardware private void SoundOutputStateChanged(object sender, PropertyChangedEventArgs e) { PlayerState = _soundOutput.PlayerState; - RepeatMode = _soundOutput.RepeatMode; - ApplyDeEmphasis = _soundOutput.ApplyDeEmphasis; Volume = _soundOutput.Volume; } #endregion + + #region Helpers + + /// + /// Reformat raw subchannel data for multiple sectors + /// + /// Raw subchannel data to format + /// Dictionary mapping subchannel to formatted data + public Dictionary ConvertSubchannels(byte[] subchannelData) + { + if(subchannelData == null || subchannelData.Length % 96 != 0) + return null; + + // Prepare the output formatted data + int modValue = subchannelData.Length / 96; + Dictionary formattedData = new Dictionary + { + ['P'] = new byte[8 * modValue], + ['Q'] = new byte[8 * modValue], + ['R'] = new byte[8 * modValue], + ['S'] = new byte[8 * modValue], + ['T'] = new byte[8 * modValue], + ['U'] = new byte[8 * modValue], + ['V'] = new byte[8 * modValue], + ['W'] = new byte[8 * modValue], + }; + + // Read in 96-byte chunks + for(int i = 0; i < modValue; i++) + { + byte[] buffer = new byte[96]; + Array.Copy(subchannelData, i * 96, buffer, 0, 96); + var singleSubchannel = new SubchannelData(buffer); + Dictionary singleData = singleSubchannel.ConvertData(); + + Array.Copy(singleData['P'], 0, formattedData['P'], 8 * i, 8); + Array.Copy(singleData['Q'], 0, formattedData['Q'], 8 * i, 8); + Array.Copy(singleData['R'], 0, formattedData['R'], 8 * i, 8); + Array.Copy(singleData['S'], 0, formattedData['S'], 8 * i, 8); + Array.Copy(singleData['T'], 0, formattedData['T'], 8 * i, 8); + Array.Copy(singleData['U'], 0, formattedData['U'], 8 * i, 8); + Array.Copy(singleData['V'], 0, formattedData['V'], 8 * i, 8); + Array.Copy(singleData['W'], 0, formattedData['W'], 8 * i, 8); + } + + return formattedData; + } + + #endregion } } \ No newline at end of file diff --git a/RedBookPlayer.Models/Hardware/PlayerOptions.cs b/RedBookPlayer.Models/Hardware/PlayerOptions.cs new file mode 100644 index 0000000..d65170d --- /dev/null +++ b/RedBookPlayer.Models/Hardware/PlayerOptions.cs @@ -0,0 +1,30 @@ +namespace RedBookPlayer.Models.Discs +{ + public class PlayerOptions + { + /// + /// Indicate how data tracks should be handled + /// + public DataPlayback DataPlayback { get; set; } = DataPlayback.Skip; + + /// + /// Indicates how to deal with multiple discs + /// + public DiscHandling DiscHandling { get; set; } = DiscHandling.SingleDisc; + + /// + /// Indicate if hidden tracks should be loaded + /// + public bool LoadHiddenTracks { get; set; } = false; + + /// + /// Indicates the repeat mode + /// + public RepeatMode RepeatMode { get; set; } = RepeatMode.None; + + /// + /// Indicates how tracks on different session should be handled + /// + public SessionHandling SessionHandling { get; set; } = SessionHandling.AllSessions; + } +} \ No newline at end of file diff --git a/RedBookPlayer.Models/Hardware/SoundOutput.cs b/RedBookPlayer.Models/Hardware/SoundOutput.cs deleted file mode 100644 index c89a223..0000000 --- a/RedBookPlayer.Models/Hardware/SoundOutput.cs +++ /dev/null @@ -1,382 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using CSCore.SoundOut; -using NWaves.Audio; -using ReactiveUI; -using RedBookPlayer.Models.Discs; - -namespace RedBookPlayer.Models.Hardware -{ - public class SoundOutput : ReactiveObject - { - #region Public Fields - - /// - /// Indicate if the output is ready to be used - /// - public bool Initialized - { - get => _initialized; - private set => this.RaiseAndSetIfChanged(ref _initialized, value); - } - - /// - /// Indicates the current player state - /// - public PlayerState PlayerState - { - get => _playerState; - private set => this.RaiseAndSetIfChanged(ref _playerState, value); - } - - /// - /// Indicates the repeat mode - /// - public RepeatMode RepeatMode - { - get => _repeatMode; - private set => this.RaiseAndSetIfChanged(ref _repeatMode, value); - } - - /// - /// Indicates if de-emphasis should be applied - /// - public bool ApplyDeEmphasis - { - get => _applyDeEmphasis; - private set => this.RaiseAndSetIfChanged(ref _applyDeEmphasis, value); - } - - /// - /// Current playback volume - /// - public int Volume - { - get => _volume; - private set - { - int tempVolume = value; - if(value > 100) - tempVolume = 100; - else if(value < 0) - tempVolume = 0; - - this.RaiseAndSetIfChanged(ref _volume, tempVolume); - } - } - - private bool _initialized; - private PlayerState _playerState; - private RepeatMode _repeatMode; - private bool _applyDeEmphasis; - private int _volume; - - #endregion - - #region Private State Variables - - /// - /// OpticalDisc from the parent player for easy access - /// - /// - /// TODO: Can we remove the need for a local reference to OpticalDisc? - /// - private OpticalDiscBase _opticalDisc; - - /// - /// Data provider for sound output - /// - private PlayerSource _source; - - /// - /// Sound output instance - /// - private ALSoundOut _soundOut; - - /// - /// Filtering stage for audio output - /// - private FilterStage _filterStage; - - /// - /// Current position in the sector - /// - private int _currentSectorReadPosition = 0; - - /// - /// Lock object for reading track data - /// - private readonly object _readingImage = new object(); - - #endregion - - /// - /// Constructor - /// - /// Default volume between 0 and 100 to use when starting playback - public SoundOutput(int defaultVolume = 100) - { - Volume = defaultVolume; - _filterStage = new FilterStage(); - } - - - /// - /// Initialize the output with a given image - /// - /// OpticalDisc to load from - /// RepeatMode for sound output - /// True if playback should begin immediately, false otherwise - public void Init(OpticalDiscBase opticalDisc, RepeatMode repeatMode, bool autoPlay) - { - // If we have an unusable disc, just return - if(opticalDisc == null || !opticalDisc.Initialized) - return; - - // Save a reference to the disc - _opticalDisc = opticalDisc; - - // Enable de-emphasis for CDs, if necessary - if(opticalDisc is CompactDisc compactDisc) - ApplyDeEmphasis = compactDisc.TrackHasEmphasis; - - // Setup de-emphasis filters - _filterStage.SetupFilters(); - - // Setup the audio output - SetupAudio(); - - // Setup the repeat mode - RepeatMode = repeatMode; - - // Initialize playback, if necessary - if(autoPlay) - _soundOut.Play(); - - // Mark the output as ready - Initialized = true; - PlayerState = PlayerState.Stopped; - - // Begin loading data - _source.Start(); - } - - /// - /// Reset the current internal state - /// - public void Reset() - { - _soundOut.Stop(); - _opticalDisc = null; - Initialized = false; - PlayerState = PlayerState.NoDisc; - } - - /// - /// Fill the current byte buffer with playable data - /// - /// Buffer to load data into - /// Offset in the buffer to load at - /// Number of bytes to load - /// Number of bytes read - public int ProviderRead(byte[] buffer, int offset, int count) - { - // Set the current volume - _soundOut.Volume = (float)Volume / 100; - - // If we have an unreadable track, just return - if(_opticalDisc.BytesPerSector <= 0) - { - Array.Clear(buffer, offset, count); - return count; - } - - // Determine how many sectors we can read - DetermineReadAmount(count, out ulong sectorsToRead, out ulong zeroSectorsAmount); - - // Get data to return - byte[] audioDataSegment = ReadData(count, sectorsToRead, zeroSectorsAmount); - if(audioDataSegment == null) - { - Array.Clear(buffer, offset, count); - return count; - } - - // Write out the audio data to the buffer - Array.Copy(audioDataSegment, 0, buffer, offset, count); - - // Set the read position in the sector for easier access - _currentSectorReadPosition += count; - if(_currentSectorReadPosition >= _opticalDisc.BytesPerSector) - { - int currentTrack = _opticalDisc.CurrentTrackNumber; - _opticalDisc.SetCurrentSector(_opticalDisc.CurrentSector + (ulong)(_currentSectorReadPosition / _opticalDisc.BytesPerSector)); - _currentSectorReadPosition %= _opticalDisc.BytesPerSector; - - if(RepeatMode == RepeatMode.None && _opticalDisc.CurrentTrackNumber < currentTrack) - Stop(); - else if(RepeatMode == RepeatMode.Single && _opticalDisc.CurrentTrackNumber != currentTrack) - _opticalDisc.LoadTrack(currentTrack); - } - - return count; - } - - #region Playback - - /// - /// Start audio playback - /// - public void Play() - { - if(_soundOut.PlaybackState != PlaybackState.Playing) - _soundOut.Play(); - - PlayerState = PlayerState.Playing; - } - - /// - /// Pause audio playback - /// - public void Pause() - { - if(_soundOut.PlaybackState != PlaybackState.Paused) - _soundOut.Pause(); - - PlayerState = PlayerState.Paused; - } - - /// - /// Stop audio playback - /// - public void Stop() - { - if(_soundOut.PlaybackState != PlaybackState.Stopped) - _soundOut.Stop(); - - PlayerState = PlayerState.Stopped; - } - - /// - /// Eject the currently loaded disc - /// - public void Eject() => Reset(); - - #endregion - - #region Helpers - - /// - /// Set de-emphasis status - /// - /// New de-emphasis status - public void SetDeEmphasis(bool apply) => ApplyDeEmphasis = apply; - - /// - /// Set repeat mode - /// - /// New repeat mode value - public void SetRepeatMode(RepeatMode repeatMode) => RepeatMode = repeatMode; - - /// - /// 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) - { - // Attempt to read 10 more sectors than requested - sectorsToRead = ((ulong)count / (ulong)_opticalDisc.BytesPerSector) + 10; - 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); - } - } - - /// - /// Read the requested amount of data from an input - /// - /// Number of bytes to load - /// Number of sectors to read - /// Number of zeroed sectors to concatenate - /// The requested amount of data, if possible - private byte[] ReadData(int count, ulong sectorsToRead, ulong zeroSectorsAmount) - { - // Create padding data for overreads - byte[] zeroSectors = new byte[(int)zeroSectorsAmount * _opticalDisc.BytesPerSector]; - byte[] audioData; - - // Attempt to read the required number of sectors - var readSectorTask = Task.Run(() => - { - lock(_readingImage) - { - for(int i = 0; i < 4; i++) - { - try - { - return _opticalDisc.ReadSectors((uint)sectorsToRead).Concat(zeroSectors).ToArray(); - } - catch { } - } - - return zeroSectors; - } - }); - - // Wait 100ms at longest for the read to occur - if(readSectorTask.Wait(TimeSpan.FromMilliseconds(100))) - audioData = readSectorTask.Result; - else - return null; - - // Load only the requested audio segment - byte[] audioDataSegment = new byte[count]; - int copyAmount = Math.Min(count, audioData.Length - _currentSectorReadPosition); - if(Math.Max(0, copyAmount) == 0) - return null; - - Array.Copy(audioData, _currentSectorReadPosition, audioDataSegment, 0, copyAmount); - - // Apply de-emphasis filtering, only if enabled - if(ApplyDeEmphasis) - _filterStage.ProcessAudioData(audioDataSegment); - - return audioDataSegment; - } - - /// - /// Sets or resets the audio playback objects - /// - private void SetupAudio() - { - if(_source == null) - { - _source = new PlayerSource(ProviderRead); - _soundOut = new ALSoundOut(100); - _soundOut.Initialize(_source); - } - else - { - _soundOut.Stop(); - } - } - - #endregion - } -} \ No newline at end of file diff --git a/RedBookPlayer.Models/Hardware/SubchannelData.cs b/RedBookPlayer.Models/Hardware/SubchannelData.cs new file mode 100644 index 0000000..93b2d24 --- /dev/null +++ b/RedBookPlayer.Models/Hardware/SubchannelData.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; + +namespace RedBookPlayer.Models.Hardware +{ + /// + /// Represents subchannel data for a single sector + /// + /// + internal class SubchannelData + { + public SubchannelPacket[] Packets { get; private set; } = new SubchannelPacket[4]; + + /// + /// Create a new subchannel data from a byte array + /// + public SubchannelData(byte[] bytes) + { + if(bytes == null || bytes.Length != 96) + return; + + byte[] buffer = new byte[24]; + for(int i = 0; i < 4; i++) + { + Array.Copy(bytes, 24 * i, buffer, 0, 24); + Packets[i] = new SubchannelPacket(buffer); + } + } + + /// + /// Convert the packet data into separate named subchannels + /// + public Dictionary ConvertData() + { + if(this.Packets == null || this.Packets.Length != 4) + return null; + + // Prepare the output formatted data + Dictionary formattedData = new Dictionary + { + ['P'] = new byte[8], + ['Q'] = new byte[8], + ['R'] = new byte[8], + ['S'] = new byte[8], + ['T'] = new byte[8], + ['U'] = new byte[8], + ['V'] = new byte[8], + ['W'] = new byte[8], + }; + + // Loop through all subchannel packets + for(int i = 0; i < 4; i++) + { + Dictionary singleData = Packets[i].ConvertData(); + + Array.Copy(singleData['P'], 0, formattedData['P'], 2 * i, 2); + Array.Copy(singleData['Q'], 0, formattedData['Q'], 2 * i, 2); + Array.Copy(singleData['R'], 0, formattedData['R'], 2 * i, 2); + Array.Copy(singleData['S'], 0, formattedData['S'], 2 * i, 2); + Array.Copy(singleData['T'], 0, formattedData['T'], 2 * i, 2); + Array.Copy(singleData['U'], 0, formattedData['U'], 2 * i, 2); + Array.Copy(singleData['V'], 0, formattedData['V'], 2 * i, 2); + Array.Copy(singleData['W'], 0, formattedData['W'], 2 * i, 2); + } + + return formattedData; + } + } +} \ No newline at end of file diff --git a/RedBookPlayer.Models/Hardware/SubchannelPacket.cs b/RedBookPlayer.Models/Hardware/SubchannelPacket.cs new file mode 100644 index 0000000..2051439 --- /dev/null +++ b/RedBookPlayer.Models/Hardware/SubchannelPacket.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; + +namespace RedBookPlayer.Models.Hardware +{ + /// + /// Represents a single packet of subcode data + /// + /// + internal class SubchannelPacket + { + public byte Command { get; private set; } + public byte Instruction { get; private set; } + public byte[] ParityQ { get; private set; } = new byte[2]; + public byte[] Data { get; private set; } = new byte[16]; + public byte[] ParityP { get; private set; } = new byte[4]; + + /// + /// Create a new subchannel packet from a byte array + /// + public SubchannelPacket(byte[] bytes) + { + if(bytes == null || bytes.Length != 24) + return; + + this.Command = bytes[0]; + this.Instruction = bytes[1]; + + Array.Copy(bytes, 2, this.ParityQ, 0, 2); + Array.Copy(bytes, 4, this.Data, 0, 16); + Array.Copy(bytes, 20, this.ParityP, 0, 4); + } + + /// + /// Convert the data into separate named subchannels + /// + public Dictionary ConvertData() + { + if(this.Data == null || this.Data.Length != 16) + return null; + + // Create the output dictionary for the formatted data + Dictionary formattedData = new Dictionary + { + ['P'] = new byte[2], + ['Q'] = new byte[2], + ['R'] = new byte[2], + ['S'] = new byte[2], + ['T'] = new byte[2], + ['U'] = new byte[2], + ['V'] = new byte[2], + ['W'] = new byte[2], + }; + + // Loop through all bytes in the subchannel data and populate + int index = -1; + for(int i = 0; i < 16; i++) + { + // Get the modulo value of the current byte + int modValue = i % 8; + if(modValue == 0) + index++; + + // Retrieve the next byte + byte b = this.Data[i]; + + // Set the respective bit in the new byte data + formattedData['P'][index] |= (byte)(HasBitSet(b, 7) ? 1 << (7 - modValue) : 0); + formattedData['Q'][index] |= (byte)(HasBitSet(b, 6) ? 1 << (7 - modValue) : 0); + formattedData['R'][index] |= (byte)(HasBitSet(b, 5) ? 1 << (7 - modValue) : 0); + formattedData['S'][index] |= (byte)(HasBitSet(b, 4) ? 1 << (7 - modValue) : 0); + formattedData['T'][index] |= (byte)(HasBitSet(b, 3) ? 1 << (7 - modValue) : 0); + formattedData['U'][index] |= (byte)(HasBitSet(b, 2) ? 1 << (7 - modValue) : 0); + formattedData['V'][index] |= (byte)(HasBitSet(b, 1) ? 1 << (7 - modValue) : 0); + formattedData['W'][index] |= (byte)(HasBitSet(b, 0) ? 1 << (7 - modValue) : 0); + } + + return formattedData; + } + + /// + /// Check if a bit is set in a byte + /// + /// Byte value to check + /// Index of the bit to check + /// True if the bit was set, false otherwise + private bool HasBitSet(byte value, int bitIndex) => (value & (1 << bitIndex)) != 0; + } +} \ No newline at end of file diff --git a/RedBookPlayer.Models/RedBookPlayer.Models.csproj b/RedBookPlayer.Models/RedBookPlayer.Models.csproj index 7ed4a85..c0a4c68 100644 --- a/RedBookPlayer.Models/RedBookPlayer.Models.csproj +++ b/RedBookPlayer.Models/RedBookPlayer.Models.csproj @@ -2,6 +2,8 @@ netcoreapp3.1 + win-x64;linux-x64 + linux-x64 true