mirror of
https://github.com/aaru-dps/RedBookPlayer.git
synced 2025-12-16 19:24:41 +00:00
Merge pull request #51 from mnadareski/backend-separation
Multi-Disc and Multi-Backend Improvements
This commit is contained in:
10
README.md
10
README.md
@@ -16,8 +16,11 @@
|
|||||||
| **Space** | Toggle Play / Pause |
|
| **Space** | Toggle Play / Pause |
|
||||||
| **Esc** | Stop Playback |
|
| **Esc** | Stop Playback |
|
||||||
| **~** | Eject |
|
| **~** | Eject |
|
||||||
|
| **Page Up** | Next Disc |
|
||||||
|
| **Page Down** | Previous Disc |
|
||||||
| **→** | Next Track |
|
| **→** | Next Track |
|
||||||
| **←** | Previous Track |
|
| **←** | Previous Track |
|
||||||
|
| **R** | Shuffle Tracks |
|
||||||
| **]** | Next Index |
|
| **]** | Next Index |
|
||||||
| **[** | Previous Index |
|
| **[** | Previous Index |
|
||||||
| **.** | Fast Forward |
|
| **.** | Fast Forward |
|
||||||
@@ -31,6 +34,13 @@ For Save Track(s):
|
|||||||
- Holding no modifying keys will prompt to save the current track
|
- Holding no modifying keys will prompt to save the current track
|
||||||
- Holding **Shift** will prompt to save all tracks (including hidden)
|
- 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:
|
For both Volume Up and Volume Down:
|
||||||
- Holding **Ctrl** will move in increments of 2
|
- Holding **Ctrl** will move in increments of 2
|
||||||
- Holding **Shift** will move in increments of 5
|
- Holding **Shift** will move in increments of 5
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
<PropertyGroup Condition="'$(RuntimeIdentifier)|$(Configuration)' == 'win-x64|Debug'">
|
<PropertyGroup Condition="'$(RuntimeIdentifier)|$(Configuration)' == 'win-x64|Debug'">
|
||||||
<DefineConstants>WindowsDebug</DefineConstants>
|
<DefineConstants>WindowsDebug</DefineConstants>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Update="**\*.xaml.cs">
|
<Compile Update="**\*.xaml.cs">
|
||||||
<DependentUpon>%(Filename)</DependentUpon>
|
<DependentUpon>%(Filename)</DependentUpon>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using RedBookPlayer.GUI.Views;
|
using RedBookPlayer.GUI.Views;
|
||||||
@@ -75,6 +76,18 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
PlayerView?.ViewModel?.ExecuteEject();
|
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
|
// Next Track
|
||||||
else if(e.Key == App.Settings.NextTrackKey || e.Key == Key.MediaNextTrack)
|
else if(e.Key == App.Settings.NextTrackKey || e.Key == Key.MediaNextTrack)
|
||||||
{
|
{
|
||||||
@@ -87,6 +100,12 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
PlayerView?.ViewModel?.ExecutePreviousTrack();
|
PlayerView?.ViewModel?.ExecutePreviousTrack();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shuffle Track List
|
||||||
|
else if(e.Key == App.Settings.ShuffleTracksKey)
|
||||||
|
{
|
||||||
|
PlayerView?.ViewModel?.ExecuteShuffle();
|
||||||
|
}
|
||||||
|
|
||||||
// Next Index
|
// Next Index
|
||||||
else if(e.Key == App.Settings.NextIndexKey)
|
else if(e.Key == App.Settings.NextIndexKey)
|
||||||
{
|
{
|
||||||
@@ -112,7 +131,7 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Volume Up
|
// Volume Up
|
||||||
else if(e.Key == App.Settings.VolumeUpKey || e.Key == Key.VolumeUp)
|
else if(e.Key == App.Settings.VolumeUpKey)
|
||||||
{
|
{
|
||||||
int increment = 1;
|
int increment = 1;
|
||||||
if(e.KeyModifiers.HasFlag(KeyModifiers.Control))
|
if(e.KeyModifiers.HasFlag(KeyModifiers.Control))
|
||||||
@@ -125,7 +144,7 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Volume Down
|
// Volume Down
|
||||||
else if(e.Key == App.Settings.VolumeDownKey || e.Key == Key.VolumeDown)
|
else if(e.Key == App.Settings.VolumeDownKey)
|
||||||
{
|
{
|
||||||
int decrement = 1;
|
int decrement = 1;
|
||||||
if(e.KeyModifiers.HasFlag(KeyModifiers.Control))
|
if(e.KeyModifiers.HasFlag(KeyModifiers.Control))
|
||||||
@@ -138,7 +157,7 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Mute Toggle
|
// Mute Toggle
|
||||||
else if(e.Key == App.Settings.ToggleMuteKey || e.Key == Key.VolumeMute)
|
else if(e.Key == App.Settings.ToggleMuteKey)
|
||||||
{
|
{
|
||||||
PlayerView?.ViewModel?.ExecuteToggleMute();
|
PlayerView?.ViewModel?.ExecuteToggleMute();
|
||||||
}
|
}
|
||||||
@@ -151,19 +170,35 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Load the first valid drag-and-dropped disc image
|
/// Load the all valid drag-and-dropped disc images
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>If more than the number of discs in the changer are added, it will begin to overwrite</remarks>
|
||||||
public async void ExecuteLoadDragDrop(object sender, DragEventArgs e)
|
public async void ExecuteLoadDragDrop(object sender, DragEventArgs e)
|
||||||
{
|
{
|
||||||
if(PlayerView?.ViewModel == null)
|
if(PlayerView?.ViewModel == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
IEnumerable<string> fileNames = e.Data.GetFileNames();
|
IEnumerable<string> fileNames = e.Data.GetFileNames();
|
||||||
foreach(string filename in fileNames)
|
if(fileNames == null || fileNames.Count() == 0)
|
||||||
{
|
{
|
||||||
bool loaded = await PlayerView.ViewModel.LoadImage(filename);
|
return;
|
||||||
if(loaded)
|
}
|
||||||
break;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,8 +31,28 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
|
|
||||||
#region Player Passthrough
|
#region Player Passthrough
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Currently selected disc
|
||||||
|
/// </summary>
|
||||||
|
public int CurrentDisc
|
||||||
|
{
|
||||||
|
get => _currentDisc;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _currentDisc, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _currentDisc;
|
||||||
|
|
||||||
#region OpticalDisc Passthrough
|
#region OpticalDisc Passthrough
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Path to the disc image
|
||||||
|
/// </summary>
|
||||||
|
public string ImagePath
|
||||||
|
{
|
||||||
|
get => _imagePath;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _imagePath, value);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Current track number
|
/// Current track number
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -148,6 +168,7 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public ulong TotalTime => _player.TotalTime;
|
public ulong TotalTime => _player.TotalTime;
|
||||||
|
|
||||||
|
private string _imagePath;
|
||||||
private int _currentTrackNumber;
|
private int _currentTrackNumber;
|
||||||
private ushort _currentTrackIndex;
|
private ushort _currentTrackIndex;
|
||||||
private ushort _currentTrackSession;
|
private ushort _currentTrackSession;
|
||||||
@@ -263,6 +284,16 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public ReactiveCommand<Unit, Unit> EjectCommand { get; }
|
public ReactiveCommand<Unit, Unit> EjectCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command for moving to the next disc
|
||||||
|
/// </summary>
|
||||||
|
public ReactiveCommand<Unit, Unit> NextDiscCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command for moving to the previous disc
|
||||||
|
/// </summary>
|
||||||
|
public ReactiveCommand<Unit, Unit> PreviousDiscCommand { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Command for moving to the next track
|
/// Command for moving to the next track
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -348,6 +379,8 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
TogglePlayPauseCommand = ReactiveCommand.Create(ExecuteTogglePlayPause);
|
TogglePlayPauseCommand = ReactiveCommand.Create(ExecuteTogglePlayPause);
|
||||||
StopCommand = ReactiveCommand.Create(ExecuteStop);
|
StopCommand = ReactiveCommand.Create(ExecuteStop);
|
||||||
EjectCommand = ReactiveCommand.Create(ExecuteEject);
|
EjectCommand = ReactiveCommand.Create(ExecuteEject);
|
||||||
|
NextDiscCommand = ReactiveCommand.Create(ExecuteNextDisc);
|
||||||
|
PreviousDiscCommand = ReactiveCommand.Create(ExecutePreviousDisc);
|
||||||
NextTrackCommand = ReactiveCommand.Create(ExecuteNextTrack);
|
NextTrackCommand = ReactiveCommand.Create(ExecuteNextTrack);
|
||||||
PreviousTrackCommand = ReactiveCommand.Create(ExecutePreviousTrack);
|
PreviousTrackCommand = ReactiveCommand.Create(ExecutePreviousTrack);
|
||||||
NextIndexCommand = ReactiveCommand.Create(ExecuteNextIndex);
|
NextIndexCommand = ReactiveCommand.Create(ExecuteNextIndex);
|
||||||
@@ -364,7 +397,9 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
ToggleDeEmphasisCommand = ReactiveCommand.Create(ExecuteToggleDeEmphasis);
|
ToggleDeEmphasisCommand = ReactiveCommand.Create(ExecuteToggleDeEmphasis);
|
||||||
|
|
||||||
// Initialize Player
|
// 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;
|
PlayerState = PlayerState.NoDisc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,25 +407,22 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
/// Initialize the view model with a given image path
|
/// Initialize the view model with a given image path
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">Path to the disc image</param>
|
/// <param name="path">Path to the disc image</param>
|
||||||
/// <param name="options">Options to pass to the optical disc factory</param>
|
/// <param name="playerOptions">Options to pass to the player</param>
|
||||||
/// <param name="repeatMode">RepeatMode for sound output</param>
|
/// <param name="opticalDiscOptions">Options to pass to the optical disc factory</param>
|
||||||
/// <param name="autoPlay">True if playback should begin immediately, false otherwise</param>
|
/// <param name="autoPlay">True if playback should begin immediately, false otherwise</param>
|
||||||
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
|
// Stop current playback, if necessary
|
||||||
if(PlayerState != PlayerState.NoDisc)
|
if(PlayerState != PlayerState.NoDisc)
|
||||||
ExecuteStop();
|
ExecuteStop();
|
||||||
|
|
||||||
// Attempt to initialize Player
|
// Attempt to initialize Player
|
||||||
_player.Init(path, options, repeatMode, autoPlay);
|
_player.Init(path, playerOptions, opticalDiscOptions, autoPlay);
|
||||||
if(_player.Initialized)
|
if(_player.Initialized)
|
||||||
{
|
|
||||||
_player.PropertyChanged += PlayerStateChanged;
|
|
||||||
PlayerStateChanged(this, null);
|
PlayerStateChanged(this, null);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Playback
|
#region Playback (UI)
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Begin playback
|
/// Begin playback
|
||||||
@@ -407,6 +439,11 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void ExecuteTogglePlayPause() => _player?.TogglePlayback();
|
public void ExecuteTogglePlayPause() => _player?.TogglePlayback();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shuffle the current track list
|
||||||
|
/// </summary>
|
||||||
|
public void ExecuteShuffle() => _player?.ShuffleTracks();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stop current playback
|
/// Stop current playback
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -417,6 +454,16 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void ExecuteEject() => _player?.Eject();
|
public void ExecuteEject() => _player?.Eject();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Move to the next disc
|
||||||
|
/// </summary>
|
||||||
|
public void ExecuteNextDisc() => _player?.NextDisc();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Move to the previous disc
|
||||||
|
/// </summary>
|
||||||
|
public void ExecutePreviousDisc() => _player?.PreviousDisc();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Move to the next playable track
|
/// Move to the next playable track
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -449,6 +496,29 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Playback (Internal)
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Select a particular disc by number
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="discNumber">Disc number to attempt to load</param>
|
||||||
|
public void SelectDisc(int discNumber) => _player?.SelectDisc(discNumber);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Select a particular index by number
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="index">Track index to attempt to load</param>
|
||||||
|
/// <param name="changeTrack">True if index changes can trigger a track change, false otherwise</param>
|
||||||
|
public void SelectIndex(ushort index, bool changeTrack) => _player?.SelectIndex(index, changeTrack);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Select a particular track by number
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="trackNumber">Track number to attempt to load</param>
|
||||||
|
public void SelectTrack(int trackNumber) => _player?.SelectTrack(trackNumber);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region Volume
|
#region Volume
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -493,6 +563,111 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Extraction
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extract a single track from the image to WAV
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="trackNumber"></param>
|
||||||
|
/// <param name="outputDirectory">Output path to write data to</param>
|
||||||
|
public void ExtractSingleTrackToWav(uint trackNumber, string outputDirectory) => _player?.ExtractSingleTrackToWav(trackNumber, outputDirectory);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extract all tracks from the image to WAV
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="outputDirectory">Output path to write data to</param>
|
||||||
|
public void ExtractAllTracksToWav(string outputDirectory) => _player?.ExtractAllTracksToWav(outputDirectory);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Setters
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set data playback method [CompactDisc only]
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dataPlayback">New playback value</param>
|
||||||
|
public void SetDataPlayback(DataPlayback dataPlayback) => _player?.SetDataPlayback(dataPlayback);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set disc handling method
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="discHandling">New playback value</param>
|
||||||
|
public void SetDiscHandling(DiscHandling discHandling) => _player?.SetDiscHandling(discHandling);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the value for loading hidden tracks [CompactDisc only]
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="load">True to enable loading hidden tracks, false otherwise</param>
|
||||||
|
public void SetLoadHiddenTracks(bool load) => _player?.SetLoadHiddenTracks(load);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set repeat mode
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="repeatMode">New repeat mode value</param>
|
||||||
|
public void SetRepeatMode(RepeatMode repeatMode) => _player?.SetRepeatMode(repeatMode);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set session handling
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sessionHandling">New session handling value</param>
|
||||||
|
public void SetSessionHandling(SessionHandling sessionHandling) => _player?.SetSessionHandling(sessionHandling);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region State Change Event Handlers
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update the view-model from the Player
|
||||||
|
/// </summary>
|
||||||
|
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
|
#region Helpers
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -540,11 +715,28 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public async void ExecuteLoad()
|
public async void ExecuteLoad()
|
||||||
{
|
{
|
||||||
string path = await GetPath();
|
string[] paths = await GetPaths();
|
||||||
if(path == null)
|
if(paths == null || paths.Length == 0)
|
||||||
|
{
|
||||||
return;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -593,19 +785,25 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
{
|
{
|
||||||
return await Dispatcher.UIThread.InvokeAsync(() =>
|
return await Dispatcher.UIThread.InvokeAsync(() =>
|
||||||
{
|
{
|
||||||
OpticalDiscOptions options = new OpticalDiscOptions
|
PlayerOptions playerOptions = new PlayerOptions
|
||||||
{
|
{
|
||||||
DataPlayback = App.Settings.DataPlayback,
|
DataPlayback = App.Settings.DataPlayback,
|
||||||
GenerateMissingToc = App.Settings.GenerateMissingTOC,
|
DiscHandling = App.Settings.DiscHandling,
|
||||||
LoadHiddenTracks = App.Settings.PlayHiddenTracks,
|
LoadHiddenTracks = App.Settings.PlayHiddenTracks,
|
||||||
|
RepeatMode = App.Settings.RepeatMode,
|
||||||
SessionHandling = App.Settings.SessionHandling,
|
SessionHandling = App.Settings.SessionHandling,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
OpticalDiscOptions opticalDiscOptions = new OpticalDiscOptions
|
||||||
|
{
|
||||||
|
GenerateMissingToc = App.Settings.GenerateMissingTOC,
|
||||||
|
};
|
||||||
|
|
||||||
// Ensure the context and view model are set
|
// Ensure the context and view model are set
|
||||||
App.PlayerView.DataContext = this;
|
App.PlayerView.DataContext = this;
|
||||||
App.PlayerView.ViewModel = this;
|
App.PlayerView.ViewModel = this;
|
||||||
|
|
||||||
Init(path, options, App.Settings.RepeatMode, App.Settings.AutoPlay);
|
Init(path, playerOptions, opticalDiscOptions, App.Settings.AutoPlay);
|
||||||
if(Initialized)
|
if(Initialized)
|
||||||
App.MainWindow.Title = "RedBookPlayer - " + path.Split('/').Last().Split('\\').Last();
|
App.MainWindow.Title = "RedBookPlayer - " + path.Split('/').Last().Split('\\').Last();
|
||||||
|
|
||||||
@@ -619,48 +817,12 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
public void RefreshFromSettings()
|
public void RefreshFromSettings()
|
||||||
{
|
{
|
||||||
SetDataPlayback(App.Settings.DataPlayback);
|
SetDataPlayback(App.Settings.DataPlayback);
|
||||||
|
SetDiscHandling(App.Settings.DiscHandling);
|
||||||
SetLoadHiddenTracks(App.Settings.PlayHiddenTracks);
|
SetLoadHiddenTracks(App.Settings.PlayHiddenTracks);
|
||||||
SetRepeatMode(App.Settings.RepeatMode);
|
SetRepeatMode(App.Settings.RepeatMode);
|
||||||
SetSessionHandling(App.Settings.SessionHandling);
|
SetSessionHandling(App.Settings.SessionHandling);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Extract a single track from the image to WAV
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="trackNumber"></param>
|
|
||||||
/// <param name="outputDirectory">Output path to write data to</param>
|
|
||||||
public void ExtractSingleTrackToWav(uint trackNumber, string outputDirectory) => _player?.ExtractSingleTrackToWav(trackNumber, outputDirectory);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Extract all tracks from the image to WAV
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="outputDirectory">Output path to write data to</param>
|
|
||||||
public void ExtractAllTracksToWav(string outputDirectory) => _player?.ExtractAllTracksToWav(outputDirectory);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set data playback method [CompactDisc only]
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dataPlayback">New playback value</param>
|
|
||||||
public void SetDataPlayback(DataPlayback dataPlayback) => _player?.SetDataPlayback(dataPlayback);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set the value for loading hidden tracks [CompactDisc only]
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="load">True to enable loading hidden tracks, false otherwise</param>
|
|
||||||
public void SetLoadHiddenTracks(bool load) => _player?.SetLoadHiddenTracks(load);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set repeat mode
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="repeatMode">New repeat mode value</param>
|
|
||||||
public void SetRepeatMode(RepeatMode repeatMode) => _player?.SetRepeatMode(repeatMode);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set session handling
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="sessionHandling">New session handling value</param>
|
|
||||||
public void SetSessionHandling(SessionHandling sessionHandling) => _player?.SetSessionHandling(sessionHandling);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generate the digit string to be interpreted by the frontend
|
/// Generate the digit string to be interpreted by the frontend
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -737,12 +899,12 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generate a path selection dialog box
|
/// Generate a path selection dialog box
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>User-selected path, if possible</returns>
|
/// <returns>User-selected paths, if possible</returns>
|
||||||
private async Task<string> GetPath()
|
private async Task<string[]> GetPaths()
|
||||||
{
|
{
|
||||||
return await Dispatcher.UIThread.InvokeAsync(async () =>
|
return await Dispatcher.UIThread.InvokeAsync(async () =>
|
||||||
{
|
{
|
||||||
var dialog = new OpenFileDialog { AllowMultiple = false };
|
var dialog = new OpenFileDialog { AllowMultiple = true };
|
||||||
List<string> knownExtensions = new Aaru.DiscImages.AaruFormat().KnownExtensions.ToList();
|
List<string> knownExtensions = new Aaru.DiscImages.AaruFormat().KnownExtensions.ToList();
|
||||||
dialog.Filters.Add(new FileDialogFilter()
|
dialog.Filters.Add(new FileDialogFilter()
|
||||||
{
|
{
|
||||||
@@ -750,7 +912,7 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
Extensions = knownExtensions.ConvertAll(e => e.TrimStart('.'))
|
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();
|
UpdateDigits();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Update the view-model from the Player
|
|
||||||
/// </summary>
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update UI
|
/// Update UI
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public List<DataPlayback> DataPlaybackValues => GenerateDataPlaybackList();
|
public List<DataPlayback> DataPlaybackValues => GenerateDataPlaybackList();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// List of all disc handling values
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public List<DiscHandling> DiscHandlingValues => GenerateDiscHandlingList();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// List of all repeat mode values
|
/// List of all repeat mode values
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -44,6 +50,16 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool AutoPlay { get; set; } = false;
|
public bool AutoPlay { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates the number of discs to allow loading and changing
|
||||||
|
/// </summary>
|
||||||
|
public int NumberOfDiscs { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates how to deal with multiple discs
|
||||||
|
/// </summary>
|
||||||
|
public DiscHandling DiscHandling { get; set; } = DiscHandling.SingleDisc;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates if an index change can trigger a track change
|
/// Indicates if an index change can trigger a track change
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -144,6 +160,16 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Key EjectKey { get; set; } = Key.OemTilde;
|
public Key EjectKey { get; set; } = Key.OemTilde;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Key assigned to move to the next disc
|
||||||
|
/// </summary>
|
||||||
|
public Key NextDiscKey { get; set; } = Key.PageUp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Key assigned to move to the previous disc
|
||||||
|
/// </summary>
|
||||||
|
public Key PreviousDiscKey { get; set; } = Key.PageDown;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Key assigned to move to the next track
|
/// Key assigned to move to the next track
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -154,6 +180,11 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Key PreviousTrackKey { get; set; } = Key.Left;
|
public Key PreviousTrackKey { get; set; } = Key.Left;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Key assigned to shuffling the track list
|
||||||
|
/// </summary>
|
||||||
|
public Key ShuffleTracksKey { get; set; } = Key.R;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Key assigned to move to the next index
|
/// Key assigned to move to the next index
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -275,6 +306,11 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private List<DataPlayback> GenerateDataPlaybackList() => Enum.GetValues(typeof(DataPlayback)).Cast<DataPlayback>().ToList();
|
private List<DataPlayback> GenerateDataPlaybackList() => Enum.GetValues(typeof(DataPlayback)).Cast<DataPlayback>().ToList();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate the list of DiscHandling values
|
||||||
|
/// </summary>
|
||||||
|
private List<DiscHandling> GenerateDiscHandlingList() => Enum.GetValues(typeof(DiscHandling)).Cast<DiscHandling>().ToList();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generate the list of Key values
|
/// Generate the list of Key values
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -8,19 +8,19 @@
|
|||||||
<viewModels:PlayerViewModel/>
|
<viewModels:PlayerViewModel/>
|
||||||
</ReactiveUserControl.ViewModel>
|
</ReactiveUserControl.ViewModel>
|
||||||
<StackPanel Margin="16" VerticalAlignment="Center">
|
<StackPanel Margin="16" VerticalAlignment="Center">
|
||||||
<Button Command="{Binding LoadCommand}" Margin="32,0,32,16">Load</Button>
|
<Button Command="{Binding LoadCommand}" Focusable="False" Margin="32,0,32,16">Load</Button>
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
||||||
<Button Command="{Binding PlayCommand}" Width="100" Margin="0,0,16,0">Play</Button>
|
<Button Command="{Binding PlayCommand}" Focusable="False" Width="100" Margin="0,0,16,0">Play</Button>
|
||||||
<Button Command="{Binding PauseCommand}" Width="100" Margin="0,0,16,0">Pause</Button>
|
<Button Command="{Binding PauseCommand}" Focusable="False" Width="100" Margin="0,0,16,0">Pause</Button>
|
||||||
<Button Command="{Binding StopCommand}" Width="100" Margin="0,0,16,0">Stop</Button>
|
<Button Command="{Binding StopCommand}" Focusable="False" Width="100" Margin="0,0,16,0">Stop</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
||||||
<Button Command="{Binding PreviousTrackCommand}" Width="100" Margin="0,0,16,0">Previous Track</Button>
|
<Button Command="{Binding PreviousTrackCommand}" Focusable="False" Width="100" Margin="0,0,16,0">Previous Track</Button>
|
||||||
<Button Command="{Binding NextTrackCommand}" Width="100" Margin="0,0,16,0">Next Track</Button>
|
<Button Command="{Binding NextTrackCommand}" Focusable="False" Width="100" Margin="0,0,16,0">Next Track</Button>
|
||||||
<Button Command="{Binding PreviousIndexCommand}" Width="100" Margin="0,0,16,0">Previous Index</Button>
|
<Button Command="{Binding PreviousIndexCommand}" Focusable="False" Width="100" Margin="0,0,16,0">Previous Index</Button>
|
||||||
<Button Command="{Binding NextIndexCommand}" Width="100" Margin="0,0,16,0">Next Index</Button>
|
<Button Command="{Binding NextIndexCommand}" Focusable="False" Width="100" Margin="0,0,16,0">Next Index</Button>
|
||||||
<RepeatButton Command="{Binding RewindCommand}" Width="100" Margin="0,0,16,0">Rewind</RepeatButton>
|
<RepeatButton Command="{Binding RewindCommand}" Focusable="False" Width="100" Margin="0,0,16,0">Rewind</RepeatButton>
|
||||||
<RepeatButton Command="{Binding FastForwardCommand}" Width="100">Fast Forward</RepeatButton>
|
<RepeatButton Command="{Binding FastForwardCommand}" Focusable="False" Width="100">Fast Forward</RepeatButton>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
||||||
<StackPanel Margin="0,0,32,0">
|
<StackPanel Margin="0,0,32,0">
|
||||||
@@ -81,12 +81,12 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
||||||
<Button Command="{Binding EnableDeEmphasisCommand}" IsVisible="{Binding !ApplyDeEmphasis}" Width="200"
|
<Button Command="{Binding EnableDeEmphasisCommand}" Focusable="False" IsVisible="{Binding !ApplyDeEmphasis}"
|
||||||
Margin="0,0,16,0">
|
Width="200" Margin="0,0,16,0">
|
||||||
Enable De-Emphasis
|
Enable De-Emphasis
|
||||||
</Button>
|
</Button>
|
||||||
<Button Command="{Binding DisableDeEmphasisCommand}" IsVisible="{Binding ApplyDeEmphasis}" Width="200"
|
<Button Command="{Binding DisableDeEmphasisCommand}" Focusable="False" IsVisible="{Binding ApplyDeEmphasis}"
|
||||||
Margin="0,0,16,0">
|
Width="200" Margin="0,0,16,0">
|
||||||
Disable De-Emphasis
|
Disable De-Emphasis
|
||||||
</Button>
|
</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -103,7 +103,8 @@
|
|||||||
<TextBlock Margin="0,0,16,0" IsVisible="{Binding QuadChannel}">4CH</TextBlock>
|
<TextBlock Margin="0,0,16,0" IsVisible="{Binding QuadChannel}">4CH</TextBlock>
|
||||||
<TextBlock Margin="0,0,16,0" Foreground="LightGray" IsVisible="{Binding !HiddenTrack}">HIDDEN</TextBlock>
|
<TextBlock Margin="0,0,16,0" Foreground="LightGray" IsVisible="{Binding !HiddenTrack}">HIDDEN</TextBlock>
|
||||||
<TextBlock Margin="0,0,16,0" IsVisible="{Binding HiddenTrack}">HIDDEN</TextBlock>
|
<TextBlock Margin="0,0,16,0" IsVisible="{Binding HiddenTrack}">HIDDEN</TextBlock>
|
||||||
<TextBlock Margin="0,0,16,0" Text="{Binding Volume}"/>
|
<TextBlock Margin="0,0,16,0" Text="{Binding Volume, StringFormat='Volume {0}%'}"/>
|
||||||
|
<TextBlock Margin="0,0,16,0" Text="{Binding CurrentDisc, StringFormat='Disc Number: {0}'}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ReactiveUserControl>
|
</ReactiveUserControl>
|
||||||
@@ -26,20 +26,30 @@
|
|||||||
<TextBlock VerticalAlignment="Center">Play hidden tracks</TextBlock>
|
<TextBlock VerticalAlignment="Center">Play hidden tracks</TextBlock>
|
||||||
</WrapPanel>
|
</WrapPanel>
|
||||||
<WrapPanel Margin="0,0,0,16">
|
<WrapPanel Margin="0,0,0,16">
|
||||||
<TextBlock Grid.Row="0" Grid.Column="0" Width="120">Data Track Playback</TextBlock>
|
<TextBlock Width="120">Data Track Playback</TextBlock>
|
||||||
<ComboBox Grid.Row="0" Grid.Column="1" Name="DataPlayback" Margin="8,0,0,0" Width="120"
|
<ComboBox Name="DataPlayback" Margin="8,0,0,0" Width="120"
|
||||||
Items="{Binding DataPlaybackValues}" SelectedItem="{Binding DataPlayback, Mode=TwoWay}" />
|
Items="{Binding DataPlaybackValues}" SelectedItem="{Binding DataPlayback, Mode=TwoWay}" />
|
||||||
</WrapPanel>
|
</WrapPanel>
|
||||||
<WrapPanel Margin="0,0,0,16">
|
<WrapPanel Margin="0,0,0,16">
|
||||||
<TextBlock Grid.Row="0" Grid.Column="0" Width="120">Session Handling</TextBlock>
|
<TextBlock Width="120">Session Handling</TextBlock>
|
||||||
<ComboBox Grid.Row="0" Grid.Column="1" Name="SessionHandling" Margin="8,0,0,0" Width="120"
|
<ComboBox Name="SessionHandling" Margin="8,0,0,0" Width="120"
|
||||||
Items="{Binding SessionHandlingValues}" SelectedItem="{Binding SessionHandling, Mode=TwoWay}" />
|
Items="{Binding SessionHandlingValues}" SelectedItem="{Binding SessionHandling, Mode=TwoWay}" />
|
||||||
</WrapPanel>
|
</WrapPanel>
|
||||||
<WrapPanel Margin="0,0,0,16">
|
<WrapPanel Margin="0,0,0,16">
|
||||||
<TextBlock Grid.Row="0" Grid.Column="0" Width="120">Repeat Mode</TextBlock>
|
<TextBlock Width="120">Repeat Mode</TextBlock>
|
||||||
<ComboBox Grid.Row="0" Grid.Column="1" Name="RepeatMode" Margin="8,0,0,0" Width="120"
|
<ComboBox Name="RepeatMode" Margin="8,0,0,0" Width="120"
|
||||||
Items="{Binding RepeatModeValues}" SelectedItem="{Binding RepeatMode, Mode=TwoWay}" />
|
Items="{Binding RepeatModeValues}" SelectedItem="{Binding RepeatMode, Mode=TwoWay}" />
|
||||||
</WrapPanel>
|
</WrapPanel>
|
||||||
|
<WrapPanel Margin="0,0,0,16">
|
||||||
|
<TextBlock Width="120" VerticalAlignment="Center">Discs in Changer</TextBlock>
|
||||||
|
<NumericUpDown Name="NumberOfDiscs" Margin="8,0,0,0" Width="120"
|
||||||
|
Value="{Binding NumberOfDiscs, Mode=TwoWay}" Minimum="1" Maximum="100" />
|
||||||
|
</WrapPanel>
|
||||||
|
<WrapPanel Margin="0,0,0,16">
|
||||||
|
<TextBlock Width="120">Disc Handling</TextBlock>
|
||||||
|
<ComboBox Name="DiscHandling" Margin="8,0,0,0" Width="120"
|
||||||
|
Items="{Binding DiscHandlingValues}" SelectedItem="{Binding DiscHandling, Mode=TwoWay}" />
|
||||||
|
</WrapPanel>
|
||||||
<WrapPanel Margin="0,0,0,16">
|
<WrapPanel Margin="0,0,0,16">
|
||||||
<CheckBox IsChecked="{Binding GenerateMissingTOC}" Margin="0,0,8,0"/>
|
<CheckBox IsChecked="{Binding GenerateMissingTOC}" Margin="0,0,8,0"/>
|
||||||
<TextBlock VerticalAlignment="Center">Generate a TOC if the disc is missing one</TextBlock>
|
<TextBlock VerticalAlignment="Center">Generate a TOC if the disc is missing one</TextBlock>
|
||||||
@@ -58,120 +68,152 @@
|
|||||||
</DockPanel>
|
</DockPanel>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem Header="Keyboard Bindings">
|
<TabItem Header="Keyboard Bindings">
|
||||||
<Grid Margin="16">
|
<StackPanel Margin="16">
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition/>
|
|
||||||
<ColumnDefinition/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition/>
|
|
||||||
<RowDefinition/>
|
|
||||||
<RowDefinition/>
|
|
||||||
<RowDefinition/>
|
|
||||||
<RowDefinition/>
|
|
||||||
<RowDefinition/>
|
|
||||||
<RowDefinition/>
|
|
||||||
<RowDefinition/>
|
|
||||||
<RowDefinition/>
|
|
||||||
<RowDefinition/>
|
|
||||||
<RowDefinition/>
|
|
||||||
<RowDefinition/>
|
|
||||||
<RowDefinition/>
|
|
||||||
<RowDefinition/>
|
|
||||||
<RowDefinition/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<!-- Load Image-->
|
<!-- Load Image-->
|
||||||
<TextBlock Grid.Row="0" Grid.Column="0" Width="120">Load Image</TextBlock>
|
<WrapPanel Margin="0,0,0,16">
|
||||||
<ComboBox Grid.Row="0" Grid.Column="1" Name="LoadImageKeyBind"
|
<TextBlock Width="120">Load Image</TextBlock>
|
||||||
Items="{Binding KeyboardList}" SelectedItem="{Binding LoadImageKey, Mode=TwoWay}"
|
<ComboBox Name="LoadImageKeyBind"
|
||||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
Items="{Binding KeyboardList}" SelectedItem="{Binding LoadImageKey, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||||
|
</WrapPanel>
|
||||||
|
|
||||||
<!-- Save Track -->
|
<!-- Save Track -->
|
||||||
<TextBlock Grid.Row="1" Grid.Column="0" Width="120">Save Track(s)</TextBlock>
|
<WrapPanel Margin="0,0,0,16">
|
||||||
<ComboBox Grid.Row="1" Grid.Column="1" Name="SaveTrackKeyBind"
|
<TextBlock Width="120">Save Track(s)</TextBlock>
|
||||||
Items="{Binding KeyboardList}" SelectedItem="{Binding SaveTrackKey, Mode=TwoWay}"
|
<ComboBox Name="SaveTrackKeyBind"
|
||||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
Items="{Binding KeyboardList}" SelectedItem="{Binding SaveTrackKey, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||||
|
</WrapPanel>
|
||||||
|
|
||||||
<!-- Toggle Play/Pause -->
|
<!-- Toggle Play/Pause -->
|
||||||
<TextBlock Grid.Row="2" Grid.Column="0" Width="120">Toggle Play/Pause</TextBlock>
|
<WrapPanel Margin="0,0,0,16">
|
||||||
<ComboBox Grid.Row="2" Grid.Column="1" Name="TogglePlaybackKeyBind"
|
<TextBlock Width="120">Toggle Play/Pause</TextBlock>
|
||||||
Items="{Binding KeyboardList}" SelectedItem="{Binding TogglePlaybackKey, Mode=TwoWay}"
|
<ComboBox Name="TogglePlaybackKeyBind"
|
||||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
Items="{Binding KeyboardList}" SelectedItem="{Binding TogglePlaybackKey, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||||
|
</WrapPanel>
|
||||||
|
|
||||||
<!-- Stop Playback-->
|
<!-- Stop Playback-->
|
||||||
<TextBlock Grid.Row="3" Grid.Column="0" Width="120">Stop Playback</TextBlock>
|
<WrapPanel Margin="0,0,0,16">
|
||||||
<ComboBox Grid.Row="3" Grid.Column="1" Name="StopPlaybackKeyBind"
|
<TextBlock Width="120">Stop Playback</TextBlock>
|
||||||
Items="{Binding KeyboardList}" SelectedItem="{Binding StopPlaybackKey, Mode=TwoWay}"
|
<ComboBox Name="StopPlaybackKeyBind"
|
||||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
Items="{Binding KeyboardList}" SelectedItem="{Binding StopPlaybackKey, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||||
|
</WrapPanel>
|
||||||
|
|
||||||
<!-- Eject Disc-->
|
<!-- Eject Disc-->
|
||||||
<TextBlock Grid.Row="4" Grid.Column="0" Width="120">Eject Disc</TextBlock>
|
<WrapPanel Margin="0,0,0,16">
|
||||||
<ComboBox Grid.Row="4" Grid.Column="1" Name="EjectKeyBind"
|
<TextBlock Width="120">Eject Disc</TextBlock>
|
||||||
Items="{Binding KeyboardList}" SelectedItem="{Binding EjectKey, Mode=TwoWay}"
|
<ComboBox Name="EjectKeyBind"
|
||||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
Items="{Binding KeyboardList}" SelectedItem="{Binding EjectKey, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||||
|
</WrapPanel>
|
||||||
|
|
||||||
|
<!-- Next Disc -->
|
||||||
|
<WrapPanel Margin="0,0,0,16">
|
||||||
|
<TextBlock Width="120">Next Disc</TextBlock>
|
||||||
|
<ComboBox Name="NextDiscKeyBind"
|
||||||
|
Items="{Binding KeyboardList}" SelectedItem="{Binding NextDiscKey, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||||
|
</WrapPanel>
|
||||||
|
|
||||||
|
<!-- Previous Disc -->
|
||||||
|
<WrapPanel Margin="0,0,0,16">
|
||||||
|
<TextBlock Width="120">Previous Disc</TextBlock>
|
||||||
|
<ComboBox Name="PreviousDiscKeyBind"
|
||||||
|
Items="{Binding KeyboardList}" SelectedItem="{Binding PreviousDiscKey, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||||
|
</WrapPanel>
|
||||||
|
|
||||||
<!-- Next Track -->
|
<!-- Next Track -->
|
||||||
<TextBlock Grid.Row="5" Grid.Column="0" Width="120">Next Track</TextBlock>
|
<WrapPanel Margin="0,0,0,16">
|
||||||
<ComboBox Grid.Row="5" Grid.Column="1" Name="NextTrackKeyBind"
|
<TextBlock Width="120">Next Track</TextBlock>
|
||||||
Items="{Binding KeyboardList}" SelectedItem="{Binding NextTrackKey, Mode=TwoWay}"
|
<ComboBox Name="NextTrackKeyBind"
|
||||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
Items="{Binding KeyboardList}" SelectedItem="{Binding NextTrackKey, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||||
|
</WrapPanel>
|
||||||
|
|
||||||
<!-- Previous Track -->
|
<!-- Previous Track -->
|
||||||
<TextBlock Grid.Row="6" Grid.Column="0" Width="120">Previous Track</TextBlock>
|
<WrapPanel Margin="0,0,0,16">
|
||||||
<ComboBox Grid.Row="6" Grid.Column="1" Name="PreviousTrackKeyBind"
|
<TextBlock Width="120">Previous Track</TextBlock>
|
||||||
Items="{Binding KeyboardList}" SelectedItem="{Binding PreviousTrackKey, Mode=TwoWay}"
|
<ComboBox Name="PreviousTrackKeyBind"
|
||||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
Items="{Binding KeyboardList}" SelectedItem="{Binding PreviousTrackKey, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||||
|
</WrapPanel>
|
||||||
|
|
||||||
|
<!-- Shuffle Tracks -->
|
||||||
|
<WrapPanel Margin="0,0,0,16">
|
||||||
|
<TextBlock Width="120">Shuffle Tracks</TextBlock>
|
||||||
|
<ComboBox Name="ShuffleTracksKeyBind"
|
||||||
|
Items="{Binding KeyboardList}" SelectedItem="{Binding ShuffleTracksKey, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||||
|
</WrapPanel>
|
||||||
|
|
||||||
<!-- Next Index -->
|
<!-- Next Index -->
|
||||||
<TextBlock Grid.Row="7" Grid.Column="0" Width="120">Next Index</TextBlock>
|
<WrapPanel Margin="0,0,0,16">
|
||||||
<ComboBox Grid.Row="7" Grid.Column="1" Name="NextIndexKeyBind"
|
<TextBlock Width="120">Next Index</TextBlock>
|
||||||
Items="{Binding KeyboardList}" SelectedItem="{Binding NextIndexKey, Mode=TwoWay}"
|
<ComboBox Name="NextIndexKeyBind"
|
||||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
Items="{Binding KeyboardList}" SelectedItem="{Binding NextIndexKey, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||||
|
</WrapPanel>
|
||||||
|
|
||||||
<!-- Previous Index -->
|
<!-- Previous Index -->
|
||||||
<TextBlock Grid.Row="8" Grid.Column="0" Width="120">Previous Index</TextBlock>
|
<WrapPanel Margin="0,0,0,16">
|
||||||
<ComboBox Grid.Row="8" Grid.Column="1" Name="PreviousIndexKeyBind"
|
<TextBlock Width="120">Previous Index</TextBlock>
|
||||||
Items="{Binding KeyboardList}" SelectedItem="{Binding PreviousIndexKey, Mode=TwoWay}"
|
<ComboBox Name="PreviousIndexKeyBind"
|
||||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
Items="{Binding KeyboardList}" SelectedItem="{Binding PreviousIndexKey, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||||
|
</WrapPanel>
|
||||||
|
|
||||||
<!-- Fast Forward -->
|
<!-- Fast Forward -->
|
||||||
<TextBlock Grid.Row="9" Grid.Column="0" Width="120">Fast-Forward</TextBlock>
|
<WrapPanel Margin="0,0,0,16">
|
||||||
<ComboBox Grid.Row="9" Grid.Column="1" Name="FastForwardPlaybackKeyBind"
|
<TextBlock Width="120">Fast-Forward</TextBlock>
|
||||||
Items="{Binding KeyboardList}" SelectedItem="{Binding FastForwardPlaybackKey, Mode=TwoWay}"
|
<ComboBox Name="FastForwardPlaybackKeyBind"
|
||||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
Items="{Binding KeyboardList}" SelectedItem="{Binding FastForwardPlaybackKey, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||||
|
</WrapPanel>
|
||||||
|
|
||||||
<!-- Rewind -->
|
<!-- Rewind -->
|
||||||
<TextBlock Grid.Row="10" Grid.Column="0" Width="120">Rewind</TextBlock>
|
<WrapPanel Margin="0,0,0,16">
|
||||||
<ComboBox Grid.Row="10" Grid.Column="1" Name="RewindPlaybackKeyBind"
|
<TextBlock Width="120">Rewind</TextBlock>
|
||||||
Items="{Binding KeyboardList}" SelectedItem="{Binding RewindPlaybackKey, Mode=TwoWay}"
|
<ComboBox Name="RewindPlaybackKeyBind"
|
||||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
Items="{Binding KeyboardList}" SelectedItem="{Binding RewindPlaybackKey, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||||
|
</WrapPanel>
|
||||||
|
|
||||||
<!-- Volume Up -->
|
<!-- Volume Up -->
|
||||||
<TextBlock Grid.Row="11" Grid.Column="0" Width="120">Volume Up</TextBlock>
|
<WrapPanel Margin="0,0,0,16">
|
||||||
<ComboBox Grid.Row="11" Grid.Column="1" Name="VolumeUpKeyBind"
|
<TextBlock Width="120">Volume Up</TextBlock>
|
||||||
Items="{Binding KeyboardList}" SelectedItem="{Binding VolumeUpKey, Mode=TwoWay}"
|
<ComboBox Name="VolumeUpKeyBind"
|
||||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
Items="{Binding KeyboardList}" SelectedItem="{Binding VolumeUpKey, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||||
|
</WrapPanel>
|
||||||
|
|
||||||
<!-- Volume Down -->
|
<!-- Volume Down -->
|
||||||
<TextBlock Grid.Row="12" Grid.Column="0" Width="120">Volume Down</TextBlock>
|
<WrapPanel Margin="0,0,0,16">
|
||||||
<ComboBox Grid.Row="12" Grid.Column="1" Name="VolumeDownKeyBind"
|
<TextBlock Width="120">Volume Down</TextBlock>
|
||||||
Items="{Binding KeyboardList}" SelectedItem="{Binding VolumeDownKey, Mode=TwoWay}"
|
<ComboBox Name="VolumeDownKeyBind"
|
||||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
Items="{Binding KeyboardList}" SelectedItem="{Binding VolumeDownKey, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||||
|
</WrapPanel>
|
||||||
|
|
||||||
<!-- Mute Toggle -->
|
<!-- Mute Toggle -->
|
||||||
<TextBlock Grid.Row="13" Grid.Column="0" Width="120">Toggle Mute</TextBlock>
|
<WrapPanel Margin="0,0,0,16">
|
||||||
<ComboBox Grid.Row="13" Grid.Column="1" Name="ToggleMuteKeyBind"
|
<TextBlock Width="120">Toggle Mute</TextBlock>
|
||||||
Items="{Binding KeyboardList}" SelectedItem="{Binding ToggleMuteKey, Mode=TwoWay}"
|
<ComboBox Name="ToggleMuteKeyBind"
|
||||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
Items="{Binding KeyboardList}" SelectedItem="{Binding ToggleMuteKey, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||||
|
</WrapPanel>
|
||||||
|
|
||||||
<!-- De-Emphasis Toggle -->
|
<!-- De-Emphasis Toggle -->
|
||||||
<TextBlock Grid.Row="14" Grid.Column="0" Width="120">Toggle De-Emphasis</TextBlock>
|
<WrapPanel Margin="0,0,0,16">
|
||||||
<ComboBox Grid.Row="14" Grid.Column="1" Name="ToggleDeEmphasisKeyBind"
|
<TextBlock Width="120">Toggle De-Emphasis</TextBlock>
|
||||||
Items="{Binding KeyboardList}" SelectedItem="{Binding ToggleDeEmphasisKey, Mode=TwoWay}"
|
<ComboBox Name="ToggleDeEmphasisKeyBind"
|
||||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
Items="{Binding KeyboardList}" SelectedItem="{Binding ToggleDeEmphasisKey, Mode=TwoWay}"
|
||||||
</Grid>
|
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||||
|
</WrapPanel>
|
||||||
|
</StackPanel>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</TabControl>
|
</TabControl>
|
||||||
<Button Name="ApplyButton" Command="{Binding ApplySettingsCommand}">Apply</Button>
|
<Button Name="ApplyButton" Command="{Binding ApplySettingsCommand}">Apply</Button>
|
||||||
|
|||||||
@@ -8,19 +8,19 @@
|
|||||||
<viewModels:PlayerViewModel/>
|
<viewModels:PlayerViewModel/>
|
||||||
</ReactiveUserControl.ViewModel>
|
</ReactiveUserControl.ViewModel>
|
||||||
<StackPanel Margin="16" VerticalAlignment="Center">
|
<StackPanel Margin="16" VerticalAlignment="Center">
|
||||||
<Button Command="{Binding LoadCommand}" Margin="32,0,32,16">Load</Button>
|
<Button Command="{Binding LoadCommand}" Focusable="False" Margin="32,0,32,16">Load</Button>
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
||||||
<Button Command="{Binding PlayCommand}" Width="100" Margin="0,0,16,0">Play</Button>
|
<Button Command="{Binding PlayCommand}" Focusable="False" Width="100" Margin="0,0,16,0">Play</Button>
|
||||||
<Button Command="{Binding PauseCommand}" Width="100" Margin="0,0,16,0">Pause</Button>
|
<Button Command="{Binding PauseCommand}" Focusable="False" Width="100" Margin="0,0,16,0">Pause</Button>
|
||||||
<Button Command="{Binding StopCommand}" Width="100" Margin="0,0,16,0">Stop</Button>
|
<Button Command="{Binding StopCommand}" Focusable="False" Width="100" Margin="0,0,16,0">Stop</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
||||||
<Button Command="{Binding PreviousTrackCommand}" Width="100" Margin="0,0,16,0">Previous Track</Button>
|
<Button Command="{Binding PreviousTrackCommand}" Focusable="False" Width="100" Margin="0,0,16,0">Previous Track</Button>
|
||||||
<Button Command="{Binding NextTrackCommand}" Width="100" Margin="0,0,16,0">Next Track</Button>
|
<Button Command="{Binding NextTrackCommand}" Focusable="False" Width="100" Margin="0,0,16,0">Next Track</Button>
|
||||||
<Button Command="{Binding PreviousIndexCommand}" Width="100" Margin="0,0,16,0">Previous Index</Button>
|
<Button Command="{Binding PreviousIndexCommand}" Focusable="False" Width="100" Margin="0,0,16,0">Previous Index</Button>
|
||||||
<Button Command="{Binding NextIndexCommand}" Width="100" Margin="0,0,16,0">Next Index</Button>
|
<Button Command="{Binding NextIndexCommand}" Focusable="False" Width="100" Margin="0,0,16,0">Next Index</Button>
|
||||||
<RepeatButton Command="{Binding RewindCommand}" Width="100" Margin="0,0,16,0">Rewind</RepeatButton>
|
<RepeatButton Command="{Binding RewindCommand}" Focusable="False" Width="100" Margin="0,0,16,0">Rewind</RepeatButton>
|
||||||
<RepeatButton Command="{Binding FastForwardCommand}" Width="100">Fast Forward</RepeatButton>
|
<RepeatButton Command="{Binding FastForwardCommand}" Focusable="False" Width="100">Fast Forward</RepeatButton>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
||||||
<StackPanel Margin="0,0,32,0">
|
<StackPanel Margin="0,0,32,0">
|
||||||
@@ -81,12 +81,12 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
||||||
<Button Command="{Binding EnableDeEmphasisCommand}" IsVisible="{Binding !ApplyDeEmphasis}" Width="200"
|
<Button Command="{Binding EnableDeEmphasisCommand}" Focusable="False" IsVisible="{Binding !ApplyDeEmphasis}"
|
||||||
Margin="0,0,16,0">
|
Width="200" Margin="0,0,16,0">
|
||||||
Enable De-Emphasis
|
Enable De-Emphasis
|
||||||
</Button>
|
</Button>
|
||||||
<Button Command="{Binding DisableDeEmphasisCommand}" IsVisible="{Binding ApplyDeEmphasis}" Width="200"
|
<Button Command="{Binding DisableDeEmphasisCommand}" Focusable="False" IsVisible="{Binding ApplyDeEmphasis}"
|
||||||
Margin="0,0,16,0">
|
Width="200" Margin="0,0,16,0">
|
||||||
Disable De-Emphasis
|
Disable De-Emphasis
|
||||||
</Button>
|
</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -103,7 +103,8 @@
|
|||||||
<TextBlock Margin="0,0,16,0" IsVisible="{Binding QuadChannel}">4CH</TextBlock>
|
<TextBlock Margin="0,0,16,0" IsVisible="{Binding QuadChannel}">4CH</TextBlock>
|
||||||
<TextBlock Margin="0,0,16,0" Foreground="LightGray" IsVisible="{Binding !HiddenTrack}">HIDDEN</TextBlock>
|
<TextBlock Margin="0,0,16,0" Foreground="LightGray" IsVisible="{Binding !HiddenTrack}">HIDDEN</TextBlock>
|
||||||
<TextBlock Margin="0,0,16,0" IsVisible="{Binding HiddenTrack}">HIDDEN</TextBlock>
|
<TextBlock Margin="0,0,16,0" IsVisible="{Binding HiddenTrack}">HIDDEN</TextBlock>
|
||||||
<TextBlock Margin="0,0,16,0" Text="{Binding Volume}"/>
|
<TextBlock Margin="0,0,16,0" Text="{Binding Volume, StringFormat='Volume {0}%'}"/>
|
||||||
|
<TextBlock Margin="0,0,16,0" Text="{Binding CurrentDisc, StringFormat='Disc Number: {0}'}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ReactiveUserControl>
|
</ReactiveUserControl>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using NWaves.Filters.BiQuad;
|
using NWaves.Filters.BiQuad;
|
||||||
|
|
||||||
namespace RedBookPlayer.Models.Hardware
|
namespace RedBookPlayer.Models.Audio
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Filter for applying de-emphasis to audio
|
/// Filter for applying de-emphasis to audio
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using NWaves.Audio;
|
using NWaves.Audio;
|
||||||
using NWaves.Filters.BiQuad;
|
using NWaves.Filters.BiQuad;
|
||||||
|
|
||||||
namespace RedBookPlayer.Models.Hardware
|
namespace RedBookPlayer.Models.Audio
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Output stage that represents all filters on the audio
|
/// Output stage that represents all filters on the audio
|
||||||
30
RedBookPlayer.Models/Audio/IAudioBackend.cs
Normal file
30
RedBookPlayer.Models/Audio/IAudioBackend.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
namespace RedBookPlayer.Models.Audio
|
||||||
|
{
|
||||||
|
public interface IAudioBackend
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Pauses the audio playback
|
||||||
|
/// </summary>
|
||||||
|
void Pause();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts the playback.
|
||||||
|
/// </summary>
|
||||||
|
void Play();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stops the audio playback
|
||||||
|
/// </summary>
|
||||||
|
void Stop();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the current playback state
|
||||||
|
/// </summary>
|
||||||
|
PlayerState GetPlayerState();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the new volume value
|
||||||
|
/// </summary>
|
||||||
|
void SetVolume(float volume);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
RedBookPlayer.Models/Audio/Linux/AudioBackend.cs
Normal file
52
RedBookPlayer.Models/Audio/Linux/AudioBackend.cs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
using CSCore.SoundOut;
|
||||||
|
|
||||||
|
namespace RedBookPlayer.Models.Audio.Linux
|
||||||
|
{
|
||||||
|
public class AudioBackend : IAudioBackend
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sound output instance
|
||||||
|
/// </summary>
|
||||||
|
private readonly ALSoundOut _soundOut;
|
||||||
|
|
||||||
|
public AudioBackend() { }
|
||||||
|
|
||||||
|
public AudioBackend(PlayerSource source)
|
||||||
|
{
|
||||||
|
_soundOut = new ALSoundOut(100);
|
||||||
|
_soundOut.Initialize(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region IAudioBackend Implementation
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void Pause() => _soundOut.Pause();
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void Play() => _soundOut.Play();
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void Stop() => _soundOut.Stop();
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public PlayerState GetPlayerState()
|
||||||
|
{
|
||||||
|
return (_soundOut?.PlaybackState) switch
|
||||||
|
{
|
||||||
|
PlaybackState.Paused => PlayerState.Paused,
|
||||||
|
PlaybackState.Playing => PlayerState.Playing,
|
||||||
|
PlaybackState.Stopped => PlayerState.Stopped,
|
||||||
|
_ => PlayerState.NoDisc,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void SetVolume(float volume)
|
||||||
|
{
|
||||||
|
if(_soundOut != null)
|
||||||
|
_soundOut.Volume = volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ using System;
|
|||||||
using CSCore;
|
using CSCore;
|
||||||
using WaveFormat = CSCore.WaveFormat;
|
using WaveFormat = CSCore.WaveFormat;
|
||||||
|
|
||||||
namespace RedBookPlayer.Models.Hardware
|
namespace RedBookPlayer.Models.Audio
|
||||||
{
|
{
|
||||||
public class PlayerSource : IWaveSource
|
public class PlayerSource : IWaveSource
|
||||||
{
|
{
|
||||||
177
RedBookPlayer.Models/Audio/SoundOutput.cs
Normal file
177
RedBookPlayer.Models/Audio/SoundOutput.cs
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
|
namespace RedBookPlayer.Models.Audio
|
||||||
|
{
|
||||||
|
public class SoundOutput : ReactiveObject
|
||||||
|
{
|
||||||
|
#region Public Fields
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicate if the output is ready to be used
|
||||||
|
/// </summary>
|
||||||
|
public bool Initialized
|
||||||
|
{
|
||||||
|
get => _initialized;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _initialized, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates the current player state
|
||||||
|
/// </summary>
|
||||||
|
public PlayerState PlayerState
|
||||||
|
{
|
||||||
|
get => _playerState;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _playerState, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current playback volume
|
||||||
|
/// </summary>
|
||||||
|
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
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Data provider for sound output
|
||||||
|
/// </summary>
|
||||||
|
private PlayerSource _source;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sound output instance
|
||||||
|
/// </summary>
|
||||||
|
private IAudioBackend _soundOut;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="read">ReadFunction to use during decoding</param>
|
||||||
|
/// <param name="defaultVolume">Default volume between 0 and 100 to use when starting playback</param>
|
||||||
|
public SoundOutput(PlayerSource.ReadFunction read, int defaultVolume = 100)
|
||||||
|
{
|
||||||
|
Volume = defaultVolume;
|
||||||
|
SetupAudio(read);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialize the output with a given image
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="autoPlay">True if playback should begin immediately, false otherwise</param>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reset the current internal state
|
||||||
|
/// </summary>
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
_soundOut.Stop();
|
||||||
|
Initialized = false;
|
||||||
|
PlayerState = PlayerState.NoDisc;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Playback
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Start audio playback
|
||||||
|
/// </summary>
|
||||||
|
public void Play()
|
||||||
|
{
|
||||||
|
if(_soundOut.GetPlayerState() != PlayerState.Playing)
|
||||||
|
_soundOut.Play();
|
||||||
|
|
||||||
|
PlayerState = PlayerState.Playing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pause audio playback
|
||||||
|
/// </summary>
|
||||||
|
public void Pause()
|
||||||
|
{
|
||||||
|
if(_soundOut.GetPlayerState() != PlayerState.Paused)
|
||||||
|
_soundOut.Pause();
|
||||||
|
|
||||||
|
PlayerState = PlayerState.Paused;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stop audio playback
|
||||||
|
/// </summary>
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
if(_soundOut.GetPlayerState() != PlayerState.Stopped)
|
||||||
|
_soundOut.Stop();
|
||||||
|
|
||||||
|
PlayerState = PlayerState.Stopped;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Eject the currently loaded disc
|
||||||
|
/// </summary>
|
||||||
|
public void Eject() => Reset();
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Helpers
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the value for the volume
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="volume">New volume value</param>
|
||||||
|
public void SetVolume(int volume)
|
||||||
|
{
|
||||||
|
Volume = volume;
|
||||||
|
_soundOut?.SetVolume((float)Volume / 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets or resets the audio playback objects
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="read">ReadFunction to use during decoding</param>
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
52
RedBookPlayer.Models/Audio/Windows/AudioBackend.cs
Normal file
52
RedBookPlayer.Models/Audio/Windows/AudioBackend.cs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
using CSCore.SoundOut;
|
||||||
|
|
||||||
|
namespace RedBookPlayer.Models.Audio.Windows
|
||||||
|
{
|
||||||
|
public class AudioBackend : IAudioBackend
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sound output instance
|
||||||
|
/// </summary>
|
||||||
|
private readonly ALSoundOut _soundOut;
|
||||||
|
|
||||||
|
public AudioBackend() { }
|
||||||
|
|
||||||
|
public AudioBackend(PlayerSource source)
|
||||||
|
{
|
||||||
|
_soundOut = new ALSoundOut(100);
|
||||||
|
_soundOut.Initialize(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region IAudioBackend Implementation
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void Pause() => _soundOut.Pause();
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void Play() => _soundOut.Play();
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void Stop() => _soundOut.Stop();
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public PlayerState GetPlayerState()
|
||||||
|
{
|
||||||
|
return (_soundOut?.PlaybackState) switch
|
||||||
|
{
|
||||||
|
PlaybackState.Paused => PlayerState.Paused,
|
||||||
|
PlaybackState.Playing => PlayerState.Playing,
|
||||||
|
PlaybackState.Stopped => PlayerState.Stopped,
|
||||||
|
_ => PlayerState.NoDisc,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void SetVolume(float volume)
|
||||||
|
{
|
||||||
|
if(_soundOut != null)
|
||||||
|
_soundOut.Volume = volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,85 +27,25 @@ namespace RedBookPlayer.Models.Discs
|
|||||||
if(_image == null)
|
if(_image == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Data tracks only and flag disabled means we can't do anything
|
// Invalid value means we can't do anything
|
||||||
if(_image.Tracks.All(t => t.TrackType != TrackType.Audio) && DataPlayback == DataPlayback.Skip)
|
if(value > _image.Tracks.Max(t => t.TrackSequence))
|
||||||
|
return;
|
||||||
|
else if(value < _image.Tracks.Min(t => t.TrackSequence))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Cache the value and the current track number
|
// Cache the current track for easy access
|
||||||
int cachedValue = value;
|
Track track = GetTrack(value);
|
||||||
int cachedTrackNumber;
|
if(track == null)
|
||||||
|
|
||||||
// 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)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
TotalIndexes = cachedTrack.Indexes.Keys.Max();
|
// Set all track flags and values
|
||||||
CurrentTrackIndex = cachedTrack.Indexes.Keys.Min();
|
SetTrackFlags(track);
|
||||||
CurrentTrackSession = cachedTrack.TrackSession;
|
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)
|
if(track == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Ensure that the value is valid, wrapping around if necessary
|
// Invalid value means we can't do anything
|
||||||
ushort fixedValue = value;
|
|
||||||
if(value > track.Indexes.Keys.Max())
|
if(value > track.Indexes.Keys.Max())
|
||||||
fixedValue = track.Indexes.Keys.Min();
|
return;
|
||||||
else if(value < track.Indexes.Keys.Min())
|
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
|
// Set new index-specific data
|
||||||
SectionStartSector = (ulong)track.Indexes[CurrentTrackIndex];
|
SectionStartSector = (ulong)track.Indexes[CurrentTrackIndex];
|
||||||
@@ -156,19 +95,18 @@ namespace RedBookPlayer.Models.Discs
|
|||||||
if(_image == null)
|
if(_image == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// If the sector is over the end of the image, then loop
|
// Invalid value means we can't do anything
|
||||||
ulong tempSector = value;
|
if(value > _image.Info.Sectors)
|
||||||
if(tempSector > _image.Info.Sectors)
|
return;
|
||||||
tempSector = 0;
|
else if(value < 0)
|
||||||
else if(tempSector < 0)
|
return;
|
||||||
tempSector = _image.Info.Sectors - 1;
|
|
||||||
|
|
||||||
// Cache the current track for easy access
|
// Cache the current track for easy access
|
||||||
Track track = GetTrack(CurrentTrackNumber);
|
Track track = GetTrack(CurrentTrackNumber);
|
||||||
if(track == null)
|
if(track == null)
|
||||||
return;
|
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 the current sector is outside of the last known track, seek to the right one
|
||||||
if(CurrentSector < track.TrackStartSector || CurrentSector > track.TrackEndSector)
|
if(CurrentSector < track.TrackStartSector || CurrentSector > track.TrackEndSector)
|
||||||
@@ -194,6 +132,11 @@ namespace RedBookPlayer.Models.Discs
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override int BytesPerSector => GetTrack(CurrentTrackNumber)?.TrackRawBytesPerSector ?? 0;
|
public override int BytesPerSector => GetTrack(CurrentTrackNumber)?.TrackRawBytesPerSector ?? 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Readonly list of all tracks in the image
|
||||||
|
/// </summary>
|
||||||
|
public List<Track> Tracks => _image?.Tracks;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the 4CH flag
|
/// Represents the 4CH flag
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -230,21 +173,6 @@ namespace RedBookPlayer.Models.Discs
|
|||||||
private set => this.RaiseAndSetIfChanged(ref _trackHasEmphasis, value);
|
private set => this.RaiseAndSetIfChanged(ref _trackHasEmphasis, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicate how data tracks should be handled
|
|
||||||
/// </summary>
|
|
||||||
public DataPlayback DataPlayback { get; set; } = DataPlayback.Skip;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicate if hidden tracks should be loaded
|
|
||||||
/// </summary>
|
|
||||||
public bool LoadHiddenTracks { get; set; } = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicates how tracks on different session should be handled
|
|
||||||
/// </summary>
|
|
||||||
public SessionHandling SessionHandling { get; set; } = SessionHandling.AllSessions;
|
|
||||||
|
|
||||||
private bool _quadChannel;
|
private bool _quadChannel;
|
||||||
private bool _isDataTrack;
|
private bool _isDataTrack;
|
||||||
private bool _copyAllowed;
|
private bool _copyAllowed;
|
||||||
@@ -290,30 +218,26 @@ namespace RedBookPlayer.Models.Discs
|
|||||||
/// Constructor
|
/// Constructor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="options">Set of options for a new disc</param>
|
/// <param name="options">Set of options for a new disc</param>
|
||||||
public CompactDisc(OpticalDiscOptions options)
|
public CompactDisc(OpticalDiscOptions options) => _generateMissingToc = options.GenerateMissingToc;
|
||||||
{
|
|
||||||
DataPlayback = options.DataPlayback;
|
|
||||||
_generateMissingToc = options.GenerateMissingToc;
|
|
||||||
LoadHiddenTracks = options.LoadHiddenTracks;
|
|
||||||
SessionHandling = options.SessionHandling;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
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 the image is null, we can't do anything
|
||||||
if(image == null)
|
if(image == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Set the current disc image
|
// Set the current disc image
|
||||||
|
ImagePath = path;
|
||||||
_image = image;
|
_image = image;
|
||||||
|
|
||||||
// Attempt to load the TOC
|
// Attempt to load the TOC
|
||||||
if(!LoadTOC())
|
if(!LoadTOC())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Load the first track
|
// Load the first track by default
|
||||||
LoadFirstTrack();
|
CurrentTrackNumber = 1;
|
||||||
|
LoadTrack(CurrentTrackNumber);
|
||||||
|
|
||||||
// Reset total indexes if not in autoplay
|
// Reset total indexes if not in autoplay
|
||||||
if(!autoPlay)
|
if(!autoPlay)
|
||||||
@@ -329,134 +253,32 @@ namespace RedBookPlayer.Models.Discs
|
|||||||
Initialized = true;
|
Initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Seeking
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override void NextTrack()
|
|
||||||
{
|
|
||||||
if(_image == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
CurrentTrackNumber++;
|
|
||||||
LoadTrack(CurrentTrackNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override void PreviousTrack()
|
|
||||||
{
|
|
||||||
if(_image == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
CurrentTrackNumber--;
|
|
||||||
LoadTrack(CurrentTrackNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
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
|
#region Helpers
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override void ExtractTrackToWav(uint trackNumber, string outputDirectory)
|
public override void ExtractTrackToWav(uint trackNumber, string outputDirectory) => ExtractTrackToWav(trackNumber, outputDirectory, DataPlayback.Skip);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extract a track to WAV
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="trackNumber">Track number to extract</param>
|
||||||
|
/// <param name="outputDirectory">Output path to write data to</param>
|
||||||
|
/// <param name="dataPlayback">DataPlayback value indicating how to handle data tracks</param>
|
||||||
|
public void ExtractTrackToWav(uint trackNumber, string outputDirectory, DataPlayback dataPlayback)
|
||||||
{
|
{
|
||||||
if(_image == null)
|
if(_image == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Get the track with that value, if possible
|
// Get the track with that value, if possible
|
||||||
Track track = _image.Tracks.FirstOrDefault(t => t.TrackSequence == trackNumber);
|
Track track = _image.Tracks.FirstOrDefault(t => t.TrackSequence == trackNumber);
|
||||||
|
if(track == null)
|
||||||
// If the track isn't valid, we can't do anything
|
|
||||||
if(track == null || !(DataPlayback != DataPlayback.Skip || track.TrackType == TrackType.Audio))
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Get the number of sectors to read
|
// Get the number of sectors to read
|
||||||
uint length = (uint)(track.TrackEndSector - track.TrackStartSector);
|
uint length = (uint)(track.TrackEndSector - track.TrackStartSector);
|
||||||
|
|
||||||
// Read in the track data to a buffer
|
// 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
|
// Build the WAV output
|
||||||
string filename = Path.Combine(outputDirectory, $"Track {trackNumber.ToString().PadLeft(2, '0')}.wav");
|
string filename = Path.Combine(outputDirectory, $"Track {trackNumber.ToString().PadLeft(2, '0')}.wav");
|
||||||
@@ -470,15 +292,20 @@ namespace RedBookPlayer.Models.Discs
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <summary>
|
||||||
public override void ExtractAllTracksToWav(string outputDirectory)
|
/// Get the track with the given sequence value, if possible
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="trackNumber">Track number to retrieve</param>
|
||||||
|
/// <returns>Track object for the requested sequence, null on error</returns>
|
||||||
|
public Track GetTrack(int trackNumber)
|
||||||
{
|
{
|
||||||
if(_image == null)
|
try
|
||||||
return;
|
|
||||||
|
|
||||||
foreach(Track track in _image.Tracks)
|
|
||||||
{
|
{
|
||||||
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
|
// 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);
|
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");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override void LoadFirstTrack()
|
public override void LoadIndex(ushort index)
|
||||||
{
|
{
|
||||||
CurrentTrackNumber = 1;
|
if(_image == null)
|
||||||
LoadTrack(CurrentTrackNumber);
|
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];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override byte[] ReadSectors(uint sectorsToRead) => ReadSectors(CurrentSector, sectorsToRead);
|
public override byte[] ReadSectors(uint sectorsToRead) => ReadSectors(CurrentSector, sectorsToRead, DataPlayback.Skip);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Read sector data from the base image starting from the specified sector
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sectorsToRead">Current number of sectors to read</param>
|
||||||
|
/// <param name="dataPlayback">DataPlayback value indicating how to handle data tracks</param>
|
||||||
|
/// <returns>Byte array representing the read sectors, if possible</returns>
|
||||||
|
public byte[] ReadSectors(uint sectorsToRead, DataPlayback dataPlayback) => ReadSectors(CurrentSector, sectorsToRead, dataPlayback);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Read subchannel data from the base image starting from the specified sector
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sectorsToRead">Current number of sectors to read</param>
|
||||||
|
/// <returns>Byte array representing the read subchannels, if possible</returns>
|
||||||
|
public byte[] ReadSubchannels(uint sectorsToRead) => ReadSubchannels(CurrentSector, sectorsToRead);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Read sector data from the base image starting from the specified sector
|
/// Read sector data from the base image starting from the specified sector
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="startSector">Sector to start at for reading</param>
|
/// <param name="startSector">Sector to start at for reading</param>
|
||||||
/// <param name="sectorsToRead">Current number of sectors to read</param>
|
/// <param name="sectorsToRead">Current number of sectors to read</param>
|
||||||
|
/// <param name="dataPlayback">DataPlayback value indicating how to handle data tracks</param>
|
||||||
/// <returns>Byte array representing the read sectors, if possible</returns>
|
/// <returns>Byte array representing the read sectors, if possible</returns>
|
||||||
private byte[] ReadSectors(ulong startSector, uint sectorsToRead)
|
/// <remarks>Should be a multiple of 96 bytes</remarks>
|
||||||
|
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);
|
return _image.ReadSectors(startSector, sectorsToRead);
|
||||||
}
|
}
|
||||||
else if(DataPlayback == DataPlayback.Blank)
|
else if(dataPlayback == DataPlayback.Blank)
|
||||||
{
|
{
|
||||||
byte[] sectors = _image.ReadSectors(startSector, sectorsToRead);
|
byte[] sectors = _image.ReadSectors(startSector, sectorsToRead);
|
||||||
Array.Clear(sectors, 0, sectors.Length);
|
Array.Clear(sectors, 0, sectors.Length);
|
||||||
@@ -533,6 +394,16 @@ namespace RedBookPlayer.Models.Discs
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Read subchannel data from the base image starting from the specified sector
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="startSector">Sector to start at for reading</param>
|
||||||
|
/// <param name="sectorsToRead">Current number of sectors to read</param>
|
||||||
|
/// <returns>Byte array representing the read subchannels, if possible</returns>
|
||||||
|
/// <remarks>Should be a multiple of 96 bytes</remarks>
|
||||||
|
private byte[] ReadSubchannels(ulong startSector, uint sectorsToRead)
|
||||||
|
=> _image.ReadSectorsTag(startSector, sectorsToRead, SectorTagType.CdSectorSubchannel);
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override void SetTotalIndexes()
|
public override void SetTotalIndexes()
|
||||||
{
|
{
|
||||||
@@ -542,23 +413,6 @@ namespace RedBookPlayer.Models.Discs
|
|||||||
TotalIndexes = GetTrack(CurrentTrackNumber)?.Indexes.Keys.Max() ?? 0;
|
TotalIndexes = GetTrack(CurrentTrackNumber)?.Indexes.Keys.Max() ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the track with the given sequence value, if possible
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="trackNumber">Track number to retrieve</param>
|
|
||||||
/// <returns>Track object for the requested sequence, null on error</returns>
|
|
||||||
private Track GetTrack(int trackNumber)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return _image.Tracks.FirstOrDefault(t => t.TrackSequence == trackNumber);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Load TOC for the current disc image
|
/// Load TOC for the current disc image
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ namespace RedBookPlayer.Models.Discs
|
|||||||
{
|
{
|
||||||
#region Public Fields
|
#region Public Fields
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Path to the disc image
|
||||||
|
/// </summary>
|
||||||
|
public string ImagePath { get; protected set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicate if the disc is ready to be used
|
/// Indicate if the disc is ready to be used
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -93,37 +98,10 @@ namespace RedBookPlayer.Models.Discs
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initialize the disc with a given image
|
/// Initialize the disc with a given image
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="path">Path of the image</param>
|
||||||
/// <param name="image">Aaruformat image to load</param>
|
/// <param name="image">Aaruformat image to load</param>
|
||||||
/// <param name="autoPlay">True if playback should begin immediately, false otherwise</param>
|
/// <param name="autoPlay">True if playback should begin immediately, false otherwise</param>
|
||||||
public abstract void Init(IOpticalMediaImage image, bool autoPlay);
|
public abstract void Init(string path, IOpticalMediaImage image, bool autoPlay);
|
||||||
|
|
||||||
#region Seeking
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Try to move to the next track, wrapping around if necessary
|
|
||||||
/// </summary>
|
|
||||||
public abstract void NextTrack();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Try to move to the previous track, wrapping around if necessary
|
|
||||||
/// </summary>
|
|
||||||
public abstract void PreviousTrack();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Try to move to the next track index
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="changeTrack">True if index changes can trigger a track change, false otherwise</param>
|
|
||||||
/// <returns>True if the track was changed, false otherwise</returns>
|
|
||||||
public abstract bool NextIndex(bool changeTrack);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Try to move to the previous track index
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="changeTrack">True if index changes can trigger a track change, false otherwise</param>
|
|
||||||
/// <returns>True if the track was changed, false otherwise</returns>
|
|
||||||
public abstract bool PreviousIndex(bool changeTrack);
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Helpers
|
#region Helpers
|
||||||
|
|
||||||
@@ -134,12 +112,6 @@ namespace RedBookPlayer.Models.Discs
|
|||||||
/// <param name="outputDirectory">Output path to write data to</param>
|
/// <param name="outputDirectory">Output path to write data to</param>
|
||||||
public abstract void ExtractTrackToWav(uint trackNumber, string outputDirectory);
|
public abstract void ExtractTrackToWav(uint trackNumber, string outputDirectory);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Extract all tracks to WAV
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="outputDirectory">Output path to write data to</param>
|
|
||||||
public abstract void ExtractAllTracksToWav(string outputDirectory);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Load the desired track, if possible
|
/// Load the desired track, if possible
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -147,9 +119,10 @@ namespace RedBookPlayer.Models.Discs
|
|||||||
public abstract void LoadTrack(int track);
|
public abstract void LoadTrack(int track);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Load the first valid track in the image
|
/// Load the desired index, if possible
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract void LoadFirstTrack();
|
/// <param name="index">Index number to load</param>
|
||||||
|
public abstract void LoadIndex(ushort index);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Read sector data from the base image starting from the current sector
|
/// Read sector data from the base image starting from the current sector
|
||||||
|
|||||||
@@ -4,26 +4,11 @@ namespace RedBookPlayer.Models.Discs
|
|||||||
{
|
{
|
||||||
#region CompactDisc
|
#region CompactDisc
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicate how data tracks should be handled
|
|
||||||
/// </summary>
|
|
||||||
public DataPlayback DataPlayback { get; set; } = DataPlayback.Skip;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicate if a TOC should be generated if missing
|
/// Indicate if a TOC should be generated if missing
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool GenerateMissingToc { get; set; } = false;
|
public bool GenerateMissingToc { get; set; } = false;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicate if hidden tracks should be loaded
|
|
||||||
/// </summary>
|
|
||||||
public bool LoadHiddenTracks { get; set; } = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicates how tracks on different session should be handled
|
|
||||||
/// </summary>
|
|
||||||
public SessionHandling SessionHandling { get; set; } = SessionHandling.AllSessions;
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,6 +21,23 @@ namespace RedBookPlayer.Models
|
|||||||
Play = 2,
|
Play = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determine how to handle multiple discs
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Used with both repeat and shuffle</remarks>
|
||||||
|
public enum DiscHandling
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Only deal with tracks on the current disc
|
||||||
|
/// </summary>
|
||||||
|
SingleDisc = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deal with tracks on all loaded discs
|
||||||
|
/// </summary>
|
||||||
|
MultiDisc = 1,
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Current player state
|
/// Current player state
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ namespace RedBookPlayer.Models.Factories
|
|||||||
image.Open(filter);
|
image.Open(filter);
|
||||||
|
|
||||||
// Generate and instantiate the disc
|
// Generate and instantiate the disc
|
||||||
return GenerateFromImage(image, options, autoPlay);
|
return GenerateFromImage(path, image, options, autoPlay);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -44,11 +44,12 @@ namespace RedBookPlayer.Models.Factories
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generate an OpticalDisc from an input IOpticalMediaImage
|
/// Generate an OpticalDisc from an input IOpticalMediaImage
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="path">Path of the image</param>
|
||||||
/// <param name="image">IOpticalMediaImage to create from</param>
|
/// <param name="image">IOpticalMediaImage to create from</param>
|
||||||
/// <param name="options">Options to pass to the optical disc factory</param>
|
/// <param name="options">Options to pass to the optical disc factory</param>
|
||||||
/// <param name="autoPlay">True if the image should be playable immediately, false otherwise</param>
|
/// <param name="autoPlay">True if the image should be playable immediately, false otherwise</param>
|
||||||
/// <returns>Instantiated OpticalDisc, if possible</returns>
|
/// <returns>Instantiated OpticalDisc, if possible</returns>
|
||||||
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 the image is not usable, we don't do anything
|
||||||
if(!IsUsableImage(image))
|
if(!IsUsableImage(image))
|
||||||
@@ -74,7 +75,7 @@ namespace RedBookPlayer.Models.Factories
|
|||||||
return opticalDisc;
|
return opticalDisc;
|
||||||
|
|
||||||
// Instantiate the disc and return
|
// Instantiate the disc and return
|
||||||
opticalDisc.Init(image, autoPlay);
|
opticalDisc.Init(path, image, autoPlay);
|
||||||
return opticalDisc;
|
return opticalDisc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
30
RedBookPlayer.Models/Hardware/PlayerOptions.cs
Normal file
30
RedBookPlayer.Models/Hardware/PlayerOptions.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
namespace RedBookPlayer.Models.Discs
|
||||||
|
{
|
||||||
|
public class PlayerOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Indicate how data tracks should be handled
|
||||||
|
/// </summary>
|
||||||
|
public DataPlayback DataPlayback { get; set; } = DataPlayback.Skip;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates how to deal with multiple discs
|
||||||
|
/// </summary>
|
||||||
|
public DiscHandling DiscHandling { get; set; } = DiscHandling.SingleDisc;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicate if hidden tracks should be loaded
|
||||||
|
/// </summary>
|
||||||
|
public bool LoadHiddenTracks { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates the repeat mode
|
||||||
|
/// </summary>
|
||||||
|
public RepeatMode RepeatMode { get; set; } = RepeatMode.None;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates how tracks on different session should be handled
|
||||||
|
/// </summary>
|
||||||
|
public SessionHandling SessionHandling { get; set; } = SessionHandling.AllSessions;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicate if the output is ready to be used
|
|
||||||
/// </summary>
|
|
||||||
public bool Initialized
|
|
||||||
{
|
|
||||||
get => _initialized;
|
|
||||||
private set => this.RaiseAndSetIfChanged(ref _initialized, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicates the current player state
|
|
||||||
/// </summary>
|
|
||||||
public PlayerState PlayerState
|
|
||||||
{
|
|
||||||
get => _playerState;
|
|
||||||
private set => this.RaiseAndSetIfChanged(ref _playerState, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicates the repeat mode
|
|
||||||
/// </summary>
|
|
||||||
public RepeatMode RepeatMode
|
|
||||||
{
|
|
||||||
get => _repeatMode;
|
|
||||||
private set => this.RaiseAndSetIfChanged(ref _repeatMode, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicates if de-emphasis should be applied
|
|
||||||
/// </summary>
|
|
||||||
public bool ApplyDeEmphasis
|
|
||||||
{
|
|
||||||
get => _applyDeEmphasis;
|
|
||||||
private set => this.RaiseAndSetIfChanged(ref _applyDeEmphasis, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Current playback volume
|
|
||||||
/// </summary>
|
|
||||||
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
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// OpticalDisc from the parent player for easy access
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// TODO: Can we remove the need for a local reference to OpticalDisc?
|
|
||||||
/// </remarks>
|
|
||||||
private OpticalDiscBase _opticalDisc;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Data provider for sound output
|
|
||||||
/// </summary>
|
|
||||||
private PlayerSource _source;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sound output instance
|
|
||||||
/// </summary>
|
|
||||||
private ALSoundOut _soundOut;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Filtering stage for audio output
|
|
||||||
/// </summary>
|
|
||||||
private FilterStage _filterStage;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Current position in the sector
|
|
||||||
/// </summary>
|
|
||||||
private int _currentSectorReadPosition = 0;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Lock object for reading track data
|
|
||||||
/// </summary>
|
|
||||||
private readonly object _readingImage = new object();
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Constructor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="defaultVolume">Default volume between 0 and 100 to use when starting playback</param>
|
|
||||||
public SoundOutput(int defaultVolume = 100)
|
|
||||||
{
|
|
||||||
Volume = defaultVolume;
|
|
||||||
_filterStage = new FilterStage();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initialize the output with a given image
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="opticalDisc">OpticalDisc to load from</param>
|
|
||||||
/// <param name="repeatMode">RepeatMode for sound output</param>
|
|
||||||
/// <param name="autoPlay">True if playback should begin immediately, false otherwise</param>
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reset the current internal state
|
|
||||||
/// </summary>
|
|
||||||
public void Reset()
|
|
||||||
{
|
|
||||||
_soundOut.Stop();
|
|
||||||
_opticalDisc = null;
|
|
||||||
Initialized = false;
|
|
||||||
PlayerState = PlayerState.NoDisc;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Fill the current byte buffer with playable data
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="buffer">Buffer to load data into</param>
|
|
||||||
/// <param name="offset">Offset in the buffer to load at</param>
|
|
||||||
/// <param name="count">Number of bytes to load</param>
|
|
||||||
/// <returns>Number of bytes read</returns>
|
|
||||||
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
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Start audio playback
|
|
||||||
/// </summary>
|
|
||||||
public void Play()
|
|
||||||
{
|
|
||||||
if(_soundOut.PlaybackState != PlaybackState.Playing)
|
|
||||||
_soundOut.Play();
|
|
||||||
|
|
||||||
PlayerState = PlayerState.Playing;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Pause audio playback
|
|
||||||
/// </summary>
|
|
||||||
public void Pause()
|
|
||||||
{
|
|
||||||
if(_soundOut.PlaybackState != PlaybackState.Paused)
|
|
||||||
_soundOut.Pause();
|
|
||||||
|
|
||||||
PlayerState = PlayerState.Paused;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Stop audio playback
|
|
||||||
/// </summary>
|
|
||||||
public void Stop()
|
|
||||||
{
|
|
||||||
if(_soundOut.PlaybackState != PlaybackState.Stopped)
|
|
||||||
_soundOut.Stop();
|
|
||||||
|
|
||||||
PlayerState = PlayerState.Stopped;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Eject the currently loaded disc
|
|
||||||
/// </summary>
|
|
||||||
public void Eject() => Reset();
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Helpers
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set de-emphasis status
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="apply">New de-emphasis status</param>
|
|
||||||
public void SetDeEmphasis(bool apply) => ApplyDeEmphasis = apply;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set repeat mode
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="repeatMode">New repeat mode value</param>
|
|
||||||
public void SetRepeatMode(RepeatMode repeatMode) => RepeatMode = repeatMode;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set the value for the volume
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="volume">New volume value</param>
|
|
||||||
public void SetVolume(int volume) => Volume = volume;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determine the number of real and zero sectors to read
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="count">Number of requested bytes to read</param>
|
|
||||||
/// <param name="sectorsToRead">Number of sectors to read</param>
|
|
||||||
/// <param name="zeroSectorsAmount">Number of zeroed sectors to concatenate</param>
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Read the requested amount of data from an input
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="count">Number of bytes to load</param>
|
|
||||||
/// <param name="sectorsToRead">Number of sectors to read</param>
|
|
||||||
/// <param name="zeroSectorsAmount">Number of zeroed sectors to concatenate</param>
|
|
||||||
/// <returns>The requested amount of data, if possible</returns>
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets or resets the audio playback objects
|
|
||||||
/// </summary>
|
|
||||||
private void SetupAudio()
|
|
||||||
{
|
|
||||||
if(_source == null)
|
|
||||||
{
|
|
||||||
_source = new PlayerSource(ProviderRead);
|
|
||||||
_soundOut = new ALSoundOut(100);
|
|
||||||
_soundOut.Initialize(_source);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_soundOut.Stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
69
RedBookPlayer.Models/Hardware/SubchannelData.cs
Normal file
69
RedBookPlayer.Models/Hardware/SubchannelData.cs
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace RedBookPlayer.Models.Hardware
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents subchannel data for a single sector
|
||||||
|
/// </summary>
|
||||||
|
/// <see cref="https://jbum.com/cdg_revealed.html"/>
|
||||||
|
internal class SubchannelData
|
||||||
|
{
|
||||||
|
public SubchannelPacket[] Packets { get; private set; } = new SubchannelPacket[4];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new subchannel data from a byte array
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert the packet data into separate named subchannels
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<char, byte[]> ConvertData()
|
||||||
|
{
|
||||||
|
if(this.Packets == null || this.Packets.Length != 4)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Prepare the output formatted data
|
||||||
|
Dictionary<char, byte[]> formattedData = new Dictionary<char, byte[]>
|
||||||
|
{
|
||||||
|
['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<char, byte[]> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
89
RedBookPlayer.Models/Hardware/SubchannelPacket.cs
Normal file
89
RedBookPlayer.Models/Hardware/SubchannelPacket.cs
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace RedBookPlayer.Models.Hardware
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a single packet of subcode data
|
||||||
|
/// </summary>
|
||||||
|
/// <see cref="https://jbum.com/cdg_revealed.html"/>
|
||||||
|
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];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new subchannel packet from a byte array
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert the data into separate named subchannels
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<char, byte[]> ConvertData()
|
||||||
|
{
|
||||||
|
if(this.Data == null || this.Data.Length != 16)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Create the output dictionary for the formatted data
|
||||||
|
Dictionary<char, byte[]> formattedData = new Dictionary<char, byte[]>
|
||||||
|
{
|
||||||
|
['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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if a bit is set in a byte
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">Byte value to check</param>
|
||||||
|
/// <param name="bitIndex">Index of the bit to check</param>
|
||||||
|
/// <returns>True if the bit was set, false otherwise</returns>
|
||||||
|
private bool HasBitSet(byte value, int bitIndex) => (value & (1 << bitIndex)) != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||||
|
<RuntimeIdentifiers>win-x64;linux-x64</RuntimeIdentifiers>
|
||||||
|
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user