mirror of
https://github.com/aaru-dps/RedBookPlayer.git
synced 2025-12-16 11:14:39 +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 |
|
||||
| **Esc** | Stop Playback |
|
||||
| **~** | Eject |
|
||||
| **Page Up** | Next Disc |
|
||||
| **Page Down** | Previous Disc |
|
||||
| **→** | Next Track |
|
||||
| **←** | Previous Track |
|
||||
| **R** | Shuffle Tracks |
|
||||
| **]** | Next Index |
|
||||
| **[** | Previous Index |
|
||||
| **.** | Fast Forward |
|
||||
@@ -31,6 +34,13 @@ For Save Track(s):
|
||||
- Holding no modifying keys will prompt to save the current track
|
||||
- Holding **Shift** will prompt to save all tracks (including hidden)
|
||||
|
||||
For Disc Switching:
|
||||
- If you change the number of discs in the internal changer, you must restart the program for it to take effect
|
||||
|
||||
For Shuffling:
|
||||
- Shuffling only works on the current set of playable tracks
|
||||
- If you are in single disc mode and switch discs, it will not automatically shuffle the new tracks
|
||||
|
||||
For both Volume Up and Volume Down:
|
||||
- Holding **Ctrl** will move in increments of 2
|
||||
- Holding **Shift** will move in increments of 5
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<PropertyGroup Condition="'$(RuntimeIdentifier)|$(Configuration)' == 'win-x64|Debug'">
|
||||
<DefineConstants>WindowsDebug</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="**\*.xaml.cs">
|
||||
<DependentUpon>%(Filename)</DependentUpon>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using RedBookPlayer.GUI.Views;
|
||||
@@ -75,6 +76,18 @@ namespace RedBookPlayer.GUI.ViewModels
|
||||
PlayerView?.ViewModel?.ExecuteEject();
|
||||
}
|
||||
|
||||
// Next Disc
|
||||
else if(e.Key == App.Settings.NextDiscKey)
|
||||
{
|
||||
PlayerView?.ViewModel?.ExecuteNextDisc();
|
||||
}
|
||||
|
||||
// Previous Disc
|
||||
else if(e.Key == App.Settings.PreviousDiscKey)
|
||||
{
|
||||
PlayerView?.ViewModel?.ExecutePreviousDisc();
|
||||
}
|
||||
|
||||
// Next Track
|
||||
else if(e.Key == App.Settings.NextTrackKey || e.Key == Key.MediaNextTrack)
|
||||
{
|
||||
@@ -87,6 +100,12 @@ namespace RedBookPlayer.GUI.ViewModels
|
||||
PlayerView?.ViewModel?.ExecutePreviousTrack();
|
||||
}
|
||||
|
||||
// Shuffle Track List
|
||||
else if(e.Key == App.Settings.ShuffleTracksKey)
|
||||
{
|
||||
PlayerView?.ViewModel?.ExecuteShuffle();
|
||||
}
|
||||
|
||||
// Next Index
|
||||
else if(e.Key == App.Settings.NextIndexKey)
|
||||
{
|
||||
@@ -112,7 +131,7 @@ namespace RedBookPlayer.GUI.ViewModels
|
||||
}
|
||||
|
||||
// Volume Up
|
||||
else if(e.Key == App.Settings.VolumeUpKey || e.Key == Key.VolumeUp)
|
||||
else if(e.Key == App.Settings.VolumeUpKey)
|
||||
{
|
||||
int increment = 1;
|
||||
if(e.KeyModifiers.HasFlag(KeyModifiers.Control))
|
||||
@@ -125,7 +144,7 @@ namespace RedBookPlayer.GUI.ViewModels
|
||||
}
|
||||
|
||||
// Volume Down
|
||||
else if(e.Key == App.Settings.VolumeDownKey || e.Key == Key.VolumeDown)
|
||||
else if(e.Key == App.Settings.VolumeDownKey)
|
||||
{
|
||||
int decrement = 1;
|
||||
if(e.KeyModifiers.HasFlag(KeyModifiers.Control))
|
||||
@@ -138,7 +157,7 @@ namespace RedBookPlayer.GUI.ViewModels
|
||||
}
|
||||
|
||||
// Mute Toggle
|
||||
else if(e.Key == App.Settings.ToggleMuteKey || e.Key == Key.VolumeMute)
|
||||
else if(e.Key == App.Settings.ToggleMuteKey)
|
||||
{
|
||||
PlayerView?.ViewModel?.ExecuteToggleMute();
|
||||
}
|
||||
@@ -151,19 +170,35 @@ namespace RedBookPlayer.GUI.ViewModels
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load the first valid drag-and-dropped disc image
|
||||
/// Load the all valid drag-and-dropped disc images
|
||||
/// </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)
|
||||
{
|
||||
if(PlayerView?.ViewModel == null)
|
||||
return;
|
||||
|
||||
IEnumerable<string> fileNames = e.Data.GetFileNames();
|
||||
foreach(string filename in fileNames)
|
||||
if(fileNames == null || fileNames.Count() == 0)
|
||||
{
|
||||
bool loaded = await PlayerView.ViewModel.LoadImage(filename);
|
||||
if(loaded)
|
||||
break;
|
||||
return;
|
||||
}
|
||||
else if(fileNames.Count() == 1)
|
||||
{
|
||||
await PlayerView.ViewModel.LoadImage(fileNames.FirstOrDefault());
|
||||
}
|
||||
else
|
||||
{
|
||||
int lastDisc = PlayerView.ViewModel.CurrentDisc;
|
||||
foreach(string path in fileNames)
|
||||
{
|
||||
await PlayerView.ViewModel.LoadImage(path);
|
||||
|
||||
if(PlayerView.ViewModel.Initialized)
|
||||
PlayerView.ViewModel.ExecuteNextDisc();
|
||||
}
|
||||
|
||||
PlayerView.ViewModel.SelectDisc(lastDisc);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,8 +31,28 @@ namespace RedBookPlayer.GUI.ViewModels
|
||||
|
||||
#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
|
||||
|
||||
/// <summary>
|
||||
/// Path to the disc image
|
||||
/// </summary>
|
||||
public string ImagePath
|
||||
{
|
||||
get => _imagePath;
|
||||
private set => this.RaiseAndSetIfChanged(ref _imagePath, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Current track number
|
||||
/// </summary>
|
||||
@@ -148,6 +168,7 @@ namespace RedBookPlayer.GUI.ViewModels
|
||||
/// </summary>
|
||||
public ulong TotalTime => _player.TotalTime;
|
||||
|
||||
private string _imagePath;
|
||||
private int _currentTrackNumber;
|
||||
private ushort _currentTrackIndex;
|
||||
private ushort _currentTrackSession;
|
||||
@@ -263,6 +284,16 @@ namespace RedBookPlayer.GUI.ViewModels
|
||||
/// </summary>
|
||||
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>
|
||||
/// Command for moving to the next track
|
||||
/// </summary>
|
||||
@@ -348,6 +379,8 @@ namespace RedBookPlayer.GUI.ViewModels
|
||||
TogglePlayPauseCommand = ReactiveCommand.Create(ExecuteTogglePlayPause);
|
||||
StopCommand = ReactiveCommand.Create(ExecuteStop);
|
||||
EjectCommand = ReactiveCommand.Create(ExecuteEject);
|
||||
NextDiscCommand = ReactiveCommand.Create(ExecuteNextDisc);
|
||||
PreviousDiscCommand = ReactiveCommand.Create(ExecutePreviousDisc);
|
||||
NextTrackCommand = ReactiveCommand.Create(ExecuteNextTrack);
|
||||
PreviousTrackCommand = ReactiveCommand.Create(ExecutePreviousTrack);
|
||||
NextIndexCommand = ReactiveCommand.Create(ExecuteNextIndex);
|
||||
@@ -364,7 +397,9 @@ namespace RedBookPlayer.GUI.ViewModels
|
||||
ToggleDeEmphasisCommand = ReactiveCommand.Create(ExecuteToggleDeEmphasis);
|
||||
|
||||
// Initialize Player
|
||||
_player = new Player(App.Settings.Volume);
|
||||
_player = new Player(App.Settings.NumberOfDiscs, App.Settings.Volume);
|
||||
_player.PropertyChanged += PlayerStateChanged;
|
||||
PlayerStateChanged(this, null);
|
||||
PlayerState = PlayerState.NoDisc;
|
||||
}
|
||||
|
||||
@@ -372,25 +407,22 @@ namespace RedBookPlayer.GUI.ViewModels
|
||||
/// Initialize the view model with a given image path
|
||||
/// </summary>
|
||||
/// <param name="path">Path to the disc image</param>
|
||||
/// <param name="options">Options to pass to the optical disc factory</param>
|
||||
/// <param name="repeatMode">RepeatMode for sound output</param>
|
||||
/// <param name="playerOptions">Options to pass to the player</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>
|
||||
public void Init(string path, OpticalDiscOptions options, RepeatMode repeatMode, bool autoPlay)
|
||||
public void Init(string path, PlayerOptions playerOptions, OpticalDiscOptions opticalDiscOptions, bool autoPlay)
|
||||
{
|
||||
// Stop current playback, if necessary
|
||||
if(PlayerState != PlayerState.NoDisc)
|
||||
ExecuteStop();
|
||||
|
||||
// Attempt to initialize Player
|
||||
_player.Init(path, options, repeatMode, autoPlay);
|
||||
_player.Init(path, playerOptions, opticalDiscOptions, autoPlay);
|
||||
if(_player.Initialized)
|
||||
{
|
||||
_player.PropertyChanged += PlayerStateChanged;
|
||||
PlayerStateChanged(this, null);
|
||||
}
|
||||
}
|
||||
|
||||
#region Playback
|
||||
#region Playback (UI)
|
||||
|
||||
/// <summary>
|
||||
/// Begin playback
|
||||
@@ -407,6 +439,11 @@ namespace RedBookPlayer.GUI.ViewModels
|
||||
/// </summary>
|
||||
public void ExecuteTogglePlayPause() => _player?.TogglePlayback();
|
||||
|
||||
/// <summary>
|
||||
/// Shuffle the current track list
|
||||
/// </summary>
|
||||
public void ExecuteShuffle() => _player?.ShuffleTracks();
|
||||
|
||||
/// <summary>
|
||||
/// Stop current playback
|
||||
/// </summary>
|
||||
@@ -417,6 +454,16 @@ namespace RedBookPlayer.GUI.ViewModels
|
||||
/// </summary>
|
||||
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>
|
||||
/// Move to the next playable track
|
||||
/// </summary>
|
||||
@@ -449,6 +496,29 @@ namespace RedBookPlayer.GUI.ViewModels
|
||||
|
||||
#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
|
||||
|
||||
/// <summary>
|
||||
@@ -493,6 +563,111 @@ namespace RedBookPlayer.GUI.ViewModels
|
||||
|
||||
#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
|
||||
|
||||
/// <summary>
|
||||
@@ -540,11 +715,28 @@ namespace RedBookPlayer.GUI.ViewModels
|
||||
/// </summary>
|
||||
public async void ExecuteLoad()
|
||||
{
|
||||
string path = await GetPath();
|
||||
if(path == null)
|
||||
string[] paths = await GetPaths();
|
||||
if(paths == null || paths.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
else if(paths.Length == 1)
|
||||
{
|
||||
await LoadImage(paths[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
int lastDisc = CurrentDisc;
|
||||
foreach(string path in paths)
|
||||
{
|
||||
await LoadImage(path);
|
||||
|
||||
if(Initialized)
|
||||
ExecuteNextDisc();
|
||||
}
|
||||
|
||||
await LoadImage(path);
|
||||
SelectDisc(lastDisc);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -593,19 +785,25 @@ namespace RedBookPlayer.GUI.ViewModels
|
||||
{
|
||||
return await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
OpticalDiscOptions options = new OpticalDiscOptions
|
||||
PlayerOptions playerOptions = new PlayerOptions
|
||||
{
|
||||
DataPlayback = App.Settings.DataPlayback,
|
||||
GenerateMissingToc = App.Settings.GenerateMissingTOC,
|
||||
DiscHandling = App.Settings.DiscHandling,
|
||||
LoadHiddenTracks = App.Settings.PlayHiddenTracks,
|
||||
RepeatMode = App.Settings.RepeatMode,
|
||||
SessionHandling = App.Settings.SessionHandling,
|
||||
};
|
||||
|
||||
OpticalDiscOptions opticalDiscOptions = new OpticalDiscOptions
|
||||
{
|
||||
GenerateMissingToc = App.Settings.GenerateMissingTOC,
|
||||
};
|
||||
|
||||
// Ensure the context and view model are set
|
||||
App.PlayerView.DataContext = this;
|
||||
App.PlayerView.ViewModel = this;
|
||||
|
||||
Init(path, options, App.Settings.RepeatMode, App.Settings.AutoPlay);
|
||||
Init(path, playerOptions, opticalDiscOptions, App.Settings.AutoPlay);
|
||||
if(Initialized)
|
||||
App.MainWindow.Title = "RedBookPlayer - " + path.Split('/').Last().Split('\\').Last();
|
||||
|
||||
@@ -619,48 +817,12 @@ namespace RedBookPlayer.GUI.ViewModels
|
||||
public void RefreshFromSettings()
|
||||
{
|
||||
SetDataPlayback(App.Settings.DataPlayback);
|
||||
SetDiscHandling(App.Settings.DiscHandling);
|
||||
SetLoadHiddenTracks(App.Settings.PlayHiddenTracks);
|
||||
SetRepeatMode(App.Settings.RepeatMode);
|
||||
SetSessionHandling(App.Settings.SessionHandling);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// Generate the digit string to be interpreted by the frontend
|
||||
/// </summary>
|
||||
@@ -737,12 +899,12 @@ namespace RedBookPlayer.GUI.ViewModels
|
||||
/// <summary>
|
||||
/// Generate a path selection dialog box
|
||||
/// </summary>
|
||||
/// <returns>User-selected path, if possible</returns>
|
||||
private async Task<string> GetPath()
|
||||
/// <returns>User-selected paths, if possible</returns>
|
||||
private async Task<string[]> GetPaths()
|
||||
{
|
||||
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();
|
||||
dialog.Filters.Add(new FileDialogFilter()
|
||||
{
|
||||
@@ -750,7 +912,7 @@ namespace RedBookPlayer.GUI.ViewModels
|
||||
Extensions = knownExtensions.ConvertAll(e => e.TrimStart('.'))
|
||||
});
|
||||
|
||||
return (await dialog.ShowAsync(App.MainWindow))?.FirstOrDefault();
|
||||
return (await dialog.ShowAsync(App.MainWindow));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -782,46 +944,6 @@ namespace RedBookPlayer.GUI.ViewModels
|
||||
UpdateDigits();
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// Update UI
|
||||
/// </summary>
|
||||
|
||||
@@ -21,6 +21,12 @@ namespace RedBookPlayer.GUI.ViewModels
|
||||
[JsonIgnore]
|
||||
public List<DataPlayback> DataPlaybackValues => GenerateDataPlaybackList();
|
||||
|
||||
/// <summary>
|
||||
/// List of all disc handling values
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public List<DiscHandling> DiscHandlingValues => GenerateDiscHandlingList();
|
||||
|
||||
/// <summary>
|
||||
/// List of all repeat mode values
|
||||
/// </summary>
|
||||
@@ -44,6 +50,16 @@ namespace RedBookPlayer.GUI.ViewModels
|
||||
/// </summary>
|
||||
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>
|
||||
/// Indicates if an index change can trigger a track change
|
||||
/// </summary>
|
||||
@@ -144,6 +160,16 @@ namespace RedBookPlayer.GUI.ViewModels
|
||||
/// </summary>
|
||||
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>
|
||||
/// Key assigned to move to the next track
|
||||
/// </summary>
|
||||
@@ -154,6 +180,11 @@ namespace RedBookPlayer.GUI.ViewModels
|
||||
/// </summary>
|
||||
public Key PreviousTrackKey { get; set; } = Key.Left;
|
||||
|
||||
/// <summary>
|
||||
/// Key assigned to shuffling the track list
|
||||
/// </summary>
|
||||
public Key ShuffleTracksKey { get; set; } = Key.R;
|
||||
|
||||
/// <summary>
|
||||
/// Key assigned to move to the next index
|
||||
/// </summary>
|
||||
@@ -275,6 +306,11 @@ namespace RedBookPlayer.GUI.ViewModels
|
||||
/// </summary>
|
||||
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>
|
||||
/// Generate the list of Key values
|
||||
/// </summary>
|
||||
|
||||
@@ -8,19 +8,19 @@
|
||||
<viewModels:PlayerViewModel/>
|
||||
</ReactiveUserControl.ViewModel>
|
||||
<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">
|
||||
<Button Command="{Binding PlayCommand}" 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 StopCommand}" Width="100" Margin="0,0,16,0">Stop</Button>
|
||||
<Button Command="{Binding PlayCommand}" Focusable="False" Width="100" Margin="0,0,16,0">Play</Button>
|
||||
<Button Command="{Binding PauseCommand}" Focusable="False" Width="100" Margin="0,0,16,0">Pause</Button>
|
||||
<Button Command="{Binding StopCommand}" Focusable="False" Width="100" Margin="0,0,16,0">Stop</Button>
|
||||
</StackPanel>
|
||||
<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 NextTrackCommand}" 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 NextIndexCommand}" 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 FastForwardCommand}" Width="100">Fast Forward</RepeatButton>
|
||||
<Button Command="{Binding PreviousTrackCommand}" Focusable="False" Width="100" Margin="0,0,16,0">Previous Track</Button>
|
||||
<Button Command="{Binding NextTrackCommand}" Focusable="False" Width="100" Margin="0,0,16,0">Next Track</Button>
|
||||
<Button Command="{Binding PreviousIndexCommand}" Focusable="False" Width="100" Margin="0,0,16,0">Previous Index</Button>
|
||||
<Button Command="{Binding NextIndexCommand}" Focusable="False" Width="100" Margin="0,0,16,0">Next Index</Button>
|
||||
<RepeatButton Command="{Binding RewindCommand}" Focusable="False" Width="100" Margin="0,0,16,0">Rewind</RepeatButton>
|
||||
<RepeatButton Command="{Binding FastForwardCommand}" Focusable="False" Width="100">Fast Forward</RepeatButton>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
||||
<StackPanel Margin="0,0,32,0">
|
||||
@@ -81,12 +81,12 @@
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
||||
<Button Command="{Binding EnableDeEmphasisCommand}" IsVisible="{Binding !ApplyDeEmphasis}" Width="200"
|
||||
Margin="0,0,16,0">
|
||||
<Button Command="{Binding EnableDeEmphasisCommand}" Focusable="False" IsVisible="{Binding !ApplyDeEmphasis}"
|
||||
Width="200" Margin="0,0,16,0">
|
||||
Enable De-Emphasis
|
||||
</Button>
|
||||
<Button Command="{Binding DisableDeEmphasisCommand}" IsVisible="{Binding ApplyDeEmphasis}" Width="200"
|
||||
Margin="0,0,16,0">
|
||||
<Button Command="{Binding DisableDeEmphasisCommand}" Focusable="False" IsVisible="{Binding ApplyDeEmphasis}"
|
||||
Width="200" Margin="0,0,16,0">
|
||||
Disable De-Emphasis
|
||||
</Button>
|
||||
</StackPanel>
|
||||
@@ -103,7 +103,8 @@
|
||||
<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" 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>
|
||||
</ReactiveUserControl>
|
||||
@@ -26,20 +26,30 @@
|
||||
<TextBlock VerticalAlignment="Center">Play hidden tracks</TextBlock>
|
||||
</WrapPanel>
|
||||
<WrapPanel Margin="0,0,0,16">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Width="120">Data Track Playback</TextBlock>
|
||||
<ComboBox Grid.Row="0" Grid.Column="1" Name="DataPlayback" Margin="8,0,0,0" Width="120"
|
||||
<TextBlock Width="120">Data Track Playback</TextBlock>
|
||||
<ComboBox Name="DataPlayback" Margin="8,0,0,0" Width="120"
|
||||
Items="{Binding DataPlaybackValues}" SelectedItem="{Binding DataPlayback, Mode=TwoWay}" />
|
||||
</WrapPanel>
|
||||
<WrapPanel Margin="0,0,0,16">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Width="120">Session Handling</TextBlock>
|
||||
<ComboBox Grid.Row="0" Grid.Column="1" Name="SessionHandling" Margin="8,0,0,0" Width="120"
|
||||
<TextBlock Width="120">Session Handling</TextBlock>
|
||||
<ComboBox Name="SessionHandling" Margin="8,0,0,0" Width="120"
|
||||
Items="{Binding SessionHandlingValues}" SelectedItem="{Binding SessionHandling, Mode=TwoWay}" />
|
||||
</WrapPanel>
|
||||
<WrapPanel Margin="0,0,0,16">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Width="120">Repeat Mode</TextBlock>
|
||||
<ComboBox Grid.Row="0" Grid.Column="1" Name="RepeatMode" Margin="8,0,0,0" Width="120"
|
||||
<TextBlock Width="120">Repeat Mode</TextBlock>
|
||||
<ComboBox Name="RepeatMode" Margin="8,0,0,0" Width="120"
|
||||
Items="{Binding RepeatModeValues}" SelectedItem="{Binding RepeatMode, Mode=TwoWay}" />
|
||||
</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">
|
||||
<CheckBox IsChecked="{Binding GenerateMissingTOC}" Margin="0,0,8,0"/>
|
||||
<TextBlock VerticalAlignment="Center">Generate a TOC if the disc is missing one</TextBlock>
|
||||
@@ -58,120 +68,152 @@
|
||||
</DockPanel>
|
||||
</TabItem>
|
||||
<TabItem Header="Keyboard Bindings">
|
||||
<Grid 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>
|
||||
<StackPanel Margin="16">
|
||||
|
||||
<!-- Load Image-->
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Width="120">Load Image</TextBlock>
|
||||
<ComboBox Grid.Row="0" Grid.Column="1" Name="LoadImageKeyBind"
|
||||
Items="{Binding KeyboardList}" SelectedItem="{Binding LoadImageKey, Mode=TwoWay}"
|
||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||
<WrapPanel Margin="0,0,0,16">
|
||||
<TextBlock Width="120">Load Image</TextBlock>
|
||||
<ComboBox Name="LoadImageKeyBind"
|
||||
Items="{Binding KeyboardList}" SelectedItem="{Binding LoadImageKey, Mode=TwoWay}"
|
||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||
</WrapPanel>
|
||||
|
||||
<!-- Save Track -->
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Width="120">Save Track(s)</TextBlock>
|
||||
<ComboBox Grid.Row="1" Grid.Column="1" Name="SaveTrackKeyBind"
|
||||
Items="{Binding KeyboardList}" SelectedItem="{Binding SaveTrackKey, Mode=TwoWay}"
|
||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||
<WrapPanel Margin="0,0,0,16">
|
||||
<TextBlock Width="120">Save Track(s)</TextBlock>
|
||||
<ComboBox Name="SaveTrackKeyBind"
|
||||
Items="{Binding KeyboardList}" SelectedItem="{Binding SaveTrackKey, Mode=TwoWay}"
|
||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||
</WrapPanel>
|
||||
|
||||
<!-- Toggle Play/Pause -->
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Width="120">Toggle Play/Pause</TextBlock>
|
||||
<ComboBox Grid.Row="2" Grid.Column="1" Name="TogglePlaybackKeyBind"
|
||||
Items="{Binding KeyboardList}" SelectedItem="{Binding TogglePlaybackKey, Mode=TwoWay}"
|
||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||
<WrapPanel Margin="0,0,0,16">
|
||||
<TextBlock Width="120">Toggle Play/Pause</TextBlock>
|
||||
<ComboBox Name="TogglePlaybackKeyBind"
|
||||
Items="{Binding KeyboardList}" SelectedItem="{Binding TogglePlaybackKey, Mode=TwoWay}"
|
||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||
</WrapPanel>
|
||||
|
||||
<!-- Stop Playback-->
|
||||
<TextBlock Grid.Row="3" Grid.Column="0" Width="120">Stop Playback</TextBlock>
|
||||
<ComboBox Grid.Row="3" Grid.Column="1" Name="StopPlaybackKeyBind"
|
||||
Items="{Binding KeyboardList}" SelectedItem="{Binding StopPlaybackKey, Mode=TwoWay}"
|
||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||
<WrapPanel Margin="0,0,0,16">
|
||||
<TextBlock Width="120">Stop Playback</TextBlock>
|
||||
<ComboBox Name="StopPlaybackKeyBind"
|
||||
Items="{Binding KeyboardList}" SelectedItem="{Binding StopPlaybackKey, Mode=TwoWay}"
|
||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||
</WrapPanel>
|
||||
|
||||
<!-- Eject Disc-->
|
||||
<TextBlock Grid.Row="4" Grid.Column="0" Width="120">Eject Disc</TextBlock>
|
||||
<ComboBox Grid.Row="4" Grid.Column="1" Name="EjectKeyBind"
|
||||
Items="{Binding KeyboardList}" SelectedItem="{Binding EjectKey, Mode=TwoWay}"
|
||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||
<WrapPanel Margin="0,0,0,16">
|
||||
<TextBlock Width="120">Eject Disc</TextBlock>
|
||||
<ComboBox Name="EjectKeyBind"
|
||||
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 -->
|
||||
<TextBlock Grid.Row="5" Grid.Column="0" Width="120">Next Track</TextBlock>
|
||||
<ComboBox Grid.Row="5" Grid.Column="1" Name="NextTrackKeyBind"
|
||||
Items="{Binding KeyboardList}" SelectedItem="{Binding NextTrackKey, Mode=TwoWay}"
|
||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||
<WrapPanel Margin="0,0,0,16">
|
||||
<TextBlock Width="120">Next Track</TextBlock>
|
||||
<ComboBox Name="NextTrackKeyBind"
|
||||
Items="{Binding KeyboardList}" SelectedItem="{Binding NextTrackKey, Mode=TwoWay}"
|
||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||
</WrapPanel>
|
||||
|
||||
<!-- Previous Track -->
|
||||
<TextBlock Grid.Row="6" Grid.Column="0" Width="120">Previous Track</TextBlock>
|
||||
<ComboBox Grid.Row="6" Grid.Column="1" Name="PreviousTrackKeyBind"
|
||||
Items="{Binding KeyboardList}" SelectedItem="{Binding PreviousTrackKey, Mode=TwoWay}"
|
||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||
<WrapPanel Margin="0,0,0,16">
|
||||
<TextBlock Width="120">Previous Track</TextBlock>
|
||||
<ComboBox Name="PreviousTrackKeyBind"
|
||||
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 -->
|
||||
<TextBlock Grid.Row="7" Grid.Column="0" Width="120">Next Index</TextBlock>
|
||||
<ComboBox Grid.Row="7" Grid.Column="1" Name="NextIndexKeyBind"
|
||||
Items="{Binding KeyboardList}" SelectedItem="{Binding NextIndexKey, Mode=TwoWay}"
|
||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||
<WrapPanel Margin="0,0,0,16">
|
||||
<TextBlock Width="120">Next Index</TextBlock>
|
||||
<ComboBox Name="NextIndexKeyBind"
|
||||
Items="{Binding KeyboardList}" SelectedItem="{Binding NextIndexKey, Mode=TwoWay}"
|
||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||
</WrapPanel>
|
||||
|
||||
<!-- Previous Index -->
|
||||
<TextBlock Grid.Row="8" Grid.Column="0" Width="120">Previous Index</TextBlock>
|
||||
<ComboBox Grid.Row="8" Grid.Column="1" Name="PreviousIndexKeyBind"
|
||||
Items="{Binding KeyboardList}" SelectedItem="{Binding PreviousIndexKey, Mode=TwoWay}"
|
||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||
<WrapPanel Margin="0,0,0,16">
|
||||
<TextBlock Width="120">Previous Index</TextBlock>
|
||||
<ComboBox Name="PreviousIndexKeyBind"
|
||||
Items="{Binding KeyboardList}" SelectedItem="{Binding PreviousIndexKey, Mode=TwoWay}"
|
||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||
</WrapPanel>
|
||||
|
||||
<!-- Fast Forward -->
|
||||
<TextBlock Grid.Row="9" Grid.Column="0" Width="120">Fast-Forward</TextBlock>
|
||||
<ComboBox Grid.Row="9" Grid.Column="1" Name="FastForwardPlaybackKeyBind"
|
||||
Items="{Binding KeyboardList}" SelectedItem="{Binding FastForwardPlaybackKey, Mode=TwoWay}"
|
||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||
<WrapPanel Margin="0,0,0,16">
|
||||
<TextBlock Width="120">Fast-Forward</TextBlock>
|
||||
<ComboBox Name="FastForwardPlaybackKeyBind"
|
||||
Items="{Binding KeyboardList}" SelectedItem="{Binding FastForwardPlaybackKey, Mode=TwoWay}"
|
||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||
</WrapPanel>
|
||||
|
||||
<!-- Rewind -->
|
||||
<TextBlock Grid.Row="10" Grid.Column="0" Width="120">Rewind</TextBlock>
|
||||
<ComboBox Grid.Row="10" Grid.Column="1" Name="RewindPlaybackKeyBind"
|
||||
Items="{Binding KeyboardList}" SelectedItem="{Binding RewindPlaybackKey, Mode=TwoWay}"
|
||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||
<WrapPanel Margin="0,0,0,16">
|
||||
<TextBlock Width="120">Rewind</TextBlock>
|
||||
<ComboBox Name="RewindPlaybackKeyBind"
|
||||
Items="{Binding KeyboardList}" SelectedItem="{Binding RewindPlaybackKey, Mode=TwoWay}"
|
||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||
</WrapPanel>
|
||||
|
||||
<!-- Volume Up -->
|
||||
<TextBlock Grid.Row="11" Grid.Column="0" Width="120">Volume Up</TextBlock>
|
||||
<ComboBox Grid.Row="11" Grid.Column="1" Name="VolumeUpKeyBind"
|
||||
Items="{Binding KeyboardList}" SelectedItem="{Binding VolumeUpKey, Mode=TwoWay}"
|
||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||
<WrapPanel Margin="0,0,0,16">
|
||||
<TextBlock Width="120">Volume Up</TextBlock>
|
||||
<ComboBox Name="VolumeUpKeyBind"
|
||||
Items="{Binding KeyboardList}" SelectedItem="{Binding VolumeUpKey, Mode=TwoWay}"
|
||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||
</WrapPanel>
|
||||
|
||||
<!-- Volume Down -->
|
||||
<TextBlock Grid.Row="12" Grid.Column="0" Width="120">Volume Down</TextBlock>
|
||||
<ComboBox Grid.Row="12" Grid.Column="1" Name="VolumeDownKeyBind"
|
||||
Items="{Binding KeyboardList}" SelectedItem="{Binding VolumeDownKey, Mode=TwoWay}"
|
||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||
<WrapPanel Margin="0,0,0,16">
|
||||
<TextBlock Width="120">Volume Down</TextBlock>
|
||||
<ComboBox Name="VolumeDownKeyBind"
|
||||
Items="{Binding KeyboardList}" SelectedItem="{Binding VolumeDownKey, Mode=TwoWay}"
|
||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||
</WrapPanel>
|
||||
|
||||
<!-- Mute Toggle -->
|
||||
<TextBlock Grid.Row="13" Grid.Column="0" Width="120">Toggle Mute</TextBlock>
|
||||
<ComboBox Grid.Row="13" Grid.Column="1" Name="ToggleMuteKeyBind"
|
||||
Items="{Binding KeyboardList}" SelectedItem="{Binding ToggleMuteKey, Mode=TwoWay}"
|
||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||
<WrapPanel Margin="0,0,0,16">
|
||||
<TextBlock Width="120">Toggle Mute</TextBlock>
|
||||
<ComboBox Name="ToggleMuteKeyBind"
|
||||
Items="{Binding KeyboardList}" SelectedItem="{Binding ToggleMuteKey, Mode=TwoWay}"
|
||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||
</WrapPanel>
|
||||
|
||||
<!-- De-Emphasis Toggle -->
|
||||
<TextBlock Grid.Row="14" Grid.Column="0" Width="120">Toggle De-Emphasis</TextBlock>
|
||||
<ComboBox Grid.Row="14" Grid.Column="1" Name="ToggleDeEmphasisKeyBind"
|
||||
Items="{Binding KeyboardList}" SelectedItem="{Binding ToggleDeEmphasisKey, Mode=TwoWay}"
|
||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||
</Grid>
|
||||
<WrapPanel Margin="0,0,0,16">
|
||||
<TextBlock Width="120">Toggle De-Emphasis</TextBlock>
|
||||
<ComboBox Name="ToggleDeEmphasisKeyBind"
|
||||
Items="{Binding KeyboardList}" SelectedItem="{Binding ToggleDeEmphasisKey, Mode=TwoWay}"
|
||||
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||
</WrapPanel>
|
||||
</StackPanel>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
<Button Name="ApplyButton" Command="{Binding ApplySettingsCommand}">Apply</Button>
|
||||
|
||||
@@ -8,19 +8,19 @@
|
||||
<viewModels:PlayerViewModel/>
|
||||
</ReactiveUserControl.ViewModel>
|
||||
<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">
|
||||
<Button Command="{Binding PlayCommand}" 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 StopCommand}" Width="100" Margin="0,0,16,0">Stop</Button>
|
||||
<Button Command="{Binding PlayCommand}" Focusable="False" Width="100" Margin="0,0,16,0">Play</Button>
|
||||
<Button Command="{Binding PauseCommand}" Focusable="False" Width="100" Margin="0,0,16,0">Pause</Button>
|
||||
<Button Command="{Binding StopCommand}" Focusable="False" Width="100" Margin="0,0,16,0">Stop</Button>
|
||||
</StackPanel>
|
||||
<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 NextTrackCommand}" 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 NextIndexCommand}" 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 FastForwardCommand}" Width="100">Fast Forward</RepeatButton>
|
||||
<Button Command="{Binding PreviousTrackCommand}" Focusable="False" Width="100" Margin="0,0,16,0">Previous Track</Button>
|
||||
<Button Command="{Binding NextTrackCommand}" Focusable="False" Width="100" Margin="0,0,16,0">Next Track</Button>
|
||||
<Button Command="{Binding PreviousIndexCommand}" Focusable="False" Width="100" Margin="0,0,16,0">Previous Index</Button>
|
||||
<Button Command="{Binding NextIndexCommand}" Focusable="False" Width="100" Margin="0,0,16,0">Next Index</Button>
|
||||
<RepeatButton Command="{Binding RewindCommand}" Focusable="False" Width="100" Margin="0,0,16,0">Rewind</RepeatButton>
|
||||
<RepeatButton Command="{Binding FastForwardCommand}" Focusable="False" Width="100">Fast Forward</RepeatButton>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
||||
<StackPanel Margin="0,0,32,0">
|
||||
@@ -81,12 +81,12 @@
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
||||
<Button Command="{Binding EnableDeEmphasisCommand}" IsVisible="{Binding !ApplyDeEmphasis}" Width="200"
|
||||
Margin="0,0,16,0">
|
||||
<Button Command="{Binding EnableDeEmphasisCommand}" Focusable="False" IsVisible="{Binding !ApplyDeEmphasis}"
|
||||
Width="200" Margin="0,0,16,0">
|
||||
Enable De-Emphasis
|
||||
</Button>
|
||||
<Button Command="{Binding DisableDeEmphasisCommand}" IsVisible="{Binding ApplyDeEmphasis}" Width="200"
|
||||
Margin="0,0,16,0">
|
||||
<Button Command="{Binding DisableDeEmphasisCommand}" Focusable="False" IsVisible="{Binding ApplyDeEmphasis}"
|
||||
Width="200" Margin="0,0,16,0">
|
||||
Disable De-Emphasis
|
||||
</Button>
|
||||
</StackPanel>
|
||||
@@ -103,7 +103,8 @@
|
||||
<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" 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>
|
||||
</ReactiveUserControl>
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using NWaves.Filters.BiQuad;
|
||||
|
||||
namespace RedBookPlayer.Models.Hardware
|
||||
namespace RedBookPlayer.Models.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// Filter for applying de-emphasis to audio
|
||||
@@ -1,7 +1,7 @@
|
||||
using NWaves.Audio;
|
||||
using NWaves.Filters.BiQuad;
|
||||
|
||||
namespace RedBookPlayer.Models.Hardware
|
||||
namespace RedBookPlayer.Models.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// 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 WaveFormat = CSCore.WaveFormat;
|
||||
|
||||
namespace RedBookPlayer.Models.Hardware
|
||||
namespace RedBookPlayer.Models.Audio
|
||||
{
|
||||
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)
|
||||
return;
|
||||
|
||||
// Data tracks only and flag disabled means we can't do anything
|
||||
if(_image.Tracks.All(t => t.TrackType != TrackType.Audio) && DataPlayback == DataPlayback.Skip)
|
||||
// Invalid value means we can't do anything
|
||||
if(value > _image.Tracks.Max(t => t.TrackSequence))
|
||||
return;
|
||||
else if(value < _image.Tracks.Min(t => t.TrackSequence))
|
||||
return;
|
||||
|
||||
// Cache the value and the current track number
|
||||
int cachedValue = value;
|
||||
int cachedTrackNumber;
|
||||
|
||||
// Check if we're incrementing or decrementing the track
|
||||
bool increment = cachedValue >= _currentTrackNumber;
|
||||
|
||||
do
|
||||
{
|
||||
// If we're over the last track, wrap around
|
||||
if(cachedValue > _image.Tracks.Max(t => t.TrackSequence))
|
||||
{
|
||||
cachedValue = (int)_image.Tracks.Min(t => t.TrackSequence);
|
||||
if(cachedValue == 0 && !LoadHiddenTracks)
|
||||
cachedValue++;
|
||||
}
|
||||
|
||||
// If we're under the first track and we're not loading hidden tracks, wrap around
|
||||
else if(cachedValue < 1 && !LoadHiddenTracks)
|
||||
{
|
||||
cachedValue = (int)_image.Tracks.Max(t => t.TrackSequence);
|
||||
}
|
||||
|
||||
// If we're under the first valid track, wrap around
|
||||
else if(cachedValue < _image.Tracks.Min(t => t.TrackSequence))
|
||||
{
|
||||
cachedValue = (int)_image.Tracks.Max(t => t.TrackSequence);
|
||||
}
|
||||
|
||||
cachedTrackNumber = cachedValue;
|
||||
|
||||
// Cache the current track for easy access
|
||||
Track track = GetTrack(cachedTrackNumber);
|
||||
if(track == null)
|
||||
return;
|
||||
|
||||
// Set track flags from subchannel data, if possible
|
||||
SetTrackFlags(track);
|
||||
|
||||
// If the track is playable, just return
|
||||
if((TrackType == TrackType.Audio || DataPlayback != DataPlayback.Skip)
|
||||
&& (SessionHandling == SessionHandling.AllSessions || track.TrackSession == 1))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// If we're not playing the track, skip
|
||||
if(increment)
|
||||
cachedValue++;
|
||||
else
|
||||
cachedValue--;
|
||||
}
|
||||
while(cachedValue != _currentTrackNumber);
|
||||
|
||||
// If we looped around, ensure it reloads
|
||||
if(cachedValue == _currentTrackNumber)
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref _currentTrackNumber, -1);
|
||||
|
||||
Track track = GetTrack(cachedValue);
|
||||
if(track == null)
|
||||
return;
|
||||
|
||||
SetTrackFlags(track);
|
||||
}
|
||||
|
||||
this.RaiseAndSetIfChanged(ref _currentTrackNumber, cachedValue);
|
||||
|
||||
Track cachedTrack = GetTrack(cachedValue);
|
||||
if(cachedTrack == null)
|
||||
// Cache the current track for easy access
|
||||
Track track = GetTrack(value);
|
||||
if(track == null)
|
||||
return;
|
||||
|
||||
TotalIndexes = cachedTrack.Indexes.Keys.Max();
|
||||
CurrentTrackIndex = cachedTrack.Indexes.Keys.Min();
|
||||
CurrentTrackSession = cachedTrack.TrackSession;
|
||||
// Set all track flags and values
|
||||
SetTrackFlags(track);
|
||||
TotalIndexes = track.Indexes.Keys.Max();
|
||||
CurrentTrackIndex = track.Indexes.Keys.Min();
|
||||
CurrentTrackSession = track.TrackSession;
|
||||
|
||||
// Mark the track as changed
|
||||
this.RaiseAndSetIfChanged(ref _currentTrackNumber, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,14 +64,13 @@ namespace RedBookPlayer.Models.Discs
|
||||
if(track == null)
|
||||
return;
|
||||
|
||||
// Ensure that the value is valid, wrapping around if necessary
|
||||
ushort fixedValue = value;
|
||||
// Invalid value means we can't do anything
|
||||
if(value > track.Indexes.Keys.Max())
|
||||
fixedValue = track.Indexes.Keys.Min();
|
||||
return;
|
||||
else if(value < track.Indexes.Keys.Min())
|
||||
fixedValue = track.Indexes.Keys.Max();
|
||||
return;
|
||||
|
||||
this.RaiseAndSetIfChanged(ref _currentTrackIndex, fixedValue);
|
||||
this.RaiseAndSetIfChanged(ref _currentTrackIndex, value);
|
||||
|
||||
// Set new index-specific data
|
||||
SectionStartSector = (ulong)track.Indexes[CurrentTrackIndex];
|
||||
@@ -156,19 +95,18 @@ namespace RedBookPlayer.Models.Discs
|
||||
if(_image == null)
|
||||
return;
|
||||
|
||||
// If the sector is over the end of the image, then loop
|
||||
ulong tempSector = value;
|
||||
if(tempSector > _image.Info.Sectors)
|
||||
tempSector = 0;
|
||||
else if(tempSector < 0)
|
||||
tempSector = _image.Info.Sectors - 1;
|
||||
// Invalid value means we can't do anything
|
||||
if(value > _image.Info.Sectors)
|
||||
return;
|
||||
else if(value < 0)
|
||||
return;
|
||||
|
||||
// Cache the current track for easy access
|
||||
Track track = GetTrack(CurrentTrackNumber);
|
||||
if(track == null)
|
||||
return;
|
||||
|
||||
this.RaiseAndSetIfChanged(ref _currentSector, tempSector);
|
||||
this.RaiseAndSetIfChanged(ref _currentSector, value);
|
||||
|
||||
// If the current sector is outside of the last known track, seek to the right one
|
||||
if(CurrentSector < track.TrackStartSector || CurrentSector > track.TrackEndSector)
|
||||
@@ -194,6 +132,11 @@ namespace RedBookPlayer.Models.Discs
|
||||
/// <inheritdoc/>
|
||||
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>
|
||||
/// Represents the 4CH flag
|
||||
/// </summary>
|
||||
@@ -230,21 +173,6 @@ namespace RedBookPlayer.Models.Discs
|
||||
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 _isDataTrack;
|
||||
private bool _copyAllowed;
|
||||
@@ -290,30 +218,26 @@ namespace RedBookPlayer.Models.Discs
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="options">Set of options for a new disc</param>
|
||||
public CompactDisc(OpticalDiscOptions options)
|
||||
{
|
||||
DataPlayback = options.DataPlayback;
|
||||
_generateMissingToc = options.GenerateMissingToc;
|
||||
LoadHiddenTracks = options.LoadHiddenTracks;
|
||||
SessionHandling = options.SessionHandling;
|
||||
}
|
||||
public CompactDisc(OpticalDiscOptions options) => _generateMissingToc = options.GenerateMissingToc;
|
||||
|
||||
/// <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(image == null)
|
||||
return;
|
||||
|
||||
// Set the current disc image
|
||||
ImagePath = path;
|
||||
_image = image;
|
||||
|
||||
// Attempt to load the TOC
|
||||
if(!LoadTOC())
|
||||
return;
|
||||
|
||||
// Load the first track
|
||||
LoadFirstTrack();
|
||||
// Load the first track by default
|
||||
CurrentTrackNumber = 1;
|
||||
LoadTrack(CurrentTrackNumber);
|
||||
|
||||
// Reset total indexes if not in autoplay
|
||||
if(!autoPlay)
|
||||
@@ -329,134 +253,32 @@ namespace RedBookPlayer.Models.Discs
|
||||
Initialized = true;
|
||||
}
|
||||
|
||||
#region Seeking
|
||||
|
||||
/// <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
|
||||
|
||||
/// <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)
|
||||
return;
|
||||
|
||||
// Get the track with that value, if possible
|
||||
Track track = _image.Tracks.FirstOrDefault(t => t.TrackSequence == trackNumber);
|
||||
|
||||
// If the track isn't valid, we can't do anything
|
||||
if(track == null || !(DataPlayback != DataPlayback.Skip || track.TrackType == TrackType.Audio))
|
||||
if(track == null)
|
||||
return;
|
||||
|
||||
// Get the number of sectors to read
|
||||
uint length = (uint)(track.TrackEndSector - track.TrackStartSector);
|
||||
|
||||
// Read in the track data to a buffer
|
||||
byte[] buffer = ReadSectors(track.TrackStartSector, length);
|
||||
byte[] buffer = ReadSectors(track.TrackStartSector, length, dataPlayback);
|
||||
|
||||
// Build the WAV output
|
||||
string filename = Path.Combine(outputDirectory, $"Track {trackNumber.ToString().PadLeft(2, '0')}.wav");
|
||||
@@ -470,15 +292,20 @@ namespace RedBookPlayer.Models.Discs
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void ExtractAllTracksToWav(string outputDirectory)
|
||||
/// <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>
|
||||
public Track GetTrack(int trackNumber)
|
||||
{
|
||||
if(_image == null)
|
||||
return;
|
||||
|
||||
foreach(Track track in _image.Tracks)
|
||||
try
|
||||
{
|
||||
ExtractTrackToWav(track.TrackSequence, outputDirectory);
|
||||
return _image.Tracks.FirstOrDefault(t => t.TrackSequence == trackNumber);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -497,31 +324,65 @@ namespace RedBookPlayer.Models.Discs
|
||||
|
||||
// Select the first index that has a sector offset greater than or equal to 0
|
||||
CurrentSector = (ulong)(track?.Indexes.OrderBy(kvp => kvp.Key).First(kvp => kvp.Value >= 0).Value ?? 0);
|
||||
|
||||
// Load and debug output
|
||||
uint sectorCount = (uint)(track.TrackEndSector - track.TrackStartSector);
|
||||
byte[] trackData = ReadSectors(sectorCount);
|
||||
Console.WriteLine($"DEBUG: Track {trackNumber} - {sectorCount} sectors / {trackData.Length} bytes");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void LoadFirstTrack()
|
||||
public override void LoadIndex(ushort index)
|
||||
{
|
||||
CurrentTrackNumber = 1;
|
||||
LoadTrack(CurrentTrackNumber);
|
||||
if(_image == null)
|
||||
return;
|
||||
|
||||
// Cache the current track for easy access
|
||||
Track track = GetTrack(CurrentTrackNumber);
|
||||
if(track == null)
|
||||
return;
|
||||
|
||||
// If the index is invalid, just return
|
||||
if(index < track.Indexes.Keys.Min() || index > track.Indexes.Keys.Max())
|
||||
return;
|
||||
|
||||
// Select the first index that has a sector offset greater than or equal to 0
|
||||
CurrentSector = (ulong)track.Indexes[index];
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// Read sector 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>
|
||||
/// <param name="dataPlayback">DataPlayback value indicating how to handle data tracks</param>
|
||||
/// <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);
|
||||
}
|
||||
else if(DataPlayback == DataPlayback.Blank)
|
||||
else if(dataPlayback == DataPlayback.Blank)
|
||||
{
|
||||
byte[] sectors = _image.ReadSectors(startSector, sectorsToRead);
|
||||
Array.Clear(sectors, 0, sectors.Length);
|
||||
@@ -533,6 +394,16 @@ namespace RedBookPlayer.Models.Discs
|
||||
}
|
||||
}
|
||||
|
||||
/// <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/>
|
||||
public override void SetTotalIndexes()
|
||||
{
|
||||
@@ -542,23 +413,6 @@ namespace RedBookPlayer.Models.Discs
|
||||
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>
|
||||
/// Load TOC for the current disc image
|
||||
/// </summary>
|
||||
|
||||
@@ -8,6 +8,11 @@ namespace RedBookPlayer.Models.Discs
|
||||
{
|
||||
#region Public Fields
|
||||
|
||||
/// <summary>
|
||||
/// Path to the disc image
|
||||
/// </summary>
|
||||
public string ImagePath { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicate if the disc is ready to be used
|
||||
/// </summary>
|
||||
@@ -93,37 +98,10 @@ namespace RedBookPlayer.Models.Discs
|
||||
/// <summary>
|
||||
/// Initialize the disc with a given image
|
||||
/// </summary>
|
||||
/// <param name="path">Path of the image</param>
|
||||
/// <param name="image">Aaruformat image to load</param>
|
||||
/// <param name="autoPlay">True if playback should begin immediately, false otherwise</param>
|
||||
public abstract void Init(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
|
||||
public abstract void Init(string path, IOpticalMediaImage image, bool autoPlay);
|
||||
|
||||
#region Helpers
|
||||
|
||||
@@ -134,12 +112,6 @@ namespace RedBookPlayer.Models.Discs
|
||||
/// <param name="outputDirectory">Output path to write data to</param>
|
||||
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>
|
||||
/// Load the desired track, if possible
|
||||
/// </summary>
|
||||
@@ -147,9 +119,10 @@ namespace RedBookPlayer.Models.Discs
|
||||
public abstract void LoadTrack(int track);
|
||||
|
||||
/// <summary>
|
||||
/// Load the first valid track in the image
|
||||
/// Load the desired index, if possible
|
||||
/// </summary>
|
||||
public abstract void LoadFirstTrack();
|
||||
/// <param name="index">Index number to load</param>
|
||||
public abstract void LoadIndex(ushort index);
|
||||
|
||||
/// <summary>
|
||||
/// Read sector data from the base image starting from the current sector
|
||||
|
||||
@@ -4,26 +4,11 @@ namespace RedBookPlayer.Models.Discs
|
||||
{
|
||||
#region CompactDisc
|
||||
|
||||
/// <summary>
|
||||
/// Indicate how data tracks should be handled
|
||||
/// </summary>
|
||||
public DataPlayback DataPlayback { get; set; } = DataPlayback.Skip;
|
||||
|
||||
/// <summary>
|
||||
/// Indicate if a TOC should be generated if missing
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,23 @@ namespace RedBookPlayer.Models
|
||||
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>
|
||||
/// Current player state
|
||||
/// </summary>
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace RedBookPlayer.Models.Factories
|
||||
image.Open(filter);
|
||||
|
||||
// Generate and instantiate the disc
|
||||
return GenerateFromImage(image, options, autoPlay);
|
||||
return GenerateFromImage(path, image, options, autoPlay);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -44,11 +44,12 @@ namespace RedBookPlayer.Models.Factories
|
||||
/// <summary>
|
||||
/// Generate an OpticalDisc from an input IOpticalMediaImage
|
||||
/// </summary>
|
||||
/// <param name="path">Path of the image</param>
|
||||
/// <param name="image">IOpticalMediaImage to create from</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>
|
||||
/// <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(!IsUsableImage(image))
|
||||
@@ -74,7 +75,7 @@ namespace RedBookPlayer.Models.Factories
|
||||
return opticalDisc;
|
||||
|
||||
// Instantiate the disc and return
|
||||
opticalDisc.Init(image, autoPlay);
|
||||
opticalDisc.Init(path, image, autoPlay);
|
||||
return opticalDisc;
|
||||
}
|
||||
|
||||
|
||||
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>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<RuntimeIdentifiers>win-x64;linux-x64</RuntimeIdentifiers>
|
||||
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user