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