From ee0fbc4ccc3630cc6dbd88108f5ee75e74f14958 Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Tue, 5 Oct 2021 21:40:41 -0700 Subject: [PATCH] Discs and audio output should be less aware --- .../ViewModels/PlayerViewModel.cs | 19 +- .../ViewModels/SettingsViewModel.cs | 2 +- RedBookPlayer.Models/Discs/CompactDisc.cs | 344 ++------ RedBookPlayer.Models/Discs/OpticalDiscBase.cs | 39 +- .../Discs/OpticalDiscOptions.cs | 15 - RedBookPlayer.Models/Enums.cs | 9 +- .../Hardware/Linux/AudioBackend.cs | 2 +- .../Hardware/Mac/AudioBackend.cs | 52 -- RedBookPlayer.Models/Hardware/Player.cs | 748 +++++++++++++----- .../Hardware/PlayerOptions.cs | 25 + RedBookPlayer.Models/Hardware/SoundOutput.cs | 224 +----- .../Hardware/Windows/AudioBackend.cs | 2 +- 12 files changed, 685 insertions(+), 796 deletions(-) delete mode 100644 RedBookPlayer.Models/Hardware/Mac/AudioBackend.cs create mode 100644 RedBookPlayer.Models/Hardware/PlayerOptions.cs diff --git a/RedBookPlayer.GUI/ViewModels/PlayerViewModel.cs b/RedBookPlayer.GUI/ViewModels/PlayerViewModel.cs index 2a1ac21..aa8013f 100644 --- a/RedBookPlayer.GUI/ViewModels/PlayerViewModel.cs +++ b/RedBookPlayer.GUI/ViewModels/PlayerViewModel.cs @@ -406,17 +406,17 @@ 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; @@ -654,19 +654,24 @@ 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, 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(); diff --git a/RedBookPlayer.GUI/ViewModels/SettingsViewModel.cs b/RedBookPlayer.GUI/ViewModels/SettingsViewModel.cs index 685d38e..178273f 100644 --- a/RedBookPlayer.GUI/ViewModels/SettingsViewModel.cs +++ b/RedBookPlayer.GUI/ViewModels/SettingsViewModel.cs @@ -77,7 +77,7 @@ namespace RedBookPlayer.GUI.ViewModels /// /// Indicates how to repeat tracks /// - public RepeatMode RepeatMode { get; set; } = RepeatMode.All; + public RepeatMode RepeatMode { get; set; } = RepeatMode.AllSingleDisc; /// /// Indicates how to handle tracks on different sessions diff --git a/RedBookPlayer.Models/Discs/CompactDisc.cs b/RedBookPlayer.Models/Discs/CompactDisc.cs index 3cf7e8d..051fef0 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; - if(value > track.Indexes.Keys.Max()) - fixedValue = track.Indexes.Keys.Min(); - else if(value < track.Indexes.Keys.Min()) - fixedValue = track.Indexes.Keys.Max(); + // Invalid value means we can't do anything + if (value > track.Indexes.Keys.Max()) + return; + else if (value < track.Indexes.Keys.Min()) + 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,13 +218,7 @@ 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(string path, IOpticalMediaImage image, bool autoPlay) @@ -313,8 +235,9 @@ namespace RedBookPlayer.Models.Discs 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) @@ -330,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"); @@ -471,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; } } @@ -501,28 +327,49 @@ namespace RedBookPlayer.Models.Discs } /// - 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.Skip); /// /// 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) + 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); @@ -543,23 +390,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 c871713..5dcd612 100644 --- a/RedBookPlayer.Models/Discs/OpticalDiscBase.cs +++ b/RedBookPlayer.Models/Discs/OpticalDiscBase.cs @@ -103,34 +103,6 @@ namespace RedBookPlayer.Models.Discs /// True if playback should begin immediately, false otherwise public abstract void Init(string path, 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 - #region Helpers /// @@ -140,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 /// @@ -153,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 add206b..472267f 100644 --- a/RedBookPlayer.Models/Enums.cs +++ b/RedBookPlayer.Models/Enums.cs @@ -64,9 +64,14 @@ namespace RedBookPlayer.Models Single, /// - /// Repeat all tracks + /// Repeat all tracks on a single disc /// - All, + AllSingleDisc, + + /// + /// Repeat all tracks on a multiple discs + /// + AllMultiDisc, } /// diff --git a/RedBookPlayer.Models/Hardware/Linux/AudioBackend.cs b/RedBookPlayer.Models/Hardware/Linux/AudioBackend.cs index b84ffa2..92043fb 100644 --- a/RedBookPlayer.Models/Hardware/Linux/AudioBackend.cs +++ b/RedBookPlayer.Models/Hardware/Linux/AudioBackend.cs @@ -7,7 +7,7 @@ namespace RedBookPlayer.Models.Hardware.Linux /// /// Sound output instance /// - private ALSoundOut _soundOut; + private readonly ALSoundOut _soundOut; public AudioBackend() { } diff --git a/RedBookPlayer.Models/Hardware/Mac/AudioBackend.cs b/RedBookPlayer.Models/Hardware/Mac/AudioBackend.cs deleted file mode 100644 index 6f7949d..0000000 --- a/RedBookPlayer.Models/Hardware/Mac/AudioBackend.cs +++ /dev/null @@ -1,52 +0,0 @@ -using CSCore.SoundOut; - -namespace RedBookPlayer.Models.Hardware.Mac -{ - public class AudioBackend : IAudioBackend - { - /// - /// Sound output instance - /// - private 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/Player.cs b/RedBookPlayer.Models/Hardware/Player.cs index 425e5ff..684d421 100644 --- a/RedBookPlayer.Models/Hardware/Player.cs +++ b/RedBookPlayer.Models/Hardware/Player.cs @@ -1,5 +1,10 @@ +using System; 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.Discs; using RedBookPlayer.Models.Factories; @@ -36,9 +41,19 @@ namespace RedBookPlayer.Models.Hardware } } + /// + /// 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 bool _shouldInvokePlaybackModes; #region OpticalDisc Passthrough @@ -201,6 +216,15 @@ namespace RedBookPlayer.Models.Hardware 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 /// @@ -210,6 +234,15 @@ namespace RedBookPlayer.Models.Hardware 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 /// @@ -230,7 +263,9 @@ namespace RedBookPlayer.Models.Hardware private PlayerState _playerState; private DataPlayback _dataPlayback; + private bool _loadHiddenTracks; private RepeatMode _repeatMode; + private SessionHandling _sessionHandling; private bool _applyDeEmphasis; private int _volume; @@ -241,7 +276,7 @@ namespace RedBookPlayer.Models.Hardware /// /// Sound output handling class /// - private readonly SoundOutput[] _soundOutputs; + private readonly SoundOutput _soundOutput; /// /// OpticalDisc object @@ -253,6 +288,21 @@ namespace RedBookPlayer.Models.Hardware /// private int? _lastVolume = null; + /// + /// Filtering stage for audio output + /// + private 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 /// @@ -268,44 +318,51 @@ namespace RedBookPlayer.Models.Hardware numberOfDiscs = 1; _numberOfDiscs = numberOfDiscs; - _soundOutputs = new SoundOutput[_numberOfDiscs]; _opticalDiscs = new OpticalDiscBase[numberOfDiscs]; - _currentDisc = 0; - for (int i = 0; i < _numberOfDiscs; i++) - { - _soundOutputs[i] = new SoundOutput(defaultVolume); - _soundOutputs[i].SetDeEmphasis(false); - } + + _filterStage = new FilterStage(); + _soundOutput = new SoundOutput(defaultVolume); + + PropertyChanged += HandlePlaybackModes; } /// /// 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; + LoadHiddenTracks = playerOptions.LoadHiddenTracks; + RepeatMode = playerOptions.RepeatMode; + SessionHandling = playerOptions.SessionHandling; + // Initalize the disc - _opticalDiscs[CurrentDisc] = OpticalDiscFactory.GenerateFromPath(path, options, autoPlay); + _opticalDiscs[CurrentDisc] = OpticalDiscFactory.GenerateFromPath(path, opticalDiscOptions, autoPlay); if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized) return; // Add event handling for the optical disc _opticalDiscs[CurrentDisc].PropertyChanged += OpticalDiscStateChanged; + // Setup de-emphasis filters + _filterStage.SetupFilters(); + // Initialize the sound output - _soundOutputs[CurrentDisc].Init(_opticalDiscs[CurrentDisc], repeatMode, autoPlay); - if(_soundOutputs[CurrentDisc] == null || !_soundOutputs[CurrentDisc].Initialized) + _soundOutput.Init(ProviderRead, autoPlay); + if(_soundOutput == null || !_soundOutput.Initialized) return; // Add event handling for the sound output - _soundOutputs[CurrentDisc].PropertyChanged += SoundOutputStateChanged; + _soundOutput.PropertyChanged += SoundOutputStateChanged; // Mark the player as ready Initialized = true; @@ -315,7 +372,7 @@ namespace RedBookPlayer.Models.Hardware SoundOutputStateChanged(this, null); } - #region Playback + #region Playback (UI) /// /// Begin playback @@ -324,12 +381,12 @@ namespace RedBookPlayer.Models.Hardware { if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized) return; - else if(_soundOutputs[CurrentDisc] == null) + else if(_soundOutput == null) return; - else if(_soundOutputs[CurrentDisc].PlayerState != PlayerState.Paused && _soundOutputs[CurrentDisc].PlayerState != PlayerState.Stopped) + else if(_soundOutput.PlayerState != PlayerState.Paused && _soundOutput.PlayerState != PlayerState.Stopped) return; - _soundOutputs[CurrentDisc].Play(); + _soundOutput.Play(); _opticalDiscs[CurrentDisc].SetTotalIndexes(); PlayerState = PlayerState.Playing; } @@ -341,12 +398,12 @@ namespace RedBookPlayer.Models.Hardware { if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized) return; - else if(_soundOutputs[CurrentDisc] == null) + else if(_soundOutput == null) return; - else if(_soundOutputs[CurrentDisc].PlayerState != PlayerState.Playing) + else if(_soundOutput.PlayerState != PlayerState.Playing) return; - _soundOutputs[CurrentDisc]?.Pause(); + _soundOutput.Pause(); PlayerState = PlayerState.Paused; } @@ -378,13 +435,14 @@ namespace RedBookPlayer.Models.Hardware { if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized) return; - else if(_soundOutputs[CurrentDisc] == null) - return; - else if(_soundOutputs[CurrentDisc].PlayerState != PlayerState.Playing && _soundOutputs[CurrentDisc].PlayerState != PlayerState.Paused) + else if(_soundOutput == null) return; + else if(_soundOutput.PlayerState != PlayerState.Playing && _soundOutput.PlayerState != PlayerState.Paused) + return; - _soundOutputs[CurrentDisc].Stop(); - _opticalDiscs[CurrentDisc].LoadFirstTrack(); + _soundOutput.Stop(); + CurrentTrackNumber = 0; + SelectTrack(1); PlayerState = PlayerState.Stopped; } @@ -395,199 +453,47 @@ namespace RedBookPlayer.Models.Hardware { if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized) return; - else if(_soundOutputs[CurrentDisc] == null) + else if(_soundOutput == null) return; Stop(); - _soundOutputs[CurrentDisc].Eject(); + _soundOutput.Eject(); _opticalDiscs[CurrentDisc] = null; PlayerState = PlayerState.NoDisc; Initialized = false; } - /// - /// Select a particular disc by number - /// - public void SelectDisc(int discNumber) - { - PlayerState wasPlaying = PlayerState; - if (wasPlaying == PlayerState.Playing) - Stop(); - - CurrentDisc = discNumber; - if (_opticalDiscs[CurrentDisc] != null && _opticalDiscs[CurrentDisc].Initialized) - { - Initialized = true; - OpticalDiscStateChanged(this, null); - SoundOutputStateChanged(this, null); - - if(wasPlaying == PlayerState.Playing) - Play(); - } - else - { - PlayerState = PlayerState.NoDisc; - Initialized = false; - } - } - /// /// Move to the next disc /// - public void NextDisc() - { - PlayerState wasPlaying = PlayerState; - if (wasPlaying == PlayerState.Playing) - Stop(); - - CurrentDisc++; - if (_opticalDiscs[CurrentDisc] != null && _opticalDiscs[CurrentDisc].Initialized) - { - Initialized = true; - OpticalDiscStateChanged(this, null); - SoundOutputStateChanged(this, null); - - if(wasPlaying == PlayerState.Playing) - Play(); - } - else - { - PlayerState = PlayerState.NoDisc; - Initialized = false; - } - } + public void NextDisc() => SelectDisc(CurrentDisc + 1); /// /// Move to the previous disc /// - public void PreviousDisc() - { - PlayerState wasPlaying = PlayerState; - if (wasPlaying == PlayerState.Playing) - Stop(); - - CurrentDisc--; - if (_opticalDiscs[CurrentDisc] != null && _opticalDiscs[CurrentDisc].Initialized) - { - Initialized = true; - OpticalDiscStateChanged(this, null); - SoundOutputStateChanged(this, null); - - if(wasPlaying == PlayerState.Playing) - Play(); - } - else - { - PlayerState = PlayerState.NoDisc; - Initialized = false; - } - } - - /// - /// Select a particular track by number - /// - public void SelectTrack(int trackNumber) - { - if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized) - return; - - PlayerState wasPlaying = PlayerState; - if(wasPlaying == PlayerState.Playing) - Pause(); - - if(trackNumber < (HiddenTrack ? 0 : 1) || trackNumber > TotalTracks) - _opticalDiscs[CurrentDisc].LoadFirstTrack(); - else - _opticalDiscs[CurrentDisc].LoadTrack(trackNumber); - - if(_opticalDiscs[CurrentDisc] is CompactDisc compactDisc) - _soundOutputs[CurrentDisc].SetDeEmphasis(compactDisc.TrackHasEmphasis); - - if(wasPlaying == PlayerState.Playing) - Play(); - } + public void PreviousDisc() => SelectDisc(CurrentDisc - 1); /// /// Move to the next playable track /// - public void NextTrack() - { - if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized) - return; - - PlayerState wasPlaying = PlayerState; - if(wasPlaying == PlayerState.Playing) - Pause(); - - _opticalDiscs[CurrentDisc].NextTrack(); - if(_opticalDiscs[CurrentDisc] is CompactDisc compactDisc) - _soundOutputs[CurrentDisc].SetDeEmphasis(compactDisc.TrackHasEmphasis); - - if(wasPlaying == PlayerState.Playing) - Play(); - } + public void NextTrack() => SelectTrack(CurrentTrackNumber + 1); /// /// Move to the previous playable track /// - public void PreviousTrack() - { - if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized) - return; - - PlayerState wasPlaying = PlayerState; - if(wasPlaying == PlayerState.Playing) - Pause(); - - _opticalDiscs[CurrentDisc].PreviousTrack(); - if(_opticalDiscs[CurrentDisc] is CompactDisc compactDisc) - _soundOutputs[CurrentDisc].SetDeEmphasis(compactDisc.TrackHasEmphasis); - - if(wasPlaying == PlayerState.Playing) - Play(); - } + public void PreviousTrack() => SelectTrack(CurrentTrackNumber - 1); /// /// Move to the next index /// /// True if index changes can trigger a track change, false otherwise - public void NextIndex(bool changeTrack) - { - if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized) - return; - - PlayerState wasPlaying = PlayerState; - if(wasPlaying == PlayerState.Playing) - Pause(); - - _opticalDiscs[CurrentDisc].NextIndex(changeTrack); - if(_opticalDiscs[CurrentDisc] is CompactDisc compactDisc) - _soundOutputs[CurrentDisc].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(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized) - return; - - PlayerState wasPlaying = PlayerState; - if(wasPlaying == PlayerState.Playing) - Pause(); - - _opticalDiscs[CurrentDisc].PreviousIndex(changeTrack); - if(_opticalDiscs[CurrentDisc] is CompactDisc compactDisc) - _soundOutputs[CurrentDisc].SetDeEmphasis(compactDisc.TrackHasEmphasis); - - if(wasPlaying == PlayerState.Playing) - Play(); - } + public void PreviousIndex(bool changeTrack) => SelectIndex((ushort)(CurrentTrackIndex - 1), changeTrack); /// /// Fast-forward playback by 75 sectors @@ -613,6 +519,348 @@ namespace RedBookPlayer.Models.Hardware #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(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 >= _opticalDiscs[CurrentDisc].BytesPerSector) + { + 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; + } + } + + return count; + } + + /// + /// Select a disc by number + /// + /// Disc number to attempt to load + public void SelectDisc(int discNumber) + { + PlayerState wasPlaying = PlayerState; + if (wasPlaying == PlayerState.Playing) + Stop(); + + _currentSectorReadPosition = 0; + + CurrentDisc = discNumber; + if (_opticalDiscs[CurrentDisc] != null && _opticalDiscs[CurrentDisc].Initialized) + { + Initialized = true; + 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) + { + 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 + /// + /// Track number to attempt to load + /// Changing track with RepeatMode.AllMultiDisc should switch discs + public void SelectTrack(int trackNumber) + { + if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized) + return; + + 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; + + // 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); + } + + if(wasPlaying == PlayerState.Playing) + Play(); + } + + /// + /// 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)_opticalDiscs[CurrentDisc].BytesPerSector) + 10; + 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) + 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; + } + + #endregion + #region Volume /// @@ -629,7 +877,7 @@ namespace RedBookPlayer.Models.Hardware /// Set the value for the volume /// /// New volume value - public void SetVolume(int volume) => _soundOutputs[CurrentDisc]?.SetVolume(volume); + public void SetVolume(int volume) => _soundOutput?.SetVolume(volume); /// /// Temporarily mute playback @@ -670,59 +918,137 @@ namespace RedBookPlayer.Models.Hardware /// /// Set de-emphasis status /// - /// - private void SetDeEmphasis(bool apply) => _soundOutputs[CurrentDisc]?.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) => _opticalDiscs[CurrentDisc]?.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) => _opticalDiscs[CurrentDisc]?.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) - { - if(_opticalDiscs[CurrentDisc] is CompactDisc compactDisc) - compactDisc.DataPlayback = dataPlayback; - } + public void SetDataPlayback(DataPlayback dataPlayback) => DataPlayback = dataPlayback; /// /// Set the value for loading hidden tracks [CompactDisc only] /// - /// True to enable loading hidden tracks, false otherwise - public void SetLoadHiddenTracks(bool load) - { - if(_opticalDiscs[CurrentDisc] 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) => _soundOutputs[CurrentDisc]?.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(_opticalDiscs[CurrentDisc] is CompactDisc compactDisc) - compactDisc.SessionHandling = sessionHandling; + if(e.PropertyName != nameof(ShouldInvokePlaybackModes)) + return; + + // Always stop before doing anything else + PlayerState wasPlaying = PlayerState; + await Dispatcher.UIThread.InvokeAsync(Stop); + + switch(RepeatMode) + { + case RepeatMode.None: + // No-op + break; + case RepeatMode.Single: + _opticalDiscs[CurrentDisc].LoadTrack(CurrentTrackNumber); + break; + case RepeatMode.AllSingleDisc: + SelectTrack(1); + break; + case RepeatMode.AllMultiDisc: + do + { + NextDisc(); + } + while(_opticalDiscs[CurrentDisc] != null && !_opticalDiscs[CurrentDisc].Initialized); + + SelectTrack(1); + break; + } + + _shouldInvokePlaybackModes = false; + if(wasPlaying == PlayerState.Playing) + await Dispatcher.UIThread.InvokeAsync(Play); } /// @@ -759,10 +1085,8 @@ namespace RedBookPlayer.Models.Hardware /// private void SoundOutputStateChanged(object sender, PropertyChangedEventArgs e) { - PlayerState = _soundOutputs[CurrentDisc].PlayerState; - RepeatMode = _soundOutputs[CurrentDisc].RepeatMode; - ApplyDeEmphasis = _soundOutputs[CurrentDisc].ApplyDeEmphasis; - Volume = _soundOutputs[CurrentDisc].Volume; + PlayerState = _soundOutput.PlayerState; + Volume = _soundOutput.Volume; } #endregion diff --git a/RedBookPlayer.Models/Hardware/PlayerOptions.cs b/RedBookPlayer.Models/Hardware/PlayerOptions.cs new file mode 100644 index 0000000..43a0cbd --- /dev/null +++ b/RedBookPlayer.Models/Hardware/PlayerOptions.cs @@ -0,0 +1,25 @@ +namespace RedBookPlayer.Models.Discs +{ + public class PlayerOptions + { + /// + /// 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 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 index 27a1047..246c004 100644 --- a/RedBookPlayer.Models/Hardware/SoundOutput.cs +++ b/RedBookPlayer.Models/Hardware/SoundOutput.cs @@ -1,9 +1,5 @@ -using System; -using System.Linq; using System.Runtime.InteropServices; -using System.Threading.Tasks; using ReactiveUI; -using RedBookPlayer.Models.Discs; namespace RedBookPlayer.Models.Hardware { @@ -29,24 +25,6 @@ namespace RedBookPlayer.Models.Hardware 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 /// @@ -67,22 +45,12 @@ namespace RedBookPlayer.Models.Hardware 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 /// @@ -93,60 +61,26 @@ namespace RedBookPlayer.Models.Hardware /// private IAudioBackend _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(); - } + public SoundOutput(int defaultVolume = 100) => Volume = defaultVolume; /// /// Initialize the output with a given image /// - /// OpticalDisc to load from - /// RepeatMode for sound output + /// ReadFunction to use during decoding /// True if playback should begin immediately, false otherwise - public void Init(OpticalDiscBase opticalDisc, RepeatMode repeatMode, bool autoPlay) + public void Init(PlayerSource.ReadFunction read, 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(); + // Reset initialization + Initialized = false; // Setup the audio output - SetupAudio(); - - // Setup the repeat mode - RepeatMode = repeatMode; + SetupAudio(read); // Initialize playback, if necessary if(autoPlay) @@ -166,61 +100,10 @@ namespace RedBookPlayer.Models.Hardware 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.SetVolume((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 /// @@ -265,108 +148,25 @@ namespace RedBookPlayer.Models.Hardware #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) + public void SetVolume(int volume) { - // 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; + Volume = volume; + _soundOut.SetVolume((float)Volume / 100); } /// /// Sets or resets the audio playback objects /// - private void SetupAudio() + /// ReadFunction to use during decoding + private void SetupAudio(PlayerSource.ReadFunction read) { if(_source == null) { - _source = new PlayerSource(ProviderRead); - + _source = new PlayerSource(read); if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) _soundOut = new Linux.AudioBackend(_source); else if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) diff --git a/RedBookPlayer.Models/Hardware/Windows/AudioBackend.cs b/RedBookPlayer.Models/Hardware/Windows/AudioBackend.cs index d378429..c55b0ff 100644 --- a/RedBookPlayer.Models/Hardware/Windows/AudioBackend.cs +++ b/RedBookPlayer.Models/Hardware/Windows/AudioBackend.cs @@ -7,7 +7,7 @@ namespace RedBookPlayer.Models.Hardware.Windows /// /// Sound output instance /// - private ALSoundOut _soundOut; + private readonly ALSoundOut _soundOut; public AudioBackend() { }