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
/// 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() { }