Port only MVVM work and related fixes

This commit is contained in:
Matt Nadareski
2021-08-05 21:05:20 -07:00
parent 9aa5c1ac9f
commit ace0e5c5ec
33 changed files with 1024 additions and 796 deletions

View File

@@ -4,6 +4,8 @@
* This project is fully sponsored by the [Game Preservation Society](https://www.gamepres.org/en/). * This project is fully sponsored by the [Game Preservation Society](https://www.gamepres.org/en/).
[OpenAL](https://www.openal.org/) is required to run this application. Please install it using the most recent instructions for your operating system of choice.
## Default Player Controls ## Default Player Controls
| Key | Action | | Key | Action |

View File

@@ -5,14 +5,20 @@ using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using RedBookPlayer.GUI; using RedBookPlayer.GUI.ViewModels;
using RedBookPlayer.GUI.Views; using RedBookPlayer.GUI.Views;
namespace RedBookPlayer namespace RedBookPlayer
{ {
public class App : Application public class App : Application
{ {
public static Settings Settings; public static MainWindow MainWindow;
public static SettingsViewModel Settings;
/// <summary>
/// Read-only access to the current player view
/// </summary>
public static PlayerView PlayerView => MainWindow?.ViewModel?.PlayerView;
static App() => static App() =>
Directory.SetCurrentDirectory(Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName)); Directory.SetCurrentDirectory(Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName));
@@ -24,6 +30,7 @@ namespace RedBookPlayer
Console.WriteLine(((Exception)f.ExceptionObject).ToString()); Console.WriteLine(((Exception)f.ExceptionObject).ToString());
}; };
Settings = SettingsViewModel.Load("settings.json");
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
} }
@@ -31,10 +38,11 @@ namespace RedBookPlayer
{ {
if(ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) if(ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{ {
desktop.MainWindow = new MainWindow(); MainWindow = new MainWindow();
desktop.MainWindow = MainWindow;
desktop.ShutdownMode = ShutdownMode.OnMainWindowClose; desktop.ShutdownMode = ShutdownMode.OnMainWindowClose;
Settings = Settings.Load("settings.json"); PlayerView.ViewModel.ApplyTheme(Settings.SelectedTheme);
} }
base.OnFrameworkInitializationCompleted(); base.OnFrameworkInitializationCompleted();

View File

@@ -4,6 +4,7 @@ using System.Runtime.InteropServices;
#endif #endif
using Avalonia; using Avalonia;
using Avalonia.Logging.Serilog; using Avalonia.Logging.Serilog;
using Avalonia.ReactiveUI;
namespace RedBookPlayer.GUI namespace RedBookPlayer.GUI
{ {
@@ -24,6 +25,6 @@ namespace RedBookPlayer.GUI
static extern bool AllocConsole(); static extern bool AllocConsole();
#endif #endif
public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure<App>().UsePlatformDetect().LogToDebug(); public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure<App>().UseReactiveUI().UsePlatformDetect().LogToDebug();
} }
} }

View File

@@ -13,7 +13,7 @@
<Compile Update="**\*.xaml.cs"> <Compile Update="**\*.xaml.cs">
<DependentUpon>%(Filename)</DependentUpon> <DependentUpon>%(Filename)</DependentUpon>
</Compile> </Compile>
<AvaloniaResource Include="**\*.xaml" Exclude="bin\**;obj\**"> <AvaloniaResource Include="**\*.xaml" Exclude="bin\**;obj\**;themes\**">
<SubType>Designer</SubType> <SubType>Designer</SubType>
</AvaloniaResource> </AvaloniaResource>
</ItemGroup> </ItemGroup>
@@ -28,4 +28,11 @@
<ItemGroup> <ItemGroup>
<AvaloniaResource Include="Assets\*" /> <AvaloniaResource Include="Assets\*" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Update="themes\**">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<CopyToPublishDirectory>Always</CopyToPublishDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</None>
</ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,159 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Avalonia.Controls;
using Avalonia.Input;
using RedBookPlayer.GUI.Views;
namespace RedBookPlayer.GUI.ViewModels
{
public class MainViewModel
{
/// <summary>
/// Read-only access to the control
/// </summary>
public ContentControl ContentControl => App.MainWindow.FindControl<ContentControl>("Content");
/// <summary>
/// Read-only access to the view
/// </summary>
public PlayerView PlayerView => ContentControl?.Content as PlayerView;
#region Helpers
/// <summary>
/// Execute the result of a keypress
/// </summary>
public async void ExecuteKeyPress(object sender, KeyEventArgs e)
{
// Open settings window
if(e.Key == App.Settings.OpenSettingsKey)
{
SettingsWindow settingsWindow = new SettingsWindow() { DataContext = App.Settings };
settingsWindow.Closed += OnSettingsClosed;
settingsWindow.ShowDialog(App.MainWindow);
}
// Load image
else if(e.Key == App.Settings.LoadImageKey)
{
PlayerView?.ViewModel?.ExecuteLoad();
}
// Toggle playback
else if(e.Key == App.Settings.TogglePlaybackKey || e.Key == Key.MediaPlayPause)
{
PlayerView?.ViewModel?.ExecuteTogglePlayPause();
}
// Stop playback
else if(e.Key == App.Settings.StopPlaybackKey || e.Key == Key.MediaStop)
{
PlayerView?.ViewModel?.ExecuteStop();
}
// Next Track
else if(e.Key == App.Settings.NextTrackKey || e.Key == Key.MediaNextTrack)
{
PlayerView?.ViewModel?.ExecuteNextTrack();
}
// Previous Track
else if(e.Key == App.Settings.PreviousTrackKey || e.Key == Key.MediaPreviousTrack)
{
PlayerView?.ViewModel?.ExecutePreviousTrack();
}
// Next Index
else if(e.Key == App.Settings.NextIndexKey)
{
PlayerView?.ViewModel?.ExecuteNextIndex();
}
// Previous Index
else if(e.Key == App.Settings.PreviousIndexKey)
{
PlayerView?.ViewModel?.ExecutePreviousIndex();
}
// Fast Foward
else if(e.Key == App.Settings.FastForwardPlaybackKey)
{
PlayerView?.ViewModel?.ExecuteFastForward();
}
// Rewind
else if(e.Key == App.Settings.RewindPlaybackKey)
{
PlayerView?.ViewModel?.ExecuteRewind();
}
// Volume Up
else if(e.Key == App.Settings.VolumeUpKey || e.Key == Key.VolumeUp)
{
int increment = 1;
if(e.KeyModifiers.HasFlag(KeyModifiers.Control))
increment *= 2;
if(e.KeyModifiers.HasFlag(KeyModifiers.Shift))
increment *= 5;
if(PlayerView?.ViewModel?.Volume != null)
PlayerView.ViewModel.ExecuteSetVolume(PlayerView.ViewModel.Volume + increment);
}
// Volume Down
else if(e.Key == App.Settings.VolumeDownKey || e.Key == Key.VolumeDown)
{
int decrement = 1;
if(e.KeyModifiers.HasFlag(KeyModifiers.Control))
decrement *= 2;
if(e.KeyModifiers.HasFlag(KeyModifiers.Shift))
decrement *= 5;
if(PlayerView?.ViewModel?.Volume != null)
PlayerView.ViewModel.ExecuteSetVolume(PlayerView.ViewModel.Volume - decrement);
}
// Mute Toggle
else if(e.Key == App.Settings.ToggleMuteKey || e.Key == Key.VolumeMute)
{
PlayerView?.ViewModel?.ExecuteToggleMute();
}
// Emphasis Toggle
else if(e.Key == App.Settings.ToggleDeEmphasisKey)
{
PlayerView?.ViewModel?.ExecuteToggleDeEmphasis();
}
}
/// <summary>
/// Load the first valid drag-and-dropped disc image
/// </summary>
public async void ExecuteLoadDragDrop(object sender, DragEventArgs e)
{
if(PlayerView?.ViewModel == null)
return;
IEnumerable<string> fileNames = e.Data.GetFileNames();
foreach(string filename in fileNames)
{
bool loaded = await PlayerView.ViewModel.LoadImage(filename);
if(loaded)
break;
}
}
/// <summary>
/// Stop current playback
/// </summary>
public void ExecuteStop(object sender, CancelEventArgs e) => PlayerView?.ViewModel?.ExecuteStop();
/// <summary>
/// Handle the settings window closing
/// </summary>
private void OnSettingsClosed(object sender, EventArgs e) => PlayerView?.ViewModel?.RefreshFromSettings();
#endregion
}
}

View File

@@ -5,13 +5,16 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Reactive; using System.Reactive;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Xml;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Threading; using Avalonia.Threading;
using ReactiveUI; using ReactiveUI;
using RedBookPlayer.GUI.Views; using RedBookPlayer.Models;
using RedBookPlayer.Models.Discs;
using RedBookPlayer.Models.Hardware; using RedBookPlayer.Models.Hardware;
namespace RedBookPlayer.GUI.ViewModels namespace RedBookPlayer.GUI.ViewModels
@@ -21,7 +24,7 @@ namespace RedBookPlayer.GUI.ViewModels
/// <summary> /// <summary>
/// Player representing the internal state /// Player representing the internal state
/// </summary> /// </summary>
private Player _player; private readonly Player _player;
/// <summary> /// <summary>
/// Set of images representing the digits for the UI /// Set of images representing the digits for the UI
@@ -50,6 +53,15 @@ namespace RedBookPlayer.GUI.ViewModels
private set => this.RaiseAndSetIfChanged(ref _currentTrackIndex, value); private set => this.RaiseAndSetIfChanged(ref _currentTrackIndex, value);
} }
/// <summary>
/// Current track session
/// </summary>
public ushort CurrentTrackSession
{
get => _currentTrackSession;
private set => this.RaiseAndSetIfChanged(ref _currentTrackSession, value);
}
/// <summary> /// <summary>
/// Current sector number /// Current sector number
/// </summary> /// </summary>
@@ -140,6 +152,7 @@ namespace RedBookPlayer.GUI.ViewModels
private int _currentTrackNumber; private int _currentTrackNumber;
private ushort _currentTrackIndex; private ushort _currentTrackIndex;
private ushort _currentTrackSession;
private ulong _currentSector; private ulong _currentSector;
private ulong _sectionStartSector; private ulong _sectionStartSector;
@@ -156,15 +169,28 @@ namespace RedBookPlayer.GUI.ViewModels
/// <summary> /// <summary>
/// Indicate if the model is ready to be used /// Indicate if the model is ready to be used
/// </summary> /// </summary>
public bool Initialized => _player?.Initialized ?? false; public bool Initialized
{
get => _initialized;
private set => this.RaiseAndSetIfChanged(ref _initialized, value);
}
/// <summary> /// <summary>
/// Indicate if the output is playing /// Indicate if the output is playing
/// </summary> /// </summary>
public bool? Playing public PlayerState PlayerState
{ {
get => _playing; get => _playerState;
private set => this.RaiseAndSetIfChanged(ref _playing, value); private set => this.RaiseAndSetIfChanged(ref _playerState, value);
}
/// <summary>
/// Indicates how to handle playback of data tracks
/// </summary>
public DataPlayback DataPlayback
{
get => _dataPlayback;
private set => this.RaiseAndSetIfChanged(ref _dataPlayback, value);
} }
/// <summary> /// <summary>
@@ -185,7 +211,9 @@ namespace RedBookPlayer.GUI.ViewModels
private set => this.RaiseAndSetIfChanged(ref _volume, value); private set => this.RaiseAndSetIfChanged(ref _volume, value);
} }
private bool? _playing; private bool _initialized;
private PlayerState _playerState;
private DataPlayback _dataPlayback;
private bool _applyDeEmphasis; private bool _applyDeEmphasis;
private int _volume; private int _volume;
@@ -299,6 +327,7 @@ namespace RedBookPlayer.GUI.ViewModels
/// </summary> /// </summary>
public PlayerViewModel() public PlayerViewModel()
{ {
// Initialize commands
LoadCommand = ReactiveCommand.Create(ExecuteLoad); LoadCommand = ReactiveCommand.Create(ExecuteLoad);
PlayCommand = ReactiveCommand.Create(ExecutePlay); PlayCommand = ReactiveCommand.Create(ExecutePlay);
@@ -319,25 +348,27 @@ namespace RedBookPlayer.GUI.ViewModels
EnableDeEmphasisCommand = ReactiveCommand.Create(ExecuteEnableDeEmphasis); EnableDeEmphasisCommand = ReactiveCommand.Create(ExecuteEnableDeEmphasis);
DisableDeEmphasisCommand = ReactiveCommand.Create(ExecuteDisableDeEmphasis); DisableDeEmphasisCommand = ReactiveCommand.Create(ExecuteDisableDeEmphasis);
ToggleDeEmphasisCommand = ReactiveCommand.Create(ExecuteToggleDeEmphasis); ToggleDeEmphasisCommand = ReactiveCommand.Create(ExecuteToggleDeEmphasis);
// Initialize Player
_player = new Player(App.Settings.Volume);
PlayerState = PlayerState.NoDisc;
} }
/// <summary> /// <summary>
/// Initialize the view model with a given image path /// Initialize the view model with a given image path
/// </summary> /// </summary>
/// <param name="path">Path to the disc image</param> /// <param name="path">Path to the disc image</param>
/// <param name="generateMissingToc">Generate a TOC if the disc is missing one [CompactDisc only]</param> /// <param name="options">Options to pass to the optical disc factory</param>
/// <param name="loadHiddenTracks">Load hidden tracks for playback [CompactDisc only]</param>
/// <param name="loadDataTracks">Load data tracks for playback [CompactDisc only]</param>
/// <param name="autoPlay">True if playback should begin immediately, false otherwise</param> /// <param name="autoPlay">True if playback should begin immediately, false otherwise</param>
/// <param name="defaultVolume">Default volume between 0 and 100 to use when starting playback</param> public void Init(string path, OpticalDiscOptions options, bool autoPlay)
public void Init(string path, bool generateMissingToc, bool loadHiddenTracks, bool loadDataTracks, bool autoPlay, int defaultVolume)
{ {
// Stop current playback, if necessary // Stop current playback, if necessary
if(Playing != null) ExecuteStop(); if(PlayerState != PlayerState.NoDisc)
ExecuteStop();
// Create and attempt to initialize new Player // Attempt to initialize Player
_player = new Player(path, generateMissingToc, loadHiddenTracks, loadDataTracks, autoPlay, defaultVolume); _player.Init(path, options, autoPlay);
if(Initialized) if(_player.Initialized)
{ {
_player.PropertyChanged += PlayerStateChanged; _player.PropertyChanged += PlayerStateChanged;
PlayerStateChanged(this, null); PlayerStateChanged(this, null);
@@ -444,6 +475,46 @@ namespace RedBookPlayer.GUI.ViewModels
#region Helpers #region Helpers
/// <summary>
/// Apply a custom theme to the player
/// </summary>
/// <param name="theme">Path to the theme under the themes directory</param>
public void ApplyTheme(string theme)
{
// If the PlayerView isn't set, don't do anything
if(App.PlayerView == null)
return;
// If no theme path is provided, we can ignore
if(string.IsNullOrWhiteSpace(theme))
return;
string themeDirectory = $"{Directory.GetCurrentDirectory()}/themes/{theme}";
string xamlPath = $"{themeDirectory}/view.xaml";
if(!File.Exists(xamlPath))
{
Console.WriteLine("Warning: specified theme doesn't exist, reverting to default");
return;
}
try
{
string xaml = File.ReadAllText(xamlPath);
xaml = xaml.Replace("Source=\"", $"Source=\"file://{themeDirectory}/");
LoadTheme(xaml);
}
catch(XmlException ex)
{
Console.WriteLine($"Error: invalid theme XAML ({ex.Message}), reverting to default");
LoadTheme(null);
}
App.MainWindow.Width = App.PlayerView.Width;
App.MainWindow.Height = App.PlayerView.Height;
InitializeDigits();
}
/// <summary> /// <summary>
/// Load a disc image from a selection box /// Load a disc image from a selection box
/// </summary> /// </summary>
@@ -461,35 +532,36 @@ namespace RedBookPlayer.GUI.ViewModels
/// </summary> /// </summary>
public void InitializeDigits() public void InitializeDigits()
{ {
PlayerView playerView = MainWindow.Instance.ContentControl.Content as PlayerView; if(App.PlayerView == null)
return;
_digits = new Image[] _digits = new Image[]
{ {
playerView.FindControl<Image>("TrackDigit1"), App.PlayerView.FindControl<Image>("TrackDigit1"),
playerView.FindControl<Image>("TrackDigit2"), App.PlayerView.FindControl<Image>("TrackDigit2"),
playerView.FindControl<Image>("IndexDigit1"), App.PlayerView.FindControl<Image>("IndexDigit1"),
playerView.FindControl<Image>("IndexDigit2"), App.PlayerView.FindControl<Image>("IndexDigit2"),
playerView.FindControl<Image>("TimeDigit1"), App.PlayerView.FindControl<Image>("TimeDigit1"),
playerView.FindControl<Image>("TimeDigit2"), App.PlayerView.FindControl<Image>("TimeDigit2"),
playerView.FindControl<Image>("TimeDigit3"), App.PlayerView.FindControl<Image>("TimeDigit3"),
playerView.FindControl<Image>("TimeDigit4"), App.PlayerView.FindControl<Image>("TimeDigit4"),
playerView.FindControl<Image>("TimeDigit5"), App.PlayerView.FindControl<Image>("TimeDigit5"),
playerView.FindControl<Image>("TimeDigit6"), App.PlayerView.FindControl<Image>("TimeDigit6"),
playerView.FindControl<Image>("TotalTracksDigit1"), App.PlayerView.FindControl<Image>("TotalTracksDigit1"),
playerView.FindControl<Image>("TotalTracksDigit2"), App.PlayerView.FindControl<Image>("TotalTracksDigit2"),
playerView.FindControl<Image>("TotalIndexesDigit1"), App.PlayerView.FindControl<Image>("TotalIndexesDigit1"),
playerView.FindControl<Image>("TotalIndexesDigit2"), App.PlayerView.FindControl<Image>("TotalIndexesDigit2"),
playerView.FindControl<Image>("TotalTimeDigit1"), App.PlayerView.FindControl<Image>("TotalTimeDigit1"),
playerView.FindControl<Image>("TotalTimeDigit2"), App.PlayerView.FindControl<Image>("TotalTimeDigit2"),
playerView.FindControl<Image>("TotalTimeDigit3"), App.PlayerView.FindControl<Image>("TotalTimeDigit3"),
playerView.FindControl<Image>("TotalTimeDigit4"), App.PlayerView.FindControl<Image>("TotalTimeDigit4"),
playerView.FindControl<Image>("TotalTimeDigit5"), App.PlayerView.FindControl<Image>("TotalTimeDigit5"),
playerView.FindControl<Image>("TotalTimeDigit6"), App.PlayerView.FindControl<Image>("TotalTimeDigit6"),
}; };
} }
@@ -501,19 +573,39 @@ namespace RedBookPlayer.GUI.ViewModels
{ {
return await Dispatcher.UIThread.InvokeAsync(() => return await Dispatcher.UIThread.InvokeAsync(() =>
{ {
Init(path, App.Settings.GenerateMissingTOC, App.Settings.PlayHiddenTracks, App.Settings.PlayDataTracks, App.Settings.AutoPlay, App.Settings.Volume); OpticalDiscOptions options = new OpticalDiscOptions
{
DataPlayback = App.Settings.DataPlayback,
GenerateMissingToc = App.Settings.GenerateMissingTOC,
LoadHiddenTracks = App.Settings.PlayHiddenTracks,
};
// Ensure the context and view model are set
App.PlayerView.DataContext = this;
App.PlayerView.ViewModel = this;
Init(path, options, App.Settings.AutoPlay);
if(Initialized) if(Initialized)
MainWindow.Instance.Title = "RedBookPlayer - " + path.Split('/').Last().Split('\\').Last(); App.MainWindow.Title = "RedBookPlayer - " + path.Split('/').Last().Split('\\').Last();
return Initialized; return Initialized;
}); });
} }
/// <summary> /// <summary>
/// Set the value for loading data tracks [CompactDisc only] /// Refresh the view model from the current settings
/// </summary> /// </summary>
/// <param name="load">True to enable loading data tracks, false otherwise</param> public void RefreshFromSettings()
public void SetLoadDataTracks(bool load) => _player?.SetLoadDataTracks(load); {
SetDataPlayback(App.Settings.DataPlayback);
SetLoadHiddenTracks(App.Settings.PlayHiddenTracks);
}
/// <summary>
/// Set data playback method [CompactDisc only]
/// </summary>
/// <param name="dataPlayback">New playback value</param>
public void SetDataPlayback(DataPlayback dataPlayback) => _player?.SetDataPlayback(dataPlayback);
/// <summary> /// <summary>
/// Set the value for loading hidden tracks [CompactDisc only] /// Set the value for loading hidden tracks [CompactDisc only]
@@ -569,18 +661,9 @@ namespace RedBookPlayer.GUI.ViewModels
{ {
try try
{ {
if(App.Settings.SelectedTheme == "default") string themeDirectory = $"{Directory.GetCurrentDirectory()}/themes/{App.Settings.SelectedTheme}";
{ using FileStream stream = File.Open($"{themeDirectory}/{character}.png", FileMode.Open);
IAssetLoader assets = AvaloniaLocator.Current.GetService<IAssetLoader>(); return new Bitmap(stream);
return new Bitmap(assets.Open(new Uri($"avares://RedBookPlayer/Assets/{character}.png")));
}
else
{
string themeDirectory = $"{Directory.GetCurrentDirectory()}/themes/{App.Settings.SelectedTheme}";
using FileStream stream = File.Open($"{themeDirectory}/{character}.png", FileMode.Open);
return new Bitmap(stream);
}
} }
catch catch
{ {
@@ -619,18 +702,56 @@ namespace RedBookPlayer.GUI.ViewModels
Extensions = knownExtensions.ConvertAll(e => e.TrimStart('.')) Extensions = knownExtensions.ConvertAll(e => e.TrimStart('.'))
}); });
return (await dialog.ShowAsync(MainWindow.Instance))?.FirstOrDefault(); return (await dialog.ShowAsync(App.MainWindow))?.FirstOrDefault();
}); });
} }
/// <summary>
/// Load the theme from a XAML, if possible
/// </summary>
/// <param name="xaml">XAML data representing the theme, null for default</param>
private void LoadTheme(string xaml)
{
// If the view is null, we can't load the theme
if(App.PlayerView == null)
return;
try
{
if(xaml != null)
new AvaloniaXamlLoader().Load(xaml, null, App.PlayerView);
else
AvaloniaXamlLoader.Load(App.PlayerView);
}
catch(Exception ex)
{
Console.Error.WriteLine(ex);
}
// Ensure the context and view model are set
App.PlayerView.DataContext = this;
App.PlayerView.ViewModel = this;
UpdateDigits();
}
/// <summary> /// <summary>
/// Update the view-model from the Player /// Update the view-model from the Player
/// </summary> /// </summary>
private void PlayerStateChanged(object sender, PropertyChangedEventArgs e) private void PlayerStateChanged(object sender, PropertyChangedEventArgs e)
{ {
if(_player?.Initialized != true) if(_player == null)
return; return;
if(!_player.Initialized)
{
Dispatcher.UIThread.InvokeAsync(() =>
{
App.MainWindow.Title = "RedBookPlayer";
});
}
Initialized = _player.Initialized;
CurrentTrackNumber = _player.CurrentTrackNumber; CurrentTrackNumber = _player.CurrentTrackNumber;
CurrentTrackIndex = _player.CurrentTrackIndex; CurrentTrackIndex = _player.CurrentTrackIndex;
CurrentSector = _player.CurrentSector; CurrentSector = _player.CurrentSector;
@@ -643,7 +764,8 @@ namespace RedBookPlayer.GUI.ViewModels
CopyAllowed = _player.CopyAllowed; CopyAllowed = _player.CopyAllowed;
TrackHasEmphasis = _player.TrackHasEmphasis; TrackHasEmphasis = _player.TrackHasEmphasis;
Playing = _player.Playing; PlayerState = _player.PlayerState;
DataPlayback = _player.DataPlayback;
ApplyDeEmphasis = _player.ApplyDeEmphasis; ApplyDeEmphasis = _player.ApplyDeEmphasis;
Volume = _player.Volume; Volume = _player.Volume;
@@ -655,6 +777,10 @@ namespace RedBookPlayer.GUI.ViewModels
/// </summary> /// </summary>
private void UpdateDigits() private void UpdateDigits()
{ {
// Ensure the digits
if(_digits == null)
InitializeDigits();
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.Post(() =>
{ {
string digitString = GenerateDigitString() ?? string.Empty.PadLeft(20, '-'); string digitString = GenerateDigitString() ?? string.Empty.PadLeft(20, '-');

View File

@@ -1,15 +1,32 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Reactive;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization;
using Avalonia.Input; using Avalonia.Input;
using RedBookPlayer.GUI.Views; using ReactiveUI;
using RedBookPlayer.Models;
namespace RedBookPlayer.GUI namespace RedBookPlayer.GUI.ViewModels
{ {
public class Settings public class SettingsViewModel : ReactiveObject
{ {
#region Player Settings #region Player Settings
/// <summary>
/// List of all data playback values
/// </summary>
[JsonIgnore]
public List<DataPlayback> DataPlaybackValues => GenerateDataPlaybackList();
/// <summary>
/// List of all themes
/// </summary>
[JsonIgnore]
public List<string> ThemeValues => GenerateThemeList();
/// <summary> /// <summary>
/// Indicates if discs should start playing on load /// Indicates if discs should start playing on load
/// </summary> /// </summary>
@@ -30,42 +47,51 @@ namespace RedBookPlayer.GUI
/// </remarks> /// </remarks>
public bool PlayHiddenTracks { get; set; } = false; public bool PlayHiddenTracks { get; set; } = false;
/// <summary>
/// Indicates if data tracks should be played like old, non-compliant players
/// </summary>
public bool PlayDataTracks { get; set; } = false;
/// <summary> /// <summary>
/// Generate a TOC if the disc is missing one /// Generate a TOC if the disc is missing one
/// </summary> /// </summary>
public bool GenerateMissingTOC { get; set; } = true; public bool GenerateMissingTOC { get; set; } = true;
/// <summary>
/// Indicates how to deal with data tracks
/// </summary>
public DataPlayback DataPlayback { get; set; } = DataPlayback.Skip;
/// <summary> /// <summary>
/// Indicates the default playback volume /// Indicates the default playback volume
/// </summary> /// </summary>
public int Volume public int Volume
{ {
get => _volume; get => _volume;
set private set
{ {
int tempValue;
if(value > 100) if(value > 100)
_volume = 100; tempValue = 100;
else if(value < 0) else if(value < 0)
_volume = 0; tempValue = 0;
else else
_volume = value; tempValue = value;
this.RaiseAndSetIfChanged(ref _volume, tempValue);
} }
} }
/// <summary> /// <summary>
/// Indicates the currently selected theme /// Indicates the currently selected theme
/// </summary> /// </summary>
public string SelectedTheme { get; set; } = "default"; public string SelectedTheme { get; set; } = "Default";
#endregion #endregion
#region Key Mappings #region Key Mappings
/// <summary>
/// List of all keyboard keys
/// </summary>
[JsonIgnore]
public List<Key> KeyboardList => GenerateKeyList();
/// <summary> /// <summary>
/// Key assigned to open settings /// Key assigned to open settings
/// </summary> /// </summary>
@@ -86,6 +112,11 @@ namespace RedBookPlayer.GUI
/// </summary> /// </summary>
public Key StopPlaybackKey { get; set; } = Key.Escape; public Key StopPlaybackKey { get; set; } = Key.Escape;
/// <summary>
/// Key assigned to eject the disc
/// </summary>
public Key EjectKey { get; set; } = Key.OemTilde;
/// <summary> /// <summary>
/// Key assigned to move to the next track /// Key assigned to move to the next track
/// </summary> /// </summary>
@@ -138,6 +169,15 @@ namespace RedBookPlayer.GUI
#endregion #endregion
#region Commands
/// <summary>
/// Command for applying settings
/// </summary>
public ReactiveCommand<Unit, Unit> ApplySettingsCommand { get; }
#endregion
/// <summary> /// <summary>
/// Path to the settings file /// Path to the settings file
/// </summary> /// </summary>
@@ -148,44 +188,50 @@ namespace RedBookPlayer.GUI
/// </summary> /// </summary>
private int _volume = 100; private int _volume = 100;
public Settings() {} public SettingsViewModel() : this(null) { }
public Settings(string filePath) => _filePath = filePath; public SettingsViewModel(string filePath)
{
_filePath = filePath;
ApplySettingsCommand = ReactiveCommand.Create(ExecuteApplySettings);
}
/// <summary> /// <summary>
/// Load settings from a file /// Load settings from a file
/// </summary> /// </summary>
/// <param name="filePath">Path to the settings JSON file</param> /// <param name="filePath">Path to the settings JSON file</param>
/// <returns>Settings derived from the input file, if possible</returns> /// <returns>Settings derived from the input file, if possible</returns>
public static Settings Load(string filePath) public static SettingsViewModel Load(string filePath)
{ {
if(File.Exists(filePath)) if(File.Exists(filePath))
{ {
try try
{ {
Settings settings = JsonSerializer.Deserialize<Settings>(File.ReadAllText(filePath)); SettingsViewModel settings = JsonSerializer.Deserialize<SettingsViewModel>(File.ReadAllText(filePath));
settings._filePath = filePath; settings._filePath = filePath;
MainWindow.ApplyTheme(settings.SelectedTheme);
return settings; return settings;
} }
catch(JsonException) catch(JsonException)
{ {
Console.WriteLine("Couldn't parse settings, reverting to default"); Console.WriteLine("Couldn't parse settings, reverting to default");
return new Settings(filePath); return new SettingsViewModel(filePath);
} }
} }
return new Settings(filePath); return new SettingsViewModel(filePath);
} }
/// <summary> /// <summary>
/// Save settings to a file /// Apply settings from the UI
/// </summary> /// </summary>
public void Save() public void ExecuteApplySettings()
{ {
if(!string.IsNullOrWhiteSpace(SelectedTheme))
App.PlayerView?.ViewModel?.ApplyTheme(SelectedTheme);
var options = new JsonSerializerOptions var options = new JsonSerializerOptions
{ {
WriteIndented = true WriteIndented = true
@@ -194,5 +240,45 @@ namespace RedBookPlayer.GUI
string json = JsonSerializer.Serialize(this, options); string json = JsonSerializer.Serialize(this, options);
File.WriteAllText(_filePath, json); File.WriteAllText(_filePath, json);
} }
#region Generation
/// <summary>
/// Generate the list of DataPlayback values
/// </summary>
private List<DataPlayback> GenerateDataPlaybackList() => Enum.GetValues(typeof(DataPlayback)).Cast<DataPlayback>().ToList();
/// <summary>
/// Generate the list of Key values
/// </summary>
private List<Key> GenerateKeyList() => Enum.GetValues(typeof(Key)).Cast<Key>().ToList();
/// <summary>
/// Generate the list of valid themes
/// </summary>
private List<string> GenerateThemeList()
{
// Create a list of all found themes
List<string> items = new List<string>();
// Ensure the theme directory exists
if(!Directory.Exists("themes/"))
Directory.CreateDirectory("themes/");
// Add all theme directories if they're valid
foreach(string dir in Directory.EnumerateDirectories("themes/"))
{
string themeName = dir.Split('/')[1];
if(!File.Exists($"themes/{themeName}/view.xaml"))
continue;
items.Add(themeName);
}
return items;
}
#endregion
} }
} }

View File

@@ -1,7 +1,17 @@
<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" <ReactiveWindow xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
xmlns:rxui="clr-namespace:Avalonia;assembly=Avalonia.ReactiveUI"
xmlns:viewModels="clr-namespace:RedBookPlayer.GUI.ViewModels;assembly=RedBookPlayer.GUI"
xmlns:views="clr-namespace:RedBookPlayer.GUI.Views;assembly=RedBookPlayer.GUI"
x:Class="RedBookPlayer.GUI.Views.MainWindow" Title="RedBookPlayer" SizeToContent="WidthAndHeight" x:Class="RedBookPlayer.GUI.Views.MainWindow" Title="RedBookPlayer" SizeToContent="WidthAndHeight"
DragDrop.AllowDrop="True"> CanResize="False" DragDrop.AllowDrop="True">
<ContentControl Name="Content" /> <ReactiveWindow.ViewModel>
</Window> <viewModels:MainViewModel/>
</ReactiveWindow.ViewModel>
<ContentControl Name="Content">
<ContentControl.Content>
<views:PlayerView/>
</ContentControl.Content>
</ContentControl>
</ReactiveWindow>

View File

@@ -1,73 +1,13 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Xml;
using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
using RedBookPlayer.GUI.ViewModels; using RedBookPlayer.GUI.ViewModels;
namespace RedBookPlayer.GUI.Views namespace RedBookPlayer.GUI.Views
{ {
public class MainWindow : Window public class MainWindow : ReactiveWindow<MainViewModel>
{ {
public static MainWindow Instance; public MainWindow() => InitializeComponent();
public ContentControl ContentControl;
public Window settingsWindow;
public MainWindow()
{
Instance = this;
InitializeComponent();
}
/// <summary>
/// Apply a custom theme to the player
/// </summary>
/// <param name="theme">Path to the theme under the themes directory</param>
public static void ApplyTheme(string theme)
{
// If no theme path is provided, we can ignore
if(string.IsNullOrWhiteSpace(theme))
return;
// If we already have a view, cache the view model
PlayerViewModel pvm = ((PlayerView)Instance.ContentControl.Content).PlayerViewModel;
// If the theme name is "default", we assume the internal theme is used
if(theme.Equals("default", StringComparison.CurrentCultureIgnoreCase))
{
Instance.ContentControl.Content = new PlayerView(pvm);
}
else
{
string themeDirectory = $"{Directory.GetCurrentDirectory()}/themes/{theme}";
string xamlPath = $"{themeDirectory}/view.xaml";
if(!File.Exists(xamlPath))
{
Console.WriteLine("Warning: specified theme doesn't exist, reverting to default");
return;
}
try
{
string xaml = File.ReadAllText(xamlPath);
xaml = xaml.Replace("Source=\"", $"Source=\"file://{themeDirectory}/");
Instance.ContentControl.Content = new PlayerView(xaml, pvm);
}
catch(XmlException ex)
{
Console.WriteLine($"Error: invalid theme XAML ({ex.Message}), reverting to default");
Instance.ContentControl.Content = new PlayerView(pvm);
}
}
Instance.Width = ((PlayerView)Instance.ContentControl.Content).Width;
Instance.Height = ((PlayerView)Instance.ContentControl.Content).Height;
pvm.InitializeDigits();
}
/// <summary> /// <summary>
/// Initialize the main window /// Initialize the main window
@@ -76,162 +16,10 @@ namespace RedBookPlayer.GUI.Views
{ {
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
ContentControl = this.FindControl<ContentControl>("Content"); // Add handlers
ContentControl.Content = new PlayerView(); Closing += ViewModel.ExecuteStop;
AddHandler(DragDrop.DropEvent, ViewModel.ExecuteLoadDragDrop);
Instance.MaxWidth = ((PlayerView)Instance.ContentControl.Content).Width; KeyDown += ViewModel.ExecuteKeyPress;
Instance.MaxHeight = ((PlayerView)Instance.ContentControl.Content).Height;
ContentControl.Content = new PlayerView();
((PlayerView)ContentControl.Content).PlayerViewModel.InitializeDigits();
CanResize = false;
KeyDown += OnKeyDown;
Closing += (s, e) =>
{
settingsWindow?.Close();
settingsWindow = null;
};
Closing += (e, f) =>
{
((PlayerView)ContentControl.Content).PlayerViewModel.ExecuteStop();
};
AddHandler(DragDrop.DropEvent, MainWindow_Drop);
} }
#region Event Handlers
public async void MainWindow_Drop(object sender, DragEventArgs e)
{
PlayerView playerView = ContentControl.Content as PlayerView;
if(playerView == null)
return;
IEnumerable<string> fileNames = e.Data.GetFileNames();
foreach(string filename in fileNames)
{
bool loaded = await playerView?.PlayerViewModel?.LoadImage(filename);
if(loaded)
break;
}
}
public void OnKeyDown(object sender, KeyEventArgs e)
{
PlayerView playerView = ContentControl.Content as PlayerView;
// Open settings window
if(e.Key == App.Settings.OpenSettingsKey)
{
settingsWindow = new SettingsWindow(App.Settings);
settingsWindow.Closed += OnSettingsClosed;
settingsWindow.Show();
}
// Load image
else if (e.Key == App.Settings.LoadImageKey)
{
playerView?.PlayerViewModel?.ExecuteLoad();
}
// Toggle playback
else if(e.Key == App.Settings.TogglePlaybackKey || e.Key == Key.MediaPlayPause)
{
playerView?.PlayerViewModel?.ExecuteTogglePlayPause();
}
// Stop playback
else if(e.Key == App.Settings.StopPlaybackKey || e.Key == Key.MediaStop)
{
playerView?.PlayerViewModel?.ExecuteStop();
}
// Next Track
else if(e.Key == App.Settings.NextTrackKey || e.Key == Key.MediaNextTrack)
{
playerView?.PlayerViewModel?.ExecuteNextTrack();
}
// Previous Track
else if(e.Key == App.Settings.PreviousTrackKey || e.Key == Key.MediaPreviousTrack)
{
playerView?.PlayerViewModel?.ExecutePreviousTrack();
}
// Next Index
else if(e.Key == App.Settings.NextIndexKey)
{
playerView?.PlayerViewModel?.ExecuteNextIndex();
}
// Previous Index
else if(e.Key == App.Settings.PreviousIndexKey)
{
playerView?.PlayerViewModel?.ExecutePreviousIndex();
}
// Fast Foward
else if(e.Key == App.Settings.FastForwardPlaybackKey)
{
playerView?.PlayerViewModel?.ExecuteFastForward();
}
// Rewind
else if(e.Key == App.Settings.RewindPlaybackKey)
{
playerView?.PlayerViewModel?.ExecuteRewind();
}
// Volume Up
else if(e.Key == App.Settings.VolumeUpKey || e.Key == Key.VolumeUp)
{
int increment = 1;
if(e.KeyModifiers.HasFlag(KeyModifiers.Control))
increment *= 2;
if(e.KeyModifiers.HasFlag(KeyModifiers.Shift))
increment *= 5;
if(playerView?.PlayerViewModel?.Volume != null)
playerView.PlayerViewModel.ExecuteSetVolume(playerView.PlayerViewModel.Volume + increment);
}
// Volume Down
else if(e.Key == App.Settings.VolumeDownKey || e.Key == Key.VolumeDown)
{
int decrement = 1;
if(e.KeyModifiers.HasFlag(KeyModifiers.Control))
decrement *= 2;
if(e.KeyModifiers.HasFlag(KeyModifiers.Shift))
decrement *= 5;
if (playerView?.PlayerViewModel?.Volume != null)
playerView.PlayerViewModel.ExecuteSetVolume(playerView.PlayerViewModel.Volume - decrement);
}
// Mute Toggle
else if(e.Key == App.Settings.ToggleMuteKey || e.Key == Key.VolumeMute)
{
playerView?.PlayerViewModel?.ExecuteToggleMute();
}
// Emphasis Toggle
else if(e.Key == App.Settings.ToggleDeEmphasisKey)
{
playerView?.PlayerViewModel?.ExecuteToggleDeEmphasis();
}
}
public void OnSettingsClosed(object sender, EventArgs e)
{
PlayerView playerView = ContentControl.Content as PlayerView;
playerView?.UpdateViewModel();
}
#endregion
} }
} }

View File

@@ -1,7 +1,12 @@
<UserControl xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" <ReactiveUserControl xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
x:Class="RedBookPlayer.GUI.Views.PlayerView" Width="900" Height="400"> xmlns:rxui="clr-namespace:Avalonia;assembly=Avalonia.ReactiveUI"
xmlns:viewModels="clr-namespace:RedBookPlayer.GUI.ViewModels;assembly=RedBookPlayer.GUI"
x:Class="RedBookPlayer.GUI.Views.PlayerView" Width="900" Height="400" Background="White">
<ReactiveUserControl.ViewModel>
<viewModels:PlayerViewModel/>
</ReactiveUserControl.ViewModel>
<StackPanel Margin="16" VerticalAlignment="Center"> <StackPanel Margin="16" VerticalAlignment="Center">
<Button Command="{Binding LoadCommand}" Margin="32,0,32,16">Load</Button> <Button Command="{Binding LoadCommand}" Margin="32,0,32,16">Load</Button>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16"> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
@@ -101,4 +106,4 @@
<TextBlock Margin="0,0,16,0" Text="{Binding Volume}"/> <TextBlock Margin="0,0,16,0" Text="{Binding Volume}"/>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</UserControl> </ReactiveUserControl>

View File

@@ -1,73 +1,9 @@
using Avalonia.Controls; using Avalonia.ReactiveUI;
using Avalonia.Markup.Xaml;
using RedBookPlayer.GUI.ViewModels; using RedBookPlayer.GUI.ViewModels;
namespace RedBookPlayer.GUI.Views namespace RedBookPlayer.GUI.Views
{ {
public class PlayerView : UserControl public class PlayerView : ReactiveUserControl<PlayerViewModel>
{ {
/// <summary>
/// Read-only access to the view model
/// </summary>
public PlayerViewModel PlayerViewModel => DataContext as PlayerViewModel;
/// <summary>
/// Initialize the UI based on the default theme
/// </summary>
public PlayerView() : this(null, null) { }
/// <summary>
/// Initialize the UI based on the default theme with an existing view model
/// </summary>
/// <param name="xaml">XAML data representing the theme, null for default</param>
/// <param name="playerViewModel">Existing PlayerViewModel to load in instead of creating a new one</param>
public PlayerView(PlayerViewModel playerViewModel) : this(null, playerViewModel) { }
/// <summary>
/// Initialize the UI based on the currently selected theme
/// </summary>
/// <param name="xaml">XAML data representing the theme, null for default</param>
/// <param name="playerViewModel">Existing PlayerViewModel to load in instead of creating a new one</param>
public PlayerView(string xaml, PlayerViewModel playerViewModel)
{
LoadTheme(xaml);
if(playerViewModel != null)
DataContext = playerViewModel;
else
DataContext = new PlayerViewModel();
}
#region Helpers
/// <summary>
/// Update the view model with new settings
/// </summary>
public void UpdateViewModel()
{
PlayerViewModel.SetLoadDataTracks(App.Settings.PlayDataTracks);
PlayerViewModel.SetLoadHiddenTracks(App.Settings.PlayHiddenTracks);
}
/// <summary>
/// Load the theme from a XAML, if possible
/// </summary>
/// <param name="xaml">XAML data representing the theme, null for default</param>
private void LoadTheme(string xaml)
{
try
{
if(xaml != null)
new AvaloniaXamlLoader().Load(xaml, null, this);
else
AvaloniaXamlLoader.Load(this);
}
catch
{
AvaloniaXamlLoader.Load(this);
}
}
#endregion
} }
} }

View File

@@ -1,7 +1,12 @@
<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" <ReactiveWindow xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
xmlns:rxui="clr-namespace:Avalonia;assembly=Avalonia.ReactiveUI" d:DesignWidth="800"
xmlns:viewModels="clr-namespace:RedBookPlayer.GUI.ViewModels;assembly=RedBookPlayer.GUI"
d:DesignHeight="450" x:Class="RedBookPlayer.GUI.Views.SettingsWindow" Title="Settings" SizeToContent="WidthAndHeight"> d:DesignHeight="450" x:Class="RedBookPlayer.GUI.Views.SettingsWindow" Title="Settings" SizeToContent="WidthAndHeight">
<ReactiveWindow.ViewModel>
<viewModels:SettingsViewModel/>
</ReactiveWindow.ViewModel>
<StackPanel> <StackPanel>
<TabControl> <TabControl>
<TabItem Header="UI Settings"> <TabItem Header="UI Settings">
@@ -21,8 +26,9 @@
<TextBlock VerticalAlignment="Center">Play hidden tracks</TextBlock> <TextBlock VerticalAlignment="Center">Play hidden tracks</TextBlock>
</WrapPanel> </WrapPanel>
<WrapPanel Margin="0,0,0,16"> <WrapPanel Margin="0,0,0,16">
<CheckBox IsChecked="{Binding PlayDataTracks}" Margin="0,0,8,0"/> <TextBlock Grid.Row="0" Grid.Column="0" Width="120">Data Track Playback</TextBlock>
<TextBlock VerticalAlignment="Center">Play data tracks like old, non-compliant players</TextBlock> <ComboBox Grid.Row="0" Grid.Column="1" Name="DataPlayback" Margin="8,0,0,0" Width="120"
Items="{Binding DataPlaybackValues}" SelectedItem="{Binding DataPlayback, Mode=TwoWay}" />
</WrapPanel> </WrapPanel>
<WrapPanel Margin="0,0,0,16"> <WrapPanel Margin="0,0,0,16">
<CheckBox IsChecked="{Binding GenerateMissingTOC}" Margin="0,0,8,0"/> <CheckBox IsChecked="{Binding GenerateMissingTOC}" Margin="0,0,8,0"/>
@@ -33,11 +39,12 @@
<TextBlock VerticalAlignment="Center" DockPanel.Dock="Right" Text="%" /> <TextBlock VerticalAlignment="Center" DockPanel.Dock="Right" Text="%" />
<TextBlock VerticalAlignment="Center" Margin="8,0,0,0" DockPanel.Dock="Right" Text="{Binding Volume}" <TextBlock VerticalAlignment="Center" Margin="8,0,0,0" DockPanel.Dock="Right" Text="{Binding Volume}"
Name="VolumeLabel" /> Name="VolumeLabel" />
<Slider Minimum="0" Maximum="100" SmallChange="1" LargeChange="10" Value="{Binding Volume}" <Slider Minimum="0" Maximum="100" SmallChange="1" LargeChange="10" Value="{Binding Volume, Mode=TwoWay}"
Name="VolumeSlider" /> Name="VolumeSlider"/>
</DockPanel> </DockPanel>
</StackPanel> </StackPanel>
<ListBox Name="ThemeList" SelectionMode="Single" Margin="0,0,0,16" /> <ListBox Name="ThemeList" SelectionMode="Single" Margin="0,0,0,16"
Items="{Binding ThemeValues}" SelectedItem="{Binding SelectedTheme, Mode=TwoWay}"/>
</DockPanel> </DockPanel>
</TabItem> </TabItem>
<TabItem Header="Keyboard Bindings"> <TabItem Header="Keyboard Bindings">
@@ -61,62 +68,96 @@
<RowDefinition/> <RowDefinition/>
<RowDefinition/> <RowDefinition/>
<RowDefinition/> <RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- Load Image--> <!-- Load Image-->
<TextBlock Grid.Row="0" Grid.Column="0" Width="120">Load Image</TextBlock> <TextBlock Grid.Row="0" Grid.Column="0" Width="120">Load Image</TextBlock>
<ComboBox Grid.Row="0" Grid.Column="1" HorizontalAlignment="Right" Name="LoadImageKeyBind" Margin="8,0,0,0" Width="120"/> <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"/>
<!-- Toggle Play/Pause --> <!-- Toggle Play/Pause -->
<TextBlock Grid.Row="1" Grid.Column="0" Width="120">Toggle Play/Pause</TextBlock> <TextBlock Grid.Row="2" Grid.Column="0" Width="120">Toggle Play/Pause</TextBlock>
<ComboBox Grid.Row="1" Grid.Column="1" HorizontalAlignment="Right" Name="TogglePlaybackKeyBind" Margin="8,0,0,0" Width="120"/> <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"/>
<!-- Stop Playback--> <!-- Stop Playback-->
<TextBlock Grid.Row="2" Grid.Column="0" Width="120">Stop Playback</TextBlock> <TextBlock Grid.Row="3" Grid.Column="0" Width="120">Stop Playback</TextBlock>
<ComboBox Grid.Row="2" Grid.Column="1" HorizontalAlignment="Right" Name="StopPlaybackKeyBind" Margin="8,0,0,0" Width="120"/> <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"/>
<!-- 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"/>
<!-- Next Track --> <!-- Next Track -->
<TextBlock Grid.Row="3" Grid.Column="0" Width="120">Next Track</TextBlock> <TextBlock Grid.Row="5" Grid.Column="0" Width="120">Next Track</TextBlock>
<ComboBox Grid.Row="3" Grid.Column="1" HorizontalAlignment="Right" Name="NextTrackKeyBind" Margin="8,0,0,0" Width="120"/> <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"/>
<!-- Previous Track --> <!-- Previous Track -->
<TextBlock Grid.Row="4" Grid.Column="0" Width="120">Previous Track</TextBlock> <TextBlock Grid.Row="6" Grid.Column="0" Width="120">Previous Track</TextBlock>
<ComboBox Grid.Row="4" Grid.Column="1" HorizontalAlignment="Right" Name="PreviousTrackKeyBind" Margin="8,0,0,0" Width="120"/> <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"/>
<!-- Next Index --> <!-- Next Index -->
<TextBlock Grid.Row="5" Grid.Column="0" Width="120">Next Index</TextBlock> <TextBlock Grid.Row="7" Grid.Column="0" Width="120">Next Index</TextBlock>
<ComboBox Grid.Row="5" Grid.Column="1" HorizontalAlignment="Right" Name="NextIndexKeyBind" Margin="8,0,0,0" Width="120"/> <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"/>
<!-- Previous Index --> <!-- Previous Index -->
<TextBlock Grid.Row="6" Grid.Column="0" Width="120">Previous Index</TextBlock> <TextBlock Grid.Row="8" Grid.Column="0" Width="120">Previous Index</TextBlock>
<ComboBox Grid.Row="6" Grid.Column="1" HorizontalAlignment="Right" Name="PreviousIndexKeyBind" Margin="8,0,0,0" Width="120"/> <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"/>
<!-- Fast Forward --> <!-- Fast Forward -->
<TextBlock Grid.Row="7" Grid.Column="0" Width="120">Fast-Forward</TextBlock> <TextBlock Grid.Row="9" Grid.Column="0" Width="120">Fast-Forward</TextBlock>
<ComboBox Grid.Row="7" Grid.Column="1" HorizontalAlignment="Right" Name="FastForwardPlaybackKeyBind" Margin="8,0,0,0" Width="120"/> <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"/>
<!-- Rewind --> <!-- Rewind -->
<TextBlock Grid.Row="8" Grid.Column="0" Width="120">Rewind</TextBlock> <TextBlock Grid.Row="10" Grid.Column="0" Width="120">Rewind</TextBlock>
<ComboBox Grid.Row="8" Grid.Column="1" HorizontalAlignment="Right" Name="RewindPlaybackKeyBind" Margin="8,0,0,0" Width="120"/> <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"/>
<!-- Volume Up --> <!-- Volume Up -->
<TextBlock Grid.Row="9" Grid.Column="0" Width="120">Volume Up</TextBlock> <TextBlock Grid.Row="11" Grid.Column="0" Width="120">Volume Up</TextBlock>
<ComboBox Grid.Row="9" Grid.Column="1" HorizontalAlignment="Right" Name="VolumeUpKeyBind" Margin="8,0,0,0" Width="120"/> <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"/>
<!-- Volume Down --> <!-- Volume Down -->
<TextBlock Grid.Row="10" Grid.Column="0" Width="120">Volume Down</TextBlock> <TextBlock Grid.Row="12" Grid.Column="0" Width="120">Volume Down</TextBlock>
<ComboBox Grid.Row="10" Grid.Column="1" HorizontalAlignment="Right" Name="VolumeDownKeyBind" Margin="8,0,0,0" Width="120"/> <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"/>
<!-- Mute Toggle --> <!-- Mute Toggle -->
<TextBlock Grid.Row="11" Grid.Column="0" Width="120">Toggle Mute</TextBlock> <TextBlock Grid.Row="13" Grid.Column="0" Width="120">Toggle Mute</TextBlock>
<ComboBox Grid.Row="11" Grid.Column="1" HorizontalAlignment="Right" Name="ToggleMuteKeyBind" Margin="8,0,0,0" Width="120"/> <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"/>
<!-- De-Emphasis Toggle --> <!-- De-Emphasis Toggle -->
<TextBlock Grid.Row="12" Grid.Column="0" Width="120">Toggle De-Emphasis</TextBlock> <TextBlock Grid.Row="14" Grid.Column="0" Width="120">Toggle De-Emphasis</TextBlock>
<ComboBox Grid.Row="12" Grid.Column="1" HorizontalAlignment="Right" Name="ToggleDeEmphasisKeyBind" Margin="8,0,0,0" Width="120"/> <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> </Grid>
</TabItem> </TabItem>
</TabControl> </TabControl>
<Button Name="ApplyButton">Apply</Button> <Button Name="ApplyButton" Command="{Binding ApplySettingsCommand}">Apply</Button>
</StackPanel> </StackPanel>
</Window> </ReactiveWindow>

View File

@@ -1,186 +1,9 @@
using System; using Avalonia.ReactiveUI;
using System.Collections.Generic; using RedBookPlayer.GUI.ViewModels;
using System.IO;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
namespace RedBookPlayer.GUI.Views namespace RedBookPlayer.GUI.Views
{ {
public class SettingsWindow : Window public class SettingsWindow : ReactiveWindow<SettingsViewModel>
{ {
private readonly Settings _settings;
private string _selectedTheme;
private ListBox _themeList;
public SettingsWindow() {}
public SettingsWindow(Settings settings)
{
DataContext = _settings = settings;
InitializeComponent();
}
public void ThemeList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count == 0)
return;
_selectedTheme = (string)e.AddedItems[0];
}
public void ApplySettings(object sender, RoutedEventArgs e)
{
if (!string.IsNullOrWhiteSpace(_selectedTheme))
{
_settings.SelectedTheme = _selectedTheme;
MainWindow.ApplyTheme(_selectedTheme);
}
SaveKeyboardList();
_settings.Save();
}
public void UpdateView() => this.FindControl<TextBlock>("VolumeLabel").Text = _settings.Volume.ToString();
void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
PopulateThemes();
PopulateKeyboardList();
this.FindControl<Button>("ApplyButton").Click += ApplySettings;
this.FindControl<Slider>("VolumeSlider").PropertyChanged += (s, e) => UpdateView();
}
/// <summary>
/// Populate the list of themes
/// </summary>
private void PopulateThemes()
{
// Get a reference to the theme list
_themeList = this.FindControl<ListBox>("ThemeList");
_themeList.SelectionChanged += ThemeList_SelectionChanged;
// Create a list of all found themes
List<string> items = new List<string>();
items.Add("default");
// Ensure the theme directory exists
if(!Directory.Exists("themes/"))
Directory.CreateDirectory("themes/");
// Add all theme directories if they're valid
foreach(string dir in Directory.EnumerateDirectories("themes/"))
{
string themeName = dir.Split('/')[1];
if(!File.Exists($"themes/{themeName}/view.xaml"))
continue;
items.Add(themeName);
}
_themeList.Items = items;
}
/// <summary>
/// Populate all of the keyboard bindings
/// </summary>
private void PopulateKeyboardList()
{
// Access all of the combo boxes
ComboBox loadImageKeyBind = this.FindControl<ComboBox>("LoadImageKeyBind");
ComboBox togglePlaybackKeyBind = this.FindControl<ComboBox>("TogglePlaybackKeyBind");
ComboBox stopPlaybackKeyBind = this.FindControl<ComboBox>("StopPlaybackKeyBind");
ComboBox nextTrackKeyBind = this.FindControl<ComboBox>("NextTrackKeyBind");
ComboBox previousTrackKeyBind = this.FindControl<ComboBox>("PreviousTrackKeyBind");
ComboBox nextIndexKeyBind = this.FindControl<ComboBox>("NextIndexKeyBind");
ComboBox previousIndexKeyBind = this.FindControl<ComboBox>("PreviousIndexKeyBind");
ComboBox fastForwardPlaybackKeyBind = this.FindControl<ComboBox>("FastForwardPlaybackKeyBind");
ComboBox rewindPlaybackKeyBind = this.FindControl<ComboBox>("RewindPlaybackKeyBind");
ComboBox volumeUpKeyBind = this.FindControl<ComboBox>("VolumeUpKeyBind");
ComboBox volumeDownKeyBind = this.FindControl<ComboBox>("VolumeDownKeyBind");
ComboBox toggleMuteKeyBind = this.FindControl<ComboBox>("ToggleMuteKeyBind");
ComboBox toggleDeEmphasisKeyBind = this.FindControl<ComboBox>("ToggleDeEmphasisKeyBind");
// Assign the list of values to all of them
Array keyboardList = GenerateKeyboardList();
loadImageKeyBind.Items = keyboardList;
togglePlaybackKeyBind.Items = keyboardList;
stopPlaybackKeyBind.Items = keyboardList;
nextTrackKeyBind.Items = keyboardList;
previousTrackKeyBind.Items = keyboardList;
nextIndexKeyBind.Items = keyboardList;
previousIndexKeyBind.Items = keyboardList;
fastForwardPlaybackKeyBind.Items = keyboardList;
rewindPlaybackKeyBind.Items = keyboardList;
volumeUpKeyBind.Items = keyboardList;
volumeDownKeyBind.Items = keyboardList;
toggleMuteKeyBind.Items = keyboardList;
toggleDeEmphasisKeyBind.Items = keyboardList;
// Set all of the currently selected items
loadImageKeyBind.SelectedItem = _settings.LoadImageKey;
togglePlaybackKeyBind.SelectedItem = _settings.TogglePlaybackKey;
stopPlaybackKeyBind.SelectedItem = _settings.StopPlaybackKey;
nextTrackKeyBind.SelectedItem = _settings.NextTrackKey;
previousTrackKeyBind.SelectedItem = _settings.PreviousTrackKey;
nextIndexKeyBind.SelectedItem = _settings.NextIndexKey;
previousIndexKeyBind.SelectedItem = _settings.PreviousIndexKey;
fastForwardPlaybackKeyBind.SelectedItem = _settings.FastForwardPlaybackKey;
rewindPlaybackKeyBind.SelectedItem = _settings.RewindPlaybackKey;
volumeUpKeyBind.SelectedItem = _settings.VolumeUpKey;
volumeDownKeyBind.SelectedItem = _settings.VolumeDownKey;
toggleMuteKeyBind.SelectedItem = _settings.ToggleMuteKey;
toggleDeEmphasisKeyBind.SelectedItem = _settings.ToggleDeEmphasisKey;
}
/// <summary>
/// Save back all values from keyboard bindings
/// </summary>
private void SaveKeyboardList()
{
// Access all of the combo boxes
ComboBox loadImageKeyBind = this.FindControl<ComboBox>("LoadImageKeyBind");
ComboBox togglePlaybackKeyBind = this.FindControl<ComboBox>("TogglePlaybackKeyBind");
ComboBox stopPlaybackKeyBind = this.FindControl<ComboBox>("StopPlaybackKeyBind");
ComboBox nextTrackKeyBind = this.FindControl<ComboBox>("NextTrackKeyBind");
ComboBox previousTrackKeyBind = this.FindControl<ComboBox>("PreviousTrackKeyBind");
ComboBox nextIndexKeyBind = this.FindControl<ComboBox>("NextIndexKeyBind");
ComboBox previousIndexKeyBind = this.FindControl<ComboBox>("PreviousIndexKeyBind");
ComboBox fastForwardPlaybackKeyBind = this.FindControl<ComboBox>("FastForwardPlaybackKeyBind");
ComboBox rewindPlaybackKeyBind = this.FindControl<ComboBox>("RewindPlaybackKeyBind");
ComboBox volumeUpKeyBind = this.FindControl<ComboBox>("VolumeUpKeyBind");
ComboBox volumeDownKeyBind = this.FindControl<ComboBox>("VolumeDownKeyBind");
ComboBox toggleMuteKeyBind = this.FindControl<ComboBox>("ToggleMuteKeyBind");
ComboBox toggleDeEmphasisKeyBind = this.FindControl<ComboBox>("ToggleDeEmphasisKeyBind");
// Set all of the currently selected items
_settings.LoadImageKey = (Key)loadImageKeyBind.SelectedItem;
_settings.TogglePlaybackKey = (Key)togglePlaybackKeyBind.SelectedItem;
_settings.StopPlaybackKey = (Key)stopPlaybackKeyBind.SelectedItem;
_settings.NextTrackKey = (Key)nextTrackKeyBind.SelectedItem;
_settings.PreviousTrackKey = (Key)previousTrackKeyBind.SelectedItem;
_settings.NextIndexKey = (Key)nextIndexKeyBind.SelectedItem;
_settings.PreviousIndexKey = (Key)previousIndexKeyBind.SelectedItem;
_settings.FastForwardPlaybackKey = (Key)fastForwardPlaybackKeyBind.SelectedItem;
_settings.RewindPlaybackKey = (Key)rewindPlaybackKeyBind.SelectedItem;
_settings.VolumeUpKey = (Key)volumeUpKeyBind.SelectedItem;
_settings.VolumeDownKey = (Key)volumeDownKeyBind.SelectedItem;
_settings.ToggleMuteKey = (Key)toggleMuteKeyBind.SelectedItem;
_settings.ToggleDeEmphasisKey = (Key)toggleDeEmphasisKeyBind.SelectedItem;
}
/// <summary>
/// Generate a list of keyboard keys for mapping
/// </summary>
/// <returns></returns>
private Array GenerateKeyboardList()
{
return Enum.GetValues(typeof(Key));
}
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 B

View File

@@ -0,0 +1,109 @@
<ReactiveUserControl xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
xmlns:rxui="clr-namespace:Avalonia;assembly=Avalonia.ReactiveUI"
xmlns:viewModels="clr-namespace:RedBookPlayer.GUI.ViewModels;assembly=RedBookPlayer.GUI"
x:Class="RedBookPlayer.GUI.Views.PlayerView" Width="900" Height="400" Background="White">
<ReactiveUserControl.ViewModel>
<viewModels:PlayerViewModel/>
</ReactiveUserControl.ViewModel>
<StackPanel Margin="16" VerticalAlignment="Center">
<Button Command="{Binding LoadCommand}" 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>
</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>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
<StackPanel Margin="0,0,32,0">
<TextBlock Margin="0,0,0,4">TRACK</TextBlock>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Image Name="TrackDigit1" Width="42" Height="51" Source="/-.png" />
<Image Name="TrackDigit2" Width="42" Height="51" Source="/-.png" />
</StackPanel>
</StackPanel>
<StackPanel Margin="0,0,32,0">
<TextBlock Margin="0,0,0,4">INDEX</TextBlock>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Image Name="IndexDigit1" Width="42" Height="51" Source="/-.png" />
<Image Name="IndexDigit2" Width="42" Height="51" Source="/-.png" />
</StackPanel>
</StackPanel>
<StackPanel>
<TextBlock Margin="0,0,0,4">TIME</TextBlock>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Image Name="TimeDigit1" Width="42" Height="51" Source="/-.png" />
<Image Name="TimeDigit2" Width="42" Height="51" Source="/-.png" />
<Image Width="11" Height="51" Source="/colon.png" />
<Image Name="TimeDigit3" Width="42" Height="51" Source="/-.png" />
<Image Name="TimeDigit4" Width="42" Height="51" Source="/-.png" />
<Image Width="11" Height="51" Source="/colon.png" />
<Image Name="TimeDigit5" Width="42" Height="51" Source="/-.png" />
<Image Name="TimeDigit6" Width="42" Height="51" Source="/-.png" />
</StackPanel>
</StackPanel>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
<StackPanel Margin="0,0,32,0">
<TextBlock Margin="0,0,0,4">TRACKS</TextBlock>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Image Name="TotalTracksDigit1" Width="42" Height="51" Source="/-.png" />
<Image Name="TotalTracksDigit2" Width="42" Height="51" Source="/-.png" />
</StackPanel>
</StackPanel>
<StackPanel Margin="0,0,32,0">
<TextBlock Margin="0,0,0,4">INDEXES</TextBlock>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Image Name="TotalIndexesDigit1" Width="42" Height="51" Source="/-.png" />
<Image Name="TotalIndexesDigit2" Width="42" Height="51" Source="/-.png" />
</StackPanel>
</StackPanel>
<StackPanel>
<TextBlock Margin="0,0,0,4">TOTAL</TextBlock>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Image Name="TotalTimeDigit1" Width="42" Height="51" Source="/-.png" />
<Image Name="TotalTimeDigit2" Width="42" Height="51" Source="/-.png" />
<Image Width="11" Height="51" Source="/colon.png" />
<Image Name="TotalTimeDigit3" Width="42" Height="51" Source="/-.png" />
<Image Name="TotalTimeDigit4" Width="42" Height="51" Source="/-.png" />
<Image Width="11" Height="51" Source="/colon.png" />
<Image Name="TotalTimeDigit5" Width="42" Height="51" Source="/-.png" />
<Image Name="TotalTimeDigit6" Width="42" Height="51" Source="/-.png" />
</StackPanel>
</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">
Enable De-Emphasis
</Button>
<Button Command="{Binding DisableDeEmphasisCommand}" IsVisible="{Binding ApplyDeEmphasis}" Width="200"
Margin="0,0,16,0">
Disable De-Emphasis
</Button>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<TextBlock Margin="0,0,16,0" Foreground="LightGray" IsVisible="{Binding IsDataTrack}">AUDIO</TextBlock>
<TextBlock Margin="0,0,16,0" IsVisible="{Binding !IsDataTrack}">AUDIO</TextBlock>
<TextBlock Margin="0,0,16,0" Foreground="LightGray" IsVisible="{Binding !IsDataTrack}">DATA</TextBlock>
<TextBlock Margin="0,0,16,0" IsVisible="{Binding IsDataTrack}">DATA</TextBlock>
<TextBlock Margin="0,0,16,0" Foreground="LightGray" IsVisible="{Binding !TrackHasEmphasis}">EMPHASIS</TextBlock>
<TextBlock Margin="0,0,16,0" IsVisible="{Binding TrackHasEmphasis}">EMPHASIS</TextBlock>
<TextBlock Margin="0,0,16,0" Foreground="LightGray" IsVisible="{Binding !CopyAllowed}">COPY</TextBlock>
<TextBlock Margin="0,0,16,0" IsVisible="{Binding CopyAllowed}">COPY</TextBlock>
<TextBlock Margin="0,0,16,0" Foreground="LightGray" IsVisible="{Binding !QuadChannel}">4CH</TextBlock>
<TextBlock Margin="0,0,16,0" IsVisible="{Binding QuadChannel}">4CH</TextBlock>
<TextBlock Margin="0,0,16,0" Foreground="LightGray" IsVisible="{Binding !HiddenTrack}">HIDDEN</TextBlock>
<TextBlock Margin="0,0,16,0" IsVisible="{Binding HiddenTrack}">HIDDEN</TextBlock>
<TextBlock Margin="0,0,16,0" Text="{Binding Volume}"/>
</StackPanel>
</StackPanel>
</ReactiveUserControl>

View File

@@ -26,7 +26,7 @@ namespace RedBookPlayer.Models.Discs
return; return;
// Data tracks only and flag disabled means we can't do anything // Data tracks only and flag disabled means we can't do anything
if(_image.Tracks.All(t => t.TrackType != TrackType.Audio) && !LoadDataTracks) if(_image.Tracks.All(t => t.TrackType != TrackType.Audio) && DataPlayback == DataPlayback.Skip)
return; return;
// Cache the value and the current track number // Cache the value and the current track number
@@ -69,8 +69,10 @@ namespace RedBookPlayer.Models.Discs
SetTrackFlags(track); SetTrackFlags(track);
// If the track is playable, just return // If the track is playable, just return
if(TrackType == TrackType.Audio || LoadDataTracks) if(TrackType == TrackType.Audio || DataPlayback != DataPlayback.Skip)
{
break; break;
}
// If we're not playing the track, skip // If we're not playing the track, skip
if(increment) if(increment)
@@ -143,26 +145,28 @@ namespace RedBookPlayer.Models.Discs
if(_image == null) if(_image == null)
return; 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;
// Cache the current track for easy access // Cache the current track for easy access
Track track = GetTrack(CurrentTrackNumber); Track track = GetTrack(CurrentTrackNumber);
if(track == null) if(track == null)
return; return;
this.RaiseAndSetIfChanged(ref _currentSector, value); this.RaiseAndSetIfChanged(ref _currentSector, tempSector);
if((CurrentTrackNumber < _image.Tracks.Count - 1 && CurrentSector >= (GetTrack(CurrentTrackNumber + 1)?.TrackStartSector ?? 0)) // If the current sector is outside of the last known track, seek to the right one
|| (CurrentTrackNumber > 0 && CurrentSector < track.TrackStartSector)) if(CurrentSector < track.TrackStartSector || CurrentSector > track.TrackEndSector)
{ {
foreach(Track trackData in _image.Tracks.ToArray().Reverse()) track = _image.Tracks.Last(t => CurrentSector >= t.TrackStartSector);
{ CurrentTrackNumber = (int)track.TrackSequence;
if(CurrentSector >= trackData.TrackStartSector)
{
CurrentTrackNumber = (int)trackData.TrackSequence;
break;
}
}
} }
// Set the new index, if necessary
foreach((ushort key, int i) in track.Indexes.Reverse()) foreach((ushort key, int i) in track.Indexes.Reverse())
{ {
if((int)CurrentSector >= i) if((int)CurrentSector >= i)
@@ -216,9 +220,9 @@ namespace RedBookPlayer.Models.Discs
} }
/// <summary> /// <summary>
/// Indicate if data tracks should be loaded /// Indicate how data tracks should be handled
/// </summary> /// </summary>
public bool LoadDataTracks { get; set; } = false; public DataPlayback DataPlayback { get; set; } = DataPlayback.Skip;
/// <summary> /// <summary>
/// Indicate if hidden tracks should be loaded /// Indicate if hidden tracks should be loaded
@@ -264,14 +268,12 @@ namespace RedBookPlayer.Models.Discs
/// <summary> /// <summary>
/// Constructor /// Constructor
/// </summary> /// </summary>
/// <param name="generateMissingToc">Generate a TOC if the disc is missing one</param> /// <param name="options">Set of options for a new disc</param>
/// <param name="loadHiddenTracks">Load hidden tracks for playback</param> public CompactDisc(OpticalDiscOptions options)
/// <param name="loadDataTracks">Load data tracks for playback</param>
public CompactDisc(bool generateMissingToc, bool loadHiddenTracks, bool loadDataTracks)
{ {
_generateMissingToc = generateMissingToc; DataPlayback = options.DataPlayback;
LoadHiddenTracks = loadHiddenTracks; _generateMissingToc = options.GenerateMissingToc;
LoadDataTracks = loadDataTracks; LoadHiddenTracks = options.LoadHiddenTracks;
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -392,7 +394,7 @@ namespace RedBookPlayer.Models.Discs
} }
// If the previous index has an invalid offset, change tracks if needed // If the previous index has an invalid offset, change tracks if needed
else if (track.Indexes[(ushort)(CurrentTrackIndex - 1)] < 0) else if(track.Indexes[(ushort)(CurrentTrackIndex - 1)] < 0)
{ {
if(changeTrack) if(changeTrack)
{ {
@@ -401,7 +403,7 @@ namespace RedBookPlayer.Models.Discs
return true; return true;
} }
} }
// Otherwise, just move to the previous index // Otherwise, just move to the previous index
else else
{ {
@@ -416,23 +418,7 @@ namespace RedBookPlayer.Models.Discs
#region Helpers #region Helpers
/// <inheritdoc/> /// <inheritdoc/>
public override void LoadFirstTrack() public override void LoadTrack(int trackNumber)
{
CurrentTrackNumber = 1;
LoadTrack(CurrentTrackNumber);
}
/// <inheritdoc/>
public override void SetTotalIndexes()
{
if(_image == null)
return;
TotalIndexes = GetTrack(CurrentTrackNumber)?.Indexes.Keys.Max() ?? 0;
}
/// <inheritdoc/>
protected override void LoadTrack(int trackNumber)
{ {
if(_image == null) if(_image == null)
return; return;
@@ -448,6 +434,33 @@ namespace RedBookPlayer.Models.Discs
CurrentSector = (ulong)(track?.Indexes.OrderBy(kvp => kvp.Key).First(kvp => kvp.Value >= 0).Value ?? 0); CurrentSector = (ulong)(track?.Indexes.OrderBy(kvp => kvp.Key).First(kvp => kvp.Value >= 0).Value ?? 0);
} }
/// <inheritdoc/>
public override void LoadFirstTrack()
{
CurrentTrackNumber = 1;
LoadTrack(CurrentTrackNumber);
}
/// <inheritdoc/>
public override byte[] ReadSectors(uint sectorsToRead)
{
if(TrackType == TrackType.Audio || DataPlayback == DataPlayback.Play)
return base.ReadSectors(sectorsToRead);
else if(DataPlayback == DataPlayback.Blank)
return new byte[sectorsToRead * BytesPerSector];
else
return new byte[0];
}
/// <inheritdoc/>
public override void SetTotalIndexes()
{
if(_image == null)
return;
TotalIndexes = GetTrack(CurrentTrackNumber)?.Indexes.Keys.Max() ?? 0;
}
/// <summary> /// <summary>
/// Get the track with the given sequence value, if possible /// Get the track with the given sequence value, if possible
/// </summary> /// </summary>
@@ -580,4 +593,4 @@ namespace RedBookPlayer.Models.Discs
#endregion #endregion
} }
} }

View File

@@ -122,6 +122,12 @@ namespace RedBookPlayer.Models.Discs
#region Helpers #region Helpers
/// <summary>
/// Load the desired track, if possible
/// </summary>
/// <param name="track">Track number to load</param>
public abstract void LoadTrack(int track);
/// <summary> /// <summary>
/// Load the first valid track in the image /// Load the first valid track in the image
/// </summary> /// </summary>
@@ -132,7 +138,7 @@ namespace RedBookPlayer.Models.Discs
/// </summary> /// </summary>
/// <param name="sectorsToRead">Current number of sectors to read</param> /// <param name="sectorsToRead">Current number of sectors to read</param>
/// <returns>Byte array representing the read sectors, if possible</returns> /// <returns>Byte array representing the read sectors, if possible</returns>
public byte[] ReadSectors(uint sectorsToRead) => _image.ReadSectors(CurrentSector, sectorsToRead); public virtual byte[] ReadSectors(uint sectorsToRead) => _image.ReadSectors(CurrentSector, sectorsToRead);
/// <summary> /// <summary>
/// Set the total indexes from the current track /// Set the total indexes from the current track
@@ -145,12 +151,6 @@ namespace RedBookPlayer.Models.Discs
/// <param name="sector">New sector number to use</param> /// <param name="sector">New sector number to use</param>
public void SetCurrentSector(ulong sector) => CurrentSector = sector; public void SetCurrentSector(ulong sector) => CurrentSector = sector;
/// <summary>
/// Load the desired track, if possible
/// </summary>
/// <param name="track">Track number to load</param>
protected abstract void LoadTrack(int track);
#endregion #endregion
} }
} }

View File

@@ -0,0 +1,24 @@
namespace RedBookPlayer.Models.Discs
{
public class OpticalDiscOptions
{
#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;
#endregion
}
}

View File

@@ -0,0 +1,49 @@
namespace RedBookPlayer.Models
{
/// <summary>
/// Determine how to handle data tracks
/// </summary>
public enum DataPlayback
{
/// <summary>
/// Skip playing all data tracks
/// </summary>
Skip = 0,
/// <summary>
/// Play silence for all data tracks
/// </summary>
Blank = 1,
/// <summary>
/// Play the data from all data tracks
/// </summary>
Play = 2,
}
/// <summary>
/// Current player state
/// </summary>
public enum PlayerState
{
/// <summary>
/// No disc is loaded
/// </summary>
NoDisc,
/// <summary>
/// Disc is loaded, playback is stopped
/// </summary>
Stopped,
/// <summary>
/// Disc is loaded, playback is paused
/// </summary>
Paused,
/// <summary>
/// Disc is loaded, playback enabled
/// </summary>
Playing,
}
}

View File

@@ -13,12 +13,10 @@ namespace RedBookPlayer.Models.Factories
/// Generate an OpticalDisc from an input path /// Generate an OpticalDisc from an input path
/// </summary> /// </summary>
/// <param name="path">Path to load the image from</param> /// <param name="path">Path to load the image from</param>
/// <param name="generateMissingToc">Generate a TOC if the disc is missing one [CompactDisc only]</param> /// <param name="options">Options to pass to the optical disc factory</param>
/// <param name="loadHiddenTracks">Load hidden tracks for playback [CompactDisc only]</param>
/// <param name="loadDataTracks">Load data tracks for playback [CompactDisc only]</param>
/// <param name="autoPlay">True if the image should be playable immediately, false otherwise</param> /// <param name="autoPlay">True if the image should be playable immediately, false otherwise</param>
/// <returns>Instantiated OpticalDisc, if possible</returns> /// <returns>Instantiated OpticalDisc, if possible</returns>
public static OpticalDiscBase GenerateFromPath(string path, bool generateMissingToc, bool loadHiddenTracks, bool loadDataTracks, bool autoPlay) public static OpticalDiscBase GenerateFromPath(string path, OpticalDiscOptions options, bool autoPlay)
{ {
try try
{ {
@@ -34,7 +32,7 @@ namespace RedBookPlayer.Models.Factories
image.Open(filter); image.Open(filter);
// Generate and instantiate the disc // Generate and instantiate the disc
return GenerateFromImage(image, generateMissingToc, loadHiddenTracks, loadDataTracks, autoPlay); return GenerateFromImage(image, options, autoPlay);
} }
catch catch
{ {
@@ -47,12 +45,10 @@ namespace RedBookPlayer.Models.Factories
/// Generate an OpticalDisc from an input IOpticalMediaImage /// Generate an OpticalDisc from an input IOpticalMediaImage
/// </summary> /// </summary>
/// <param name="image">IOpticalMediaImage to create from</param> /// <param name="image">IOpticalMediaImage to create from</param>
/// <param name="generateMissingToc">Generate a TOC if the disc is missing one [CompactDisc only]</param> /// <param name="options">Options to pass to the optical disc factory</param>
/// <param name="loadHiddenTracks">Load hidden tracks for playback [CompactDisc only]</param>
/// <param name="loadDataTracks">Load data tracks for playback [CompactDisc only]</param>
/// <param name="autoPlay">True if the image should be playable immediately, false otherwise</param> /// <param name="autoPlay">True if the image should be playable immediately, false otherwise</param>
/// <returns>Instantiated OpticalDisc, if possible</returns> /// <returns>Instantiated OpticalDisc, if possible</returns>
public static OpticalDiscBase GenerateFromImage(IOpticalMediaImage image, bool generateMissingToc, bool loadHiddenTracks, bool loadDataTracks, bool autoPlay) public static OpticalDiscBase GenerateFromImage(IOpticalMediaImage image, OpticalDiscOptions options, bool autoPlay)
{ {
// If the image is not usable, we don't do anything // If the image is not usable, we don't do anything
if(!IsUsableImage(image)) if(!IsUsableImage(image))
@@ -66,7 +62,7 @@ namespace RedBookPlayer.Models.Factories
{ {
case "Compact Disc": case "Compact Disc":
case "GD": case "GD":
opticalDisc = new CompactDisc(generateMissingToc, loadHiddenTracks, loadDataTracks); opticalDisc = new CompactDisc(options);
break; break;
default: default:
opticalDisc = null; opticalDisc = null;
@@ -118,4 +114,4 @@ namespace RedBookPlayer.Models.Factories
}; };
} }
} }
} }

View File

@@ -1,4 +1,3 @@
using System;
using System.ComponentModel; using System.ComponentModel;
using Aaru.CommonTypes.Enums; using Aaru.CommonTypes.Enums;
using ReactiveUI; using ReactiveUI;
@@ -12,7 +11,13 @@ namespace RedBookPlayer.Models.Hardware
/// <summary> /// <summary>
/// Indicate if the player is ready to be used /// Indicate if the player is ready to be used
/// </summary> /// </summary>
public bool Initialized { get; private set; } = false; public bool Initialized
{
get => _initialized;
private set => this.RaiseAndSetIfChanged(ref _initialized, value);
}
private bool _initialized;
#region OpticalDisc Passthrough #region OpticalDisc Passthrough
@@ -100,27 +105,27 @@ namespace RedBookPlayer.Models.Hardware
/// <summary> /// <summary>
/// Represents the total tracks on the disc /// Represents the total tracks on the disc
/// </summary> /// </summary>
public int TotalTracks => _opticalDisc.TotalTracks; public int TotalTracks => _opticalDisc?.TotalTracks ?? 0;
/// <summary> /// <summary>
/// Represents the total indices on the disc /// Represents the total indices on the disc
/// </summary> /// </summary>
public int TotalIndexes => _opticalDisc.TotalIndexes; public int TotalIndexes => _opticalDisc?.TotalIndexes ?? 0;
/// <summary> /// <summary>
/// Total sectors in the image /// Total sectors in the image
/// </summary> /// </summary>
public ulong TotalSectors => _opticalDisc.TotalSectors; public ulong TotalSectors => _opticalDisc?.TotalSectors ?? 0;
/// <summary> /// <summary>
/// Represents the time adjustment offset for the disc /// Represents the time adjustment offset for the disc
/// </summary> /// </summary>
public ulong TimeOffset => _opticalDisc.TimeOffset; public ulong TimeOffset => _opticalDisc?.TimeOffset ?? 0;
/// <summary> /// <summary>
/// Represents the total playing time for the disc /// Represents the total playing time for the disc
/// </summary> /// </summary>
public ulong TotalTime => _opticalDisc.TotalTime; public ulong TotalTime => _opticalDisc?.TotalTime ?? 0;
private int _currentTrackNumber; private int _currentTrackNumber;
private ushort _currentTrackIndex; private ushort _currentTrackIndex;
@@ -138,12 +143,21 @@ namespace RedBookPlayer.Models.Hardware
#region SoundOutput Passthrough #region SoundOutput Passthrough
/// <summary> /// <summary>
/// Indicate if the output is playing /// Indicates the current player state
/// </summary> /// </summary>
public bool? Playing public PlayerState PlayerState
{ {
get => _playing; get => _playerState;
private set => this.RaiseAndSetIfChanged(ref _playing, value); private set => this.RaiseAndSetIfChanged(ref _playerState, value);
}
/// <summary>
/// Indicates how to handle playback of data tracks
/// </summary>
public DataPlayback DataPlayback
{
get => _dataPlayback;
private set => this.RaiseAndSetIfChanged(ref _dataPlayback, value);
} }
/// <summary> /// <summary>
@@ -164,7 +178,8 @@ namespace RedBookPlayer.Models.Hardware
private set => this.RaiseAndSetIfChanged(ref _volume, value); private set => this.RaiseAndSetIfChanged(ref _volume, value);
} }
private bool? _playing; private PlayerState _playerState;
private DataPlayback _dataPlayback;
private bool _applyDeEmphasis; private bool _applyDeEmphasis;
private int _volume; private int _volume;
@@ -180,7 +195,7 @@ namespace RedBookPlayer.Models.Hardware
/// <summary> /// <summary>
/// OpticalDisc object /// OpticalDisc object
/// </summary> /// </summary>
private readonly OpticalDiscBase _opticalDisc; private OpticalDiscBase _opticalDisc;
/// <summary> /// <summary>
/// Last volume for mute toggling /// Last volume for mute toggling
@@ -190,23 +205,29 @@ namespace RedBookPlayer.Models.Hardware
#endregion #endregion
/// <summary> /// <summary>
/// Create a new Player from a given image path /// Constructor
/// </summary>
/// <param name="defaultVolume">Default volume between 0 and 100 to use when starting playback</param>
public Player(int defaultVolume)
{
Initialized = false;
_soundOutput = new SoundOutput(defaultVolume);
_soundOutput.SetDeEmphasis(false);
}
/// <summary>
/// Initializes player from a given image path
/// </summary> /// </summary>
/// <param name="path">Path to the disc image</param> /// <param name="path">Path to the disc image</param>
/// <param name="generateMissingToc">Generate a TOC if the disc is missing one [CompactDisc only]</param> /// <param name="options">Options to pass to the optical disc factory</param>
/// <param name="loadHiddenTracks">Load hidden tracks for playback [CompactDisc only]</param>
/// <param name="loadDataTracks">Load data tracks for playback [CompactDisc only]</param>
/// <param name="autoPlay">True if playback should begin immediately, false otherwise</param> /// <param name="autoPlay">True if playback should begin immediately, false otherwise</param>
/// <param name="defaultVolume">Default volume between 0 and 100 to use when starting playback</param> public void Init(string path, OpticalDiscOptions options, bool autoPlay)
public Player(string path, bool generateMissingToc, bool loadHiddenTracks, bool loadDataTracks, bool autoPlay, int defaultVolume)
{ {
// Set the internal state for initialization // Reset initialization
Initialized = false; Initialized = false;
_soundOutput = new SoundOutput();
_soundOutput.SetDeEmphasis(false);
// Initalize the disc // Initalize the disc
_opticalDisc = OpticalDiscFactory.GenerateFromPath(path, generateMissingToc, loadHiddenTracks, loadDataTracks, autoPlay); _opticalDisc = OpticalDiscFactory.GenerateFromPath(path, options, autoPlay);
if(_opticalDisc == null || !_opticalDisc.Initialized) if(_opticalDisc == null || !_opticalDisc.Initialized)
return; return;
@@ -214,7 +235,7 @@ namespace RedBookPlayer.Models.Hardware
_opticalDisc.PropertyChanged += OpticalDiscStateChanged; _opticalDisc.PropertyChanged += OpticalDiscStateChanged;
// Initialize the sound output // Initialize the sound output
_soundOutput.Init(_opticalDisc, autoPlay, defaultVolume); _soundOutput.Init(_opticalDisc, autoPlay);
if(_soundOutput == null || !_soundOutput.Initialized) if(_soundOutput == null || !_soundOutput.Initialized)
return; return;
@@ -240,12 +261,12 @@ namespace RedBookPlayer.Models.Hardware
return; return;
else if(_soundOutput == null) else if(_soundOutput == null)
return; return;
else if(_soundOutput.Playing) else if(_soundOutput.PlayerState != PlayerState.Paused && _soundOutput.PlayerState != PlayerState.Stopped)
return; return;
_soundOutput.Play(); _soundOutput.Play();
_opticalDisc.SetTotalIndexes(); _opticalDisc.SetTotalIndexes();
Playing = true; PlayerState = PlayerState.Playing;
} }
/// <summary> /// <summary>
@@ -257,11 +278,11 @@ namespace RedBookPlayer.Models.Hardware
return; return;
else if(_soundOutput == null) else if(_soundOutput == null)
return; return;
else if(!_soundOutput.Playing) else if(_soundOutput.PlayerState != PlayerState.Playing)
return; return;
_soundOutput?.Stop(); _soundOutput?.Pause();
Playing = false; PlayerState = PlayerState.Paused;
} }
/// <summary> /// <summary>
@@ -269,10 +290,20 @@ namespace RedBookPlayer.Models.Hardware
/// </summary> /// </summary>
public void TogglePlayback() public void TogglePlayback()
{ {
if(Playing == true) switch(PlayerState)
Pause(); {
else case PlayerState.NoDisc:
Play(); break;
case PlayerState.Stopped:
Play();
break;
case PlayerState.Paused:
Play();
break;
case PlayerState.Playing:
Pause();
break;
}
} }
/// <summary> /// <summary>
@@ -284,12 +315,12 @@ namespace RedBookPlayer.Models.Hardware
return; return;
else if(_soundOutput == null) else if(_soundOutput == null)
return; return;
else if(!_soundOutput.Playing) else if(_soundOutput.PlayerState != PlayerState.Playing && _soundOutput.PlayerState != PlayerState.Paused)
return; return;
_soundOutput?.Stop(); _soundOutput.Stop();
_opticalDisc.LoadFirstTrack(); _opticalDisc.LoadFirstTrack();
Playing = null; PlayerState = PlayerState.Stopped;
} }
/// <summary> /// <summary>
@@ -300,14 +331,16 @@ namespace RedBookPlayer.Models.Hardware
if(_opticalDisc == null || !_opticalDisc.Initialized) if(_opticalDisc == null || !_opticalDisc.Initialized)
return; return;
bool? wasPlaying = Playing; PlayerState wasPlaying = PlayerState;
if(wasPlaying == true) Pause(); if(wasPlaying == PlayerState.Playing)
Pause();
_opticalDisc.NextTrack(); _opticalDisc.NextTrack();
if(_opticalDisc is CompactDisc compactDisc) if(_opticalDisc is CompactDisc compactDisc)
_soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis); _soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis);
if(wasPlaying == true) Play(); if(wasPlaying == PlayerState.Playing)
Play();
} }
/// <summary> /// <summary>
@@ -318,14 +351,16 @@ namespace RedBookPlayer.Models.Hardware
if(_opticalDisc == null || !_opticalDisc.Initialized) if(_opticalDisc == null || !_opticalDisc.Initialized)
return; return;
bool? wasPlaying = Playing; PlayerState wasPlaying = PlayerState;
if(wasPlaying == true) Pause(); if(wasPlaying == PlayerState.Playing)
Pause();
_opticalDisc.PreviousTrack(); _opticalDisc.PreviousTrack();
if(_opticalDisc is CompactDisc compactDisc) if(_opticalDisc is CompactDisc compactDisc)
_soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis); _soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis);
if(wasPlaying == true) Play(); if(wasPlaying == PlayerState.Playing)
Play();
} }
/// <summary> /// <summary>
@@ -337,14 +372,16 @@ namespace RedBookPlayer.Models.Hardware
if(_opticalDisc == null || !_opticalDisc.Initialized) if(_opticalDisc == null || !_opticalDisc.Initialized)
return; return;
bool? wasPlaying = Playing; PlayerState wasPlaying = PlayerState;
if(wasPlaying == true) Pause(); if(wasPlaying == PlayerState.Playing)
Pause();
_opticalDisc.NextIndex(changeTrack); _opticalDisc.NextIndex(changeTrack);
if(_opticalDisc is CompactDisc compactDisc) if(_opticalDisc is CompactDisc compactDisc)
_soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis); _soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis);
if(wasPlaying == true) Play(); if(wasPlaying == PlayerState.Playing)
Play();
} }
/// <summary> /// <summary>
@@ -356,37 +393,38 @@ namespace RedBookPlayer.Models.Hardware
if(_opticalDisc == null || !_opticalDisc.Initialized) if(_opticalDisc == null || !_opticalDisc.Initialized)
return; return;
bool? wasPlaying = Playing; PlayerState wasPlaying = PlayerState;
if(wasPlaying == true) Pause(); if(wasPlaying == PlayerState.Playing)
Pause();
_opticalDisc.PreviousIndex(changeTrack); _opticalDisc.PreviousIndex(changeTrack);
if(_opticalDisc is CompactDisc compactDisc) if(_opticalDisc is CompactDisc compactDisc)
_soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis); _soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis);
if(wasPlaying == true) Play(); if(wasPlaying == PlayerState.Playing)
Play();
} }
/// <summary> /// <summary>
/// Fast-forward playback by 75 sectors, if possible /// Fast-forward playback by 75 sectors
/// </summary> /// </summary>
public void FastForward() public void FastForward()
{ {
if(_opticalDisc == null || !_opticalDisc.Initialized) if(_opticalDisc == null || !_opticalDisc.Initialized)
return; return;
_opticalDisc.SetCurrentSector(Math.Min(_opticalDisc.TotalSectors, _opticalDisc.CurrentSector + 75)); _opticalDisc.SetCurrentSector(_opticalDisc.CurrentSector + 75);
} }
/// <summary> /// <summary>
/// Rewind playback by 75 sectors, if possible /// Rewind playback by 75 sectors
/// </summary> /// </summary>
public void Rewind() public void Rewind()
{ {
if(_opticalDisc == null || !_opticalDisc.Initialized) if(_opticalDisc == null || !_opticalDisc.Initialized)
return; return;
if(_opticalDisc.CurrentSector >= 75) _opticalDisc.SetCurrentSector(_opticalDisc.CurrentSector - 75);
_opticalDisc.SetCurrentSector(_opticalDisc.CurrentSector - 75);
} }
#endregion #endregion
@@ -456,13 +494,13 @@ namespace RedBookPlayer.Models.Hardware
#region Helpers #region Helpers
/// <summary> /// <summary>
/// Set the value for loading data tracks [CompactDisc only] /// Set data playback method [CompactDisc only]
/// </summary> /// </summary>
/// <param name="load">True to enable loading data tracks, false otherwise</param> /// <param name="dataPlayback">New playback value</param>
public void SetLoadDataTracks(bool load) public void SetDataPlayback(DataPlayback dataPlayback)
{ {
if(_opticalDisc is CompactDisc compactDisc) if(_opticalDisc is CompactDisc compactDisc)
compactDisc.LoadDataTracks = load; compactDisc.DataPlayback = dataPlayback;
} }
/// <summary> /// <summary>
@@ -508,7 +546,7 @@ namespace RedBookPlayer.Models.Hardware
/// </summary> /// </summary>
private void SoundOutputStateChanged(object sender, PropertyChangedEventArgs e) private void SoundOutputStateChanged(object sender, PropertyChangedEventArgs e)
{ {
Playing = _soundOutput.Playing; PlayerState = _soundOutput.PlayerState;
ApplyDeEmphasis = _soundOutput.ApplyDeEmphasis; ApplyDeEmphasis = _soundOutput.ApplyDeEmphasis;
Volume = _soundOutput.Volume; Volume = _soundOutput.Volume;
} }

