Merge pull request #51 from mnadareski/backend-separation

Multi-Disc and Multi-Backend Improvements
This commit is contained in:
2021-11-29 18:16:51 +00:00
committed by GitHub
26 changed files with 2033 additions and 1077 deletions

View File

@@ -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

View File

@@ -9,6 +9,7 @@
<PropertyGroup Condition="'$(RuntimeIdentifier)|$(Configuration)' == 'win-x64|Debug'">
<DefineConstants>WindowsDebug</DefineConstants>
</PropertyGroup>
<ItemGroup>
<Compile Update="**\*.xaml.cs">
<DependentUpon>%(Filename)</DependentUpon>

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View 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);
}
}

View 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
}
}

View File

@@ -2,7 +2,7 @@ using System;
using CSCore;
using WaveFormat = CSCore.WaveFormat;
namespace RedBookPlayer.Models.Hardware
namespace RedBookPlayer.Models.Audio
{
public class PlayerSource : IWaveSource
{

View 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
}
}

View 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
}
}

View File

@@ -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>

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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>

View File

@@ -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

View 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;
}
}

View File

@@ -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
}
}

View 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;
}
}
}

View 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;
}
}

View File

@@ -2,6 +2,8 @@
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RuntimeIdentifiers>win-x64;linux-x64</RuntimeIdentifiers>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>