View File

@@ -16,15 +16,19 @@ namespace RedBookPlayer.Models.Hardware
/// <summary> /// <summary>
/// Indicate if the output is ready to be used /// Indicate if the output is ready to be used
/// </summary> /// </summary>
public bool Initialized { get; private set; } = false; public bool Initialized
{
get => _initialized;
private set => this.RaiseAndSetIfChanged(ref _initialized, value);
}
/// <summary> /// <summary>
/// Indicate if the output is playing /// Indicates the current player state
/// </summary> /// </summary>
public bool Playing public PlayerState PlayerState
{ {
get => _playing; get => _playerState;
private set => this.RaiseAndSetIfChanged(ref _playing, value); private set => this.RaiseAndSetIfChanged(ref _playerState, value);
} }
/// <summary> /// <summary>
@@ -54,7 +58,8 @@ namespace RedBookPlayer.Models.Hardware
} }
} }
private bool _playing; private bool _initialized;
private PlayerState _playerState;
private bool _applyDeEmphasis; private bool _applyDeEmphasis;
private int _volume; private int _volume;
@@ -102,13 +107,18 @@ namespace RedBookPlayer.Models.Hardware
#endregion #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;
/// <summary> /// <summary>
/// Initialize the output with a given image /// Initialize the output with a given image
/// </summary> /// </summary>
/// <param name="opticalDisc">OpticalDisc to load from</param> /// <param name="opticalDisc">OpticalDisc to load from</param>
/// <param name="autoPlay">True if playback should begin immediately, false otherwise</param> /// <param name="autoPlay">True if playback should begin immediately, false otherwise</param>
/// <param name="defaultVolume">Default volume between 0 and 100 to use when starting playback</param> public void Init(OpticalDiscBase opticalDisc, bool autoPlay)
public void Init(OpticalDiscBase opticalDisc, bool autoPlay = false, int defaultVolume = 100)
{ {
// If we have an unusable disc, just return // If we have an unusable disc, just return
if(opticalDisc == null || !opticalDisc.Initialized) if(opticalDisc == null || !opticalDisc.Initialized)
@@ -117,9 +127,6 @@ namespace RedBookPlayer.Models.Hardware
// Save a reference to the disc // Save a reference to the disc
_opticalDisc = opticalDisc; _opticalDisc = opticalDisc;
// Set the initial playback volume
Volume = defaultVolume;
// Enable de-emphasis for CDs, if necessary // Enable de-emphasis for CDs, if necessary
if(opticalDisc is CompactDisc compactDisc) if(opticalDisc is CompactDisc compactDisc)
ApplyDeEmphasis = compactDisc.TrackHasEmphasis; ApplyDeEmphasis = compactDisc.TrackHasEmphasis;
@@ -136,6 +143,7 @@ namespace RedBookPlayer.Models.Hardware
// Mark the output as ready // Mark the output as ready
Initialized = true; Initialized = true;
PlayerState = PlayerState.Stopped;
// Begin loading data // Begin loading data
_source.Start(); _source.Start();
@@ -163,57 +171,14 @@ namespace RedBookPlayer.Models.Hardware
// Determine how many sectors we can read // Determine how many sectors we can read
DetermineReadAmount(count, out ulong sectorsToRead, out ulong zeroSectorsAmount); DetermineReadAmount(count, out ulong sectorsToRead, out ulong zeroSectorsAmount);
// Create padding data for overreads // Get data to return
byte[] zeroSectors = new byte[(int)zeroSectorsAmount * _opticalDisc.BytesPerSector]; byte[] audioDataSegment = ReadData(count, sectorsToRead, zeroSectorsAmount);
byte[] audioData; if(audioDataSegment == null)
// 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(ArgumentOutOfRangeException)
{
_opticalDisc.LoadFirstTrack();
}
}
return zeroSectors;
}
});
// Wait 100ms at longest for the read to occur
if(readSectorTask.Wait(TimeSpan.FromMilliseconds(100)))
{
audioData = readSectorTask.Result;
}
else
{ {
Array.Clear(buffer, offset, count); Array.Clear(buffer, offset, count);
return count; return count;
} }
// 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)
{
Array.Clear(buffer, offset, count);
return count;
}
Array.Copy(audioData, _currentSectorReadPosition, audioDataSegment, 0, copyAmount);
// Apply de-emphasis filtering, only if enabled
if(ApplyDeEmphasis)
ProcessDeEmphasis(audioDataSegment);
// Write out the audio data to the buffer // Write out the audio data to the buffer
Array.Copy(audioDataSegment, 0, buffer, offset, count); Array.Copy(audioDataSegment, 0, buffer, offset, count);
@@ -221,6 +186,7 @@ namespace RedBookPlayer.Models.Hardware
_currentSectorReadPosition += count; _currentSectorReadPosition += count;
if(_currentSectorReadPosition >= _opticalDisc.BytesPerSector) if(_currentSectorReadPosition >= _opticalDisc.BytesPerSector)
{ {
int currentTrack = _opticalDisc.CurrentTrackNumber;
_opticalDisc.SetCurrentSector(_opticalDisc.CurrentSector + (ulong)(_currentSectorReadPosition / _opticalDisc.BytesPerSector)); _opticalDisc.SetCurrentSector(_opticalDisc.CurrentSector + (ulong)(_currentSectorReadPosition / _opticalDisc.BytesPerSector));
_currentSectorReadPosition %= _opticalDisc.BytesPerSector; _currentSectorReadPosition %= _opticalDisc.BytesPerSector;
} }
@@ -235,10 +201,10 @@ namespace RedBookPlayer.Models.Hardware
/// </summary> /// </summary>
public void Play() public void Play()
{ {
if (_soundOut.PlaybackState != PlaybackState.Playing) if(_soundOut.PlaybackState != PlaybackState.Playing)
_soundOut.Play(); _soundOut.Play();
Playing = _soundOut.PlaybackState == PlaybackState.Playing; PlayerState = PlayerState.Playing;
} }
/// <summary> /// <summary>
@@ -249,7 +215,7 @@ namespace RedBookPlayer.Models.Hardware
if(_soundOut.PlaybackState != PlaybackState.Paused) if(_soundOut.PlaybackState != PlaybackState.Paused)
_soundOut.Pause(); _soundOut.Pause();
Playing = _soundOut.PlaybackState == PlaybackState.Playing; PlayerState = PlayerState.Paused;
} }
/// <summary> /// <summary>
@@ -260,7 +226,7 @@ namespace RedBookPlayer.Models.Hardware
if(_soundOut.PlaybackState != PlaybackState.Stopped) if(_soundOut.PlaybackState != PlaybackState.Stopped)
_soundOut.Stop(); _soundOut.Stop();
Playing = _soundOut.PlaybackState == PlaybackState.Playing; PlayerState = PlayerState.Stopped;
} }
#endregion #endregion
@@ -270,7 +236,7 @@ namespace RedBookPlayer.Models.Hardware
/// <summary> /// <summary>
/// Set de-emphasis status /// Set de-emphasis status
/// </summary> /// </summary>
/// <param name="apply"></param> /// <param name="apply">New de-emphasis status</param>
public void SetDeEmphasis(bool apply) => ApplyDeEmphasis = apply; public void SetDeEmphasis(bool apply) => ApplyDeEmphasis = apply;
/// <summary> /// <summary>
@@ -287,30 +253,19 @@ namespace RedBookPlayer.Models.Hardware
/// <param name="zeroSectorsAmount">Number of zeroed sectors to concatenate</param> /// <param name="zeroSectorsAmount">Number of zeroed sectors to concatenate</param>
private void DetermineReadAmount(int count, out ulong sectorsToRead, out ulong zeroSectorsAmount) private void DetermineReadAmount(int count, out ulong sectorsToRead, out ulong zeroSectorsAmount)
{ {
do // Attempt to read 5 more sectors than requested
sectorsToRead = ((ulong)count / (ulong)_opticalDisc.BytesPerSector) + 5;
zeroSectorsAmount = 0;
// Avoid overreads by padding with 0-byte data at the end
if(_opticalDisc.CurrentSector + sectorsToRead > _opticalDisc.TotalSectors)
{ {
// Attempt to read 2 more sectors than requested ulong oldSectorsToRead = sectorsToRead;
sectorsToRead = ((ulong)count / (ulong)_opticalDisc.BytesPerSector) + 2; sectorsToRead = _opticalDisc.TotalSectors - _opticalDisc.CurrentSector;
zeroSectorsAmount = 0;
// Avoid overreads by padding with 0-byte data at the end int tempZeroSectorCount = (int)(oldSectorsToRead - sectorsToRead);
if(_opticalDisc.CurrentSector + sectorsToRead > _opticalDisc.TotalSectors) zeroSectorsAmount = (ulong)(tempZeroSectorCount < 0 ? 0 : tempZeroSectorCount);
{ }
ulong oldSectorsToRead = sectorsToRead;
sectorsToRead = _opticalDisc.TotalSectors - _opticalDisc.CurrentSector;
int tempZeroSectorCount = (int)(oldSectorsToRead - sectorsToRead);
zeroSectorsAmount = (ulong)(tempZeroSectorCount < 0 ? 0 : tempZeroSectorCount);
}
// If we're reading past the last sector of the disc, wrap around
// TODO: Have past-end reads looping back controlled by a flag instead (Repeat? Repeat All?)
if(sectorsToRead <= 0)
{
_opticalDisc.LoadFirstTrack();
_currentSectorReadPosition = 0;
}
} while(sectorsToRead <= 0);
} }
/// <summary> /// <summary>
@@ -333,6 +288,58 @@ namespace RedBookPlayer.Models.Hardware
ByteConverter.FromFloats16Bit(floatAudioData, audioData); ByteConverter.FromFloats16Bit(floatAudioData, audioData);
} }
/// <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)
ProcessDeEmphasis(audioDataSegment);
return audioDataSegment;
}
/// <summary> /// <summary>
/// Sets or resets the de-emphasis filters /// Sets or resets the de-emphasis filters
/// </summary> /// </summary>
@@ -369,4 +376,4 @@ namespace RedBookPlayer.Models.Hardware
#endregion #endregion
} }
} }