diff --git a/README.md b/README.md
index 7b0ec36..22a22c2 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,8 @@
* 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
| Key | Action |
diff --git a/RedBookPlayer.GUI/App.xaml.cs b/RedBookPlayer.GUI/App.xaml.cs
index 20e6a9d..08dedee 100644
--- a/RedBookPlayer.GUI/App.xaml.cs
+++ b/RedBookPlayer.GUI/App.xaml.cs
@@ -5,14 +5,20 @@ using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
-using RedBookPlayer.GUI;
+using RedBookPlayer.GUI.ViewModels;
using RedBookPlayer.GUI.Views;
namespace RedBookPlayer
{
public class App : Application
{
- public static Settings Settings;
+ public static MainWindow MainWindow;
+ public static SettingsViewModel Settings;
+
+ ///
+ /// Read-only access to the current player view
+ ///
+ public static PlayerView PlayerView => MainWindow?.ViewModel?.PlayerView;
static App() =>
Directory.SetCurrentDirectory(Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName));
@@ -24,6 +30,7 @@ namespace RedBookPlayer
Console.WriteLine(((Exception)f.ExceptionObject).ToString());
};
+ Settings = SettingsViewModel.Load("settings.json");
AvaloniaXamlLoader.Load(this);
}
@@ -31,10 +38,11 @@ namespace RedBookPlayer
{
if(ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
- desktop.MainWindow = new MainWindow();
+ MainWindow = new MainWindow();
+ desktop.MainWindow = MainWindow;
desktop.ShutdownMode = ShutdownMode.OnMainWindowClose;
- Settings = Settings.Load("settings.json");
+ PlayerView.ViewModel.ApplyTheme(Settings.SelectedTheme);
}
base.OnFrameworkInitializationCompleted();
diff --git a/RedBookPlayer.GUI/Program.cs b/RedBookPlayer.GUI/Program.cs
index b1919b6..55247e4 100644
--- a/RedBookPlayer.GUI/Program.cs
+++ b/RedBookPlayer.GUI/Program.cs
@@ -4,6 +4,7 @@ using System.Runtime.InteropServices;
#endif
using Avalonia;
using Avalonia.Logging.Serilog;
+using Avalonia.ReactiveUI;
namespace RedBookPlayer.GUI
{
@@ -24,6 +25,6 @@ namespace RedBookPlayer.GUI
static extern bool AllocConsole();
#endif
- public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure().UsePlatformDetect().LogToDebug();
+ public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure().UseReactiveUI().UsePlatformDetect().LogToDebug();
}
}
\ No newline at end of file
diff --git a/RedBookPlayer.GUI/RedBookPlayer.GUI.csproj b/RedBookPlayer.GUI/RedBookPlayer.GUI.csproj
index 36d6bb4..22e946e 100644
--- a/RedBookPlayer.GUI/RedBookPlayer.GUI.csproj
+++ b/RedBookPlayer.GUI/RedBookPlayer.GUI.csproj
@@ -13,7 +13,7 @@
%(Filename)
-
+
Designer
@@ -28,4 +28,11 @@
+
+
+ Always
+ Always
+ true
+
+
diff --git a/RedBookPlayer.GUI/ViewModels/MainViewModel.cs b/RedBookPlayer.GUI/ViewModels/MainViewModel.cs
new file mode 100644
index 0000000..9faa1f5
--- /dev/null
+++ b/RedBookPlayer.GUI/ViewModels/MainViewModel.cs
@@ -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
+ {
+ ///
+ /// Read-only access to the control
+ ///
+ public ContentControl ContentControl => App.MainWindow.FindControl("Content");
+
+ ///
+ /// Read-only access to the view
+ ///
+ public PlayerView PlayerView => ContentControl?.Content as PlayerView;
+
+ #region Helpers
+
+ ///
+ /// Execute the result of a keypress
+ ///
+ 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();
+ }
+ }
+
+ ///
+ /// Load the first valid drag-and-dropped disc image
+ ///
+ public async void ExecuteLoadDragDrop(object sender, DragEventArgs e)
+ {
+ if(PlayerView?.ViewModel == null)
+ return;
+
+ IEnumerable fileNames = e.Data.GetFileNames();
+ foreach(string filename in fileNames)
+ {
+ bool loaded = await PlayerView.ViewModel.LoadImage(filename);
+ if(loaded)
+ break;
+ }
+ }
+
+ ///
+ /// Stop current playback
+ ///
+ public void ExecuteStop(object sender, CancelEventArgs e) => PlayerView?.ViewModel?.ExecuteStop();
+
+ ///
+ /// Handle the settings window closing
+ ///
+ private void OnSettingsClosed(object sender, EventArgs e) => PlayerView?.ViewModel?.RefreshFromSettings();
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/RedBookPlayer.GUI/ViewModels/PlayerViewModel.cs b/RedBookPlayer.GUI/ViewModels/PlayerViewModel.cs
index 41edc50..00f92d5 100644
--- a/RedBookPlayer.GUI/ViewModels/PlayerViewModel.cs
+++ b/RedBookPlayer.GUI/ViewModels/PlayerViewModel.cs
@@ -5,13 +5,16 @@ using System.IO;
using System.Linq;
using System.Reactive;
using System.Threading.Tasks;
+using System.Xml;
using Avalonia;
using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Threading;
using ReactiveUI;
-using RedBookPlayer.GUI.Views;
+using RedBookPlayer.Models;
+using RedBookPlayer.Models.Discs;
using RedBookPlayer.Models.Hardware;
namespace RedBookPlayer.GUI.ViewModels
@@ -21,7 +24,7 @@ namespace RedBookPlayer.GUI.ViewModels
///
/// Player representing the internal state
///
- private Player _player;
+ private readonly Player _player;
///
/// Set of images representing the digits for the UI
@@ -50,6 +53,15 @@ namespace RedBookPlayer.GUI.ViewModels
private set => this.RaiseAndSetIfChanged(ref _currentTrackIndex, value);
}
+ ///
+ /// Current track session
+ ///
+ public ushort CurrentTrackSession
+ {
+ get => _currentTrackSession;
+ private set => this.RaiseAndSetIfChanged(ref _currentTrackSession, value);
+ }
+
///
/// Current sector number
///
@@ -140,6 +152,7 @@ namespace RedBookPlayer.GUI.ViewModels
private int _currentTrackNumber;
private ushort _currentTrackIndex;
+ private ushort _currentTrackSession;
private ulong _currentSector;
private ulong _sectionStartSector;
@@ -156,15 +169,28 @@ namespace RedBookPlayer.GUI.ViewModels
///
/// Indicate if the model is ready to be used
///
- public bool Initialized => _player?.Initialized ?? false;
+ public bool Initialized
+ {
+ get => _initialized;
+ private set => this.RaiseAndSetIfChanged(ref _initialized, value);
+ }
///
/// Indicate if the output is playing
///
- public bool? Playing
+ public PlayerState PlayerState
{
- get => _playing;
- private set => this.RaiseAndSetIfChanged(ref _playing, value);
+ get => _playerState;
+ private set => this.RaiseAndSetIfChanged(ref _playerState, value);
+ }
+
+ ///
+ /// Indicates how to handle playback of data tracks
+ ///
+ public DataPlayback DataPlayback
+ {
+ get => _dataPlayback;
+ private set => this.RaiseAndSetIfChanged(ref _dataPlayback, value);
}
///
@@ -185,7 +211,9 @@ namespace RedBookPlayer.GUI.ViewModels
private set => this.RaiseAndSetIfChanged(ref _volume, value);
}
- private bool? _playing;
+ private bool _initialized;
+ private PlayerState _playerState;
+ private DataPlayback _dataPlayback;
private bool _applyDeEmphasis;
private int _volume;
@@ -299,6 +327,7 @@ namespace RedBookPlayer.GUI.ViewModels
///
public PlayerViewModel()
{
+ // Initialize commands
LoadCommand = ReactiveCommand.Create(ExecuteLoad);
PlayCommand = ReactiveCommand.Create(ExecutePlay);
@@ -319,25 +348,27 @@ namespace RedBookPlayer.GUI.ViewModels
EnableDeEmphasisCommand = ReactiveCommand.Create(ExecuteEnableDeEmphasis);
DisableDeEmphasisCommand = ReactiveCommand.Create(ExecuteDisableDeEmphasis);
ToggleDeEmphasisCommand = ReactiveCommand.Create(ExecuteToggleDeEmphasis);
+
+ // Initialize Player
+ _player = new Player(App.Settings.Volume);
+ PlayerState = PlayerState.NoDisc;
}
///
/// Initialize the view model with a given image path
///
/// Path to the disc image
- /// Generate a TOC if the disc is missing one [CompactDisc only]
- /// Load hidden tracks for playback [CompactDisc only]
- /// Load data tracks for playback [CompactDisc only]
+ /// Options to pass to the optical disc factory
/// True if playback should begin immediately, false otherwise
- /// Default volume between 0 and 100 to use when starting playback
- public void Init(string path, bool generateMissingToc, bool loadHiddenTracks, bool loadDataTracks, bool autoPlay, int defaultVolume)
+ public void Init(string path, OpticalDiscOptions options, bool autoPlay)
{
// Stop current playback, if necessary
- if(Playing != null) ExecuteStop();
+ if(PlayerState != PlayerState.NoDisc)
+ ExecuteStop();
- // Create and attempt to initialize new Player
- _player = new Player(path, generateMissingToc, loadHiddenTracks, loadDataTracks, autoPlay, defaultVolume);
- if(Initialized)
+ // Attempt to initialize Player
+ _player.Init(path, options, autoPlay);
+ if(_player.Initialized)
{
_player.PropertyChanged += PlayerStateChanged;
PlayerStateChanged(this, null);
@@ -444,6 +475,46 @@ namespace RedBookPlayer.GUI.ViewModels
#region Helpers
+ ///
+ /// Apply a custom theme to the player
+ ///
+ /// Path to the theme under the themes directory
+ 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();
+ }
+
///
/// Load a disc image from a selection box
///
@@ -461,35 +532,36 @@ namespace RedBookPlayer.GUI.ViewModels
///
public void InitializeDigits()
{
- PlayerView playerView = MainWindow.Instance.ContentControl.Content as PlayerView;
+ if(App.PlayerView == null)
+ return;
_digits = new Image[]
{
- playerView.FindControl("TrackDigit1"),
- playerView.FindControl("TrackDigit2"),
+ App.PlayerView.FindControl("TrackDigit1"),
+ App.PlayerView.FindControl("TrackDigit2"),
- playerView.FindControl("IndexDigit1"),
- playerView.FindControl("IndexDigit2"),
+ App.PlayerView.FindControl("IndexDigit1"),
+ App.PlayerView.FindControl("IndexDigit2"),
- playerView.FindControl("TimeDigit1"),
- playerView.FindControl("TimeDigit2"),
- playerView.FindControl("TimeDigit3"),
- playerView.FindControl("TimeDigit4"),
- playerView.FindControl("TimeDigit5"),
- playerView.FindControl("TimeDigit6"),
+ App.PlayerView.FindControl("TimeDigit1"),
+ App.PlayerView.FindControl("TimeDigit2"),
+ App.PlayerView.FindControl("TimeDigit3"),
+ App.PlayerView.FindControl("TimeDigit4"),
+ App.PlayerView.FindControl("TimeDigit5"),
+ App.PlayerView.FindControl("TimeDigit6"),
- playerView.FindControl("TotalTracksDigit1"),
- playerView.FindControl("TotalTracksDigit2"),
+ App.PlayerView.FindControl("TotalTracksDigit1"),
+ App.PlayerView.FindControl("TotalTracksDigit2"),
- playerView.FindControl("TotalIndexesDigit1"),
- playerView.FindControl("TotalIndexesDigit2"),
+ App.PlayerView.FindControl("TotalIndexesDigit1"),
+ App.PlayerView.FindControl("TotalIndexesDigit2"),
- playerView.FindControl("TotalTimeDigit1"),
- playerView.FindControl("TotalTimeDigit2"),
- playerView.FindControl("TotalTimeDigit3"),
- playerView.FindControl("TotalTimeDigit4"),
- playerView.FindControl("TotalTimeDigit5"),
- playerView.FindControl("TotalTimeDigit6"),
+ App.PlayerView.FindControl("TotalTimeDigit1"),
+ App.PlayerView.FindControl("TotalTimeDigit2"),
+ App.PlayerView.FindControl("TotalTimeDigit3"),
+ App.PlayerView.FindControl("TotalTimeDigit4"),
+ App.PlayerView.FindControl("TotalTimeDigit5"),
+ App.PlayerView.FindControl("TotalTimeDigit6"),
};
}
@@ -501,19 +573,39 @@ namespace RedBookPlayer.GUI.ViewModels
{
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)
- MainWindow.Instance.Title = "RedBookPlayer - " + path.Split('/').Last().Split('\\').Last();
+ App.MainWindow.Title = "RedBookPlayer - " + path.Split('/').Last().Split('\\').Last();
return Initialized;
});
}
///
- /// Set the value for loading data tracks [CompactDisc only]
+ /// Refresh the view model from the current settings
///
- /// True to enable loading data tracks, false otherwise
- public void SetLoadDataTracks(bool load) => _player?.SetLoadDataTracks(load);
+ public void RefreshFromSettings()
+ {
+ SetDataPlayback(App.Settings.DataPlayback);
+ SetLoadHiddenTracks(App.Settings.PlayHiddenTracks);
+ }
+
+ ///
+ /// Set data playback method [CompactDisc only]
+ ///
+ /// New playback value
+ public void SetDataPlayback(DataPlayback dataPlayback) => _player?.SetDataPlayback(dataPlayback);
///
/// Set the value for loading hidden tracks [CompactDisc only]
@@ -569,18 +661,9 @@ namespace RedBookPlayer.GUI.ViewModels
{
try
{
- if(App.Settings.SelectedTheme == "default")
- {
- IAssetLoader assets = AvaloniaLocator.Current.GetService();
-
- 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);
- }
+ string themeDirectory = $"{Directory.GetCurrentDirectory()}/themes/{App.Settings.SelectedTheme}";
+ using FileStream stream = File.Open($"{themeDirectory}/{character}.png", FileMode.Open);
+ return new Bitmap(stream);
}
catch
{
@@ -619,18 +702,56 @@ namespace RedBookPlayer.GUI.ViewModels
Extensions = knownExtensions.ConvertAll(e => e.TrimStart('.'))
});
- return (await dialog.ShowAsync(MainWindow.Instance))?.FirstOrDefault();
+ return (await dialog.ShowAsync(App.MainWindow))?.FirstOrDefault();
});
}
+ ///
+ /// Load the theme from a XAML, if possible
+ ///
+ /// XAML data representing the theme, null for default
+ 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();
+ }
+
///
/// Update the view-model from the Player
///
private void PlayerStateChanged(object sender, PropertyChangedEventArgs e)
{
- if(_player?.Initialized != true)
+ if(_player == null)
return;
+ if(!_player.Initialized)
+ {
+ Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ App.MainWindow.Title = "RedBookPlayer";
+ });
+ }
+
+ Initialized = _player.Initialized;
+
CurrentTrackNumber = _player.CurrentTrackNumber;
CurrentTrackIndex = _player.CurrentTrackIndex;
CurrentSector = _player.CurrentSector;
@@ -643,7 +764,8 @@ namespace RedBookPlayer.GUI.ViewModels
CopyAllowed = _player.CopyAllowed;
TrackHasEmphasis = _player.TrackHasEmphasis;
- Playing = _player.Playing;
+ PlayerState = _player.PlayerState;
+ DataPlayback = _player.DataPlayback;
ApplyDeEmphasis = _player.ApplyDeEmphasis;
Volume = _player.Volume;
@@ -655,6 +777,10 @@ namespace RedBookPlayer.GUI.ViewModels
///
private void UpdateDigits()
{
+ // Ensure the digits
+ if(_digits == null)
+ InitializeDigits();
+
Dispatcher.UIThread.Post(() =>
{
string digitString = GenerateDigitString() ?? string.Empty.PadLeft(20, '-');
diff --git a/RedBookPlayer.GUI/Settings.cs b/RedBookPlayer.GUI/ViewModels/SettingsViewModel.cs
similarity index 57%
rename from RedBookPlayer.GUI/Settings.cs
rename to RedBookPlayer.GUI/ViewModels/SettingsViewModel.cs
index df14848..c57e33d 100644
--- a/RedBookPlayer.GUI/Settings.cs
+++ b/RedBookPlayer.GUI/ViewModels/SettingsViewModel.cs
@@ -1,15 +1,32 @@
using System;
+using System.Collections.Generic;
using System.IO;
+using System.Linq;
+using System.Reactive;
using System.Text.Json;
+using System.Text.Json.Serialization;
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
+ ///
+ /// List of all data playback values
+ ///
+ [JsonIgnore]
+ public List DataPlaybackValues => GenerateDataPlaybackList();
+
+ ///
+ /// List of all themes
+ ///
+ [JsonIgnore]
+ public List ThemeValues => GenerateThemeList();
+
///
/// Indicates if discs should start playing on load
///
@@ -30,42 +47,51 @@ namespace RedBookPlayer.GUI
///
public bool PlayHiddenTracks { get; set; } = false;
- ///
- /// Indicates if data tracks should be played like old, non-compliant players
- ///
- public bool PlayDataTracks { get; set; } = false;
-
///
/// Generate a TOC if the disc is missing one
///
public bool GenerateMissingTOC { get; set; } = true;
+ ///
+ /// Indicates how to deal with data tracks
+ ///
+ public DataPlayback DataPlayback { get; set; } = DataPlayback.Skip;
+
///
/// Indicates the default playback volume
///
public int Volume
{
get => _volume;
- set
+ private set
{
+ int tempValue;
if(value > 100)
- _volume = 100;
+ tempValue = 100;
else if(value < 0)
- _volume = 0;
+ tempValue = 0;
else
- _volume = value;
+ tempValue = value;
+
+ this.RaiseAndSetIfChanged(ref _volume, tempValue);
}
}
///
/// Indicates the currently selected theme
///
- public string SelectedTheme { get; set; } = "default";
+ public string SelectedTheme { get; set; } = "Default";
#endregion
#region Key Mappings
+ ///
+ /// List of all keyboard keys
+ ///
+ [JsonIgnore]
+ public List KeyboardList => GenerateKeyList();
+
///
/// Key assigned to open settings
///
@@ -86,6 +112,11 @@ namespace RedBookPlayer.GUI
///
public Key StopPlaybackKey { get; set; } = Key.Escape;
+ ///
+ /// Key assigned to eject the disc
+ ///
+ public Key EjectKey { get; set; } = Key.OemTilde;
+
///
/// Key assigned to move to the next track
///
@@ -138,6 +169,15 @@ namespace RedBookPlayer.GUI
#endregion
+ #region Commands
+
+ ///
+ /// Command for applying settings
+ ///
+ public ReactiveCommand ApplySettingsCommand { get; }
+
+ #endregion
+
///
/// Path to the settings file
///
@@ -148,44 +188,50 @@ namespace RedBookPlayer.GUI
///
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);
+ }
///
/// Load settings from a file
///
/// Path to the settings JSON file
/// Settings derived from the input file, if possible
- public static Settings Load(string filePath)
+ public static SettingsViewModel Load(string filePath)
{
if(File.Exists(filePath))
{
try
{
- Settings settings = JsonSerializer.Deserialize(File.ReadAllText(filePath));
+ SettingsViewModel settings = JsonSerializer.Deserialize(File.ReadAllText(filePath));
settings._filePath = filePath;
- MainWindow.ApplyTheme(settings.SelectedTheme);
-
return settings;
}
catch(JsonException)
{
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);
}
///
- /// Save settings to a file
+ /// Apply settings from the UI
///
- public void Save()
+ public void ExecuteApplySettings()
{
+ if(!string.IsNullOrWhiteSpace(SelectedTheme))
+ App.PlayerView?.ViewModel?.ApplyTheme(SelectedTheme);
+
var options = new JsonSerializerOptions
{
WriteIndented = true
@@ -194,5 +240,45 @@ namespace RedBookPlayer.GUI
string json = JsonSerializer.Serialize(this, options);
File.WriteAllText(_filePath, json);
}
+
+ #region Generation
+
+ ///
+ /// Generate the list of DataPlayback values
+ ///
+ private List GenerateDataPlaybackList() => Enum.GetValues(typeof(DataPlayback)).Cast().ToList();
+
+ ///
+ /// Generate the list of Key values
+ ///
+ private List GenerateKeyList() => Enum.GetValues(typeof(Key)).Cast().ToList();
+
+ ///
+ /// Generate the list of valid themes
+ ///
+ private List GenerateThemeList()
+ {
+ // Create a list of all found themes
+ List items = new List();
+
+ // 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
}
}
\ No newline at end of file
diff --git a/RedBookPlayer.GUI/Views/MainWindow.xaml b/RedBookPlayer.GUI/Views/MainWindow.xaml
index 1a3e211..eb7576d 100644
--- a/RedBookPlayer.GUI/Views/MainWindow.xaml
+++ b/RedBookPlayer.GUI/Views/MainWindow.xaml
@@ -1,7 +1,17 @@
-
-
-
\ No newline at end of file
+ CanResize="False" DragDrop.AllowDrop="True">
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/RedBookPlayer.GUI/Views/MainWindow.xaml.cs b/RedBookPlayer.GUI/Views/MainWindow.xaml.cs
index 3e5a26a..6592b05 100644
--- a/RedBookPlayer.GUI/Views/MainWindow.xaml.cs
+++ b/RedBookPlayer.GUI/Views/MainWindow.xaml.cs
@@ -1,73 +1,13 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Xml;
-using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
using RedBookPlayer.GUI.ViewModels;
namespace RedBookPlayer.GUI.Views
{
- public class MainWindow : Window
+ public class MainWindow : ReactiveWindow
{
- public static MainWindow Instance;
- public ContentControl ContentControl;
- public Window settingsWindow;
-
- public MainWindow()
- {
- Instance = this;
- InitializeComponent();
- }
-
- ///
- /// Apply a custom theme to the player
- ///
- /// Path to the theme under the themes directory
- 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();
- }
+ public MainWindow() => InitializeComponent();
///
/// Initialize the main window
@@ -76,162 +16,10 @@ namespace RedBookPlayer.GUI.Views
{
AvaloniaXamlLoader.Load(this);
- ContentControl = this.FindControl("Content");
- ContentControl.Content = new PlayerView();
-
- Instance.MaxWidth = ((PlayerView)Instance.ContentControl.Content).Width;
- 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);
+ // Add handlers
+ Closing += ViewModel.ExecuteStop;
+ AddHandler(DragDrop.DropEvent, ViewModel.ExecuteLoadDragDrop);
+ KeyDown += ViewModel.ExecuteKeyPress;
}
-
- #region Event Handlers
-
- public async void MainWindow_Drop(object sender, DragEventArgs e)
- {
- PlayerView playerView = ContentControl.Content as PlayerView;
- if(playerView == null)
- return;
-
- IEnumerable 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
}
}
\ No newline at end of file
diff --git a/RedBookPlayer.GUI/Views/PlayerView.xaml b/RedBookPlayer.GUI/Views/PlayerView.xaml
index 941522e..c599c86 100644
--- a/RedBookPlayer.GUI/Views/PlayerView.xaml
+++ b/RedBookPlayer.GUI/Views/PlayerView.xaml
@@ -1,7 +1,12 @@
-
+ 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">
+
+
+
@@ -101,4 +106,4 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/RedBookPlayer.GUI/Views/PlayerView.xaml.cs b/RedBookPlayer.GUI/Views/PlayerView.xaml.cs
index 253fd9b..4e396b9 100644
--- a/RedBookPlayer.GUI/Views/PlayerView.xaml.cs
+++ b/RedBookPlayer.GUI/Views/PlayerView.xaml.cs
@@ -1,73 +1,9 @@
-using Avalonia.Controls;
-using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
using RedBookPlayer.GUI.ViewModels;
namespace RedBookPlayer.GUI.Views
{
- public class PlayerView : UserControl
+ public class PlayerView : ReactiveUserControl
{
- ///
- /// Read-only access to the view model
- ///
- public PlayerViewModel PlayerViewModel => DataContext as PlayerViewModel;
-
- ///
- /// Initialize the UI based on the default theme
- ///
- public PlayerView() : this(null, null) { }
-
- ///
- /// Initialize the UI based on the default theme with an existing view model
- ///
- /// XAML data representing the theme, null for default
- /// Existing PlayerViewModel to load in instead of creating a new one
- public PlayerView(PlayerViewModel playerViewModel) : this(null, playerViewModel) { }
-
- ///
- /// Initialize the UI based on the currently selected theme
- ///
- /// XAML data representing the theme, null for default
- /// Existing PlayerViewModel to load in instead of creating a new one
- public PlayerView(string xaml, PlayerViewModel playerViewModel)
- {
- LoadTheme(xaml);
-
- if(playerViewModel != null)
- DataContext = playerViewModel;
- else
- DataContext = new PlayerViewModel();
- }
-
- #region Helpers
-
- ///
- /// Update the view model with new settings
- ///
- public void UpdateViewModel()
- {
- PlayerViewModel.SetLoadDataTracks(App.Settings.PlayDataTracks);
- PlayerViewModel.SetLoadHiddenTracks(App.Settings.PlayHiddenTracks);
- }
-
- ///
- /// Load the theme from a XAML, if possible
- ///
- /// XAML data representing the theme, null for default
- private void LoadTheme(string xaml)
- {
- try
- {
- if(xaml != null)
- new AvaloniaXamlLoader().Load(xaml, null, this);
- else
- AvaloniaXamlLoader.Load(this);
- }
- catch
- {
- AvaloniaXamlLoader.Load(this);
- }
- }
-
- #endregion
}
}
\ No newline at end of file
diff --git a/RedBookPlayer.GUI/Views/SettingsWindow.xaml b/RedBookPlayer.GUI/Views/SettingsWindow.xaml
index d64c388..ab0a8c3 100644
--- a/RedBookPlayer.GUI/Views/SettingsWindow.xaml
+++ b/RedBookPlayer.GUI/Views/SettingsWindow.xaml
@@ -1,7 +1,12 @@
-
+
+
+
@@ -21,8 +26,9 @@
Play hidden tracks
-
- Play data tracks like old, non-compliant players
+ Data Track Playback
+
@@ -33,11 +39,12 @@
-
+
-
+
@@ -61,62 +68,96 @@
+
+
Load Image
-
+
- Toggle Play/Pause
-
+ Toggle Play/Pause
+
- Stop Playback
-
+ Stop Playback
+
+
+
+ Eject Disc
+
- Next Track
-
+ Next Track
+
- Previous Track
-
+ Previous Track
+
- Next Index
-
+ Next Index
+
- Previous Index
-
+ Previous Index
+
- Fast-Forward
-
+ Fast-Forward
+
- Rewind
-
+ Rewind
+
- Volume Up
-
+ Volume Up
+
- Volume Down
-
+ Volume Down
+
- Toggle Mute
-
+ Toggle Mute
+
- Toggle De-Emphasis
-
+ Toggle De-Emphasis
+
-
+
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/RedBookPlayer.GUI/Views/SettingsWindow.xaml.cs b/RedBookPlayer.GUI/Views/SettingsWindow.xaml.cs
index 9b5c15d..5e0e8ed 100644
--- a/RedBookPlayer.GUI/Views/SettingsWindow.xaml.cs
+++ b/RedBookPlayer.GUI/Views/SettingsWindow.xaml.cs
@@ -1,186 +1,9 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using Avalonia.Controls;
-using Avalonia.Input;
-using Avalonia.Interactivity;
-using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+using RedBookPlayer.GUI.ViewModels;
namespace RedBookPlayer.GUI.Views
{
- public class SettingsWindow : Window
+ public class SettingsWindow : ReactiveWindow
{
- 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("VolumeLabel").Text = _settings.Volume.ToString();
-
- void InitializeComponent()
- {
- AvaloniaXamlLoader.Load(this);
-
- PopulateThemes();
- PopulateKeyboardList();
-
- this.FindControl
/// IOpticalMediaImage to create from
- /// Generate a TOC if the disc is missing one [CompactDisc only]
- /// Load hidden tracks for playback [CompactDisc only]
- /// Load data tracks for playback [CompactDisc only]
+ /// Options to pass to the optical disc factory
/// True if the image should be playable immediately, false otherwise
/// Instantiated OpticalDisc, if possible
- public static OpticalDiscBase GenerateFromImage(IOpticalMediaImage image, 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(!IsUsableImage(image))
@@ -66,7 +62,7 @@ namespace RedBookPlayer.Models.Factories
{
case "Compact Disc":
case "GD":
- opticalDisc = new CompactDisc(generateMissingToc, loadHiddenTracks, loadDataTracks);
+ opticalDisc = new CompactDisc(options);
break;
default:
opticalDisc = null;
@@ -118,4 +114,4 @@ namespace RedBookPlayer.Models.Factories
};
}
}
-}
+}
\ No newline at end of file
diff --git a/RedBookPlayer.Models/Hardware/Player.cs b/RedBookPlayer.Models/Hardware/Player.cs
index 7db9f0c..4f172ab 100644
--- a/RedBookPlayer.Models/Hardware/Player.cs
+++ b/RedBookPlayer.Models/Hardware/Player.cs
@@ -1,4 +1,3 @@
-using System;
using System.ComponentModel;
using Aaru.CommonTypes.Enums;
using ReactiveUI;
@@ -12,7 +11,13 @@ namespace RedBookPlayer.Models.Hardware
///
/// Indicate if the player is ready to be used
///
- 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
@@ -100,27 +105,27 @@ namespace RedBookPlayer.Models.Hardware
///
/// Represents the total tracks on the disc
///
- public int TotalTracks => _opticalDisc.TotalTracks;
+ public int TotalTracks => _opticalDisc?.TotalTracks ?? 0;
///
/// Represents the total indices on the disc
///
- public int TotalIndexes => _opticalDisc.TotalIndexes;
+ public int TotalIndexes => _opticalDisc?.TotalIndexes ?? 0;
///
/// Total sectors in the image
///
- public ulong TotalSectors => _opticalDisc.TotalSectors;
+ public ulong TotalSectors => _opticalDisc?.TotalSectors ?? 0;
///
/// Represents the time adjustment offset for the disc
///
- public ulong TimeOffset => _opticalDisc.TimeOffset;
+ public ulong TimeOffset => _opticalDisc?.TimeOffset ?? 0;
///
/// Represents the total playing time for the disc
///
- public ulong TotalTime => _opticalDisc.TotalTime;
+ public ulong TotalTime => _opticalDisc?.TotalTime ?? 0;
private int _currentTrackNumber;
private ushort _currentTrackIndex;
@@ -138,12 +143,21 @@ namespace RedBookPlayer.Models.Hardware
#region SoundOutput Passthrough
///
- /// Indicate if the output is playing
+ /// Indicates the current player state
///
- public bool? Playing
+ public PlayerState PlayerState
{
- get => _playing;
- private set => this.RaiseAndSetIfChanged(ref _playing, value);
+ get => _playerState;
+ private set => this.RaiseAndSetIfChanged(ref _playerState, value);
+ }
+
+ ///
+ /// Indicates how to handle playback of data tracks
+ ///
+ public DataPlayback DataPlayback
+ {
+ get => _dataPlayback;
+ private set => this.RaiseAndSetIfChanged(ref _dataPlayback, value);
}
///
@@ -164,7 +178,8 @@ namespace RedBookPlayer.Models.Hardware
private set => this.RaiseAndSetIfChanged(ref _volume, value);
}
- private bool? _playing;
+ private PlayerState _playerState;
+ private DataPlayback _dataPlayback;
private bool _applyDeEmphasis;
private int _volume;
@@ -180,7 +195,7 @@ namespace RedBookPlayer.Models.Hardware
///
/// OpticalDisc object
///
- private readonly OpticalDiscBase _opticalDisc;
+ private OpticalDiscBase _opticalDisc;
///
/// Last volume for mute toggling
@@ -190,23 +205,29 @@ namespace RedBookPlayer.Models.Hardware
#endregion
///
- /// Create a new Player from a given image path
+ /// Constructor
+ ///
+ /// Default volume between 0 and 100 to use when starting playback
+ public Player(int defaultVolume)
+ {
+ Initialized = false;
+ _soundOutput = new SoundOutput(defaultVolume);
+ _soundOutput.SetDeEmphasis(false);
+ }
+
+ ///
+ /// Initializes player from a given image path
///
/// Path to the disc image
- /// Generate a TOC if the disc is missing one [CompactDisc only]
- /// Load hidden tracks for playback [CompactDisc only]
- /// Load data tracks for playback [CompactDisc only]
+ /// Options to pass to the optical disc factory
/// True if playback should begin immediately, false otherwise
- /// Default volume between 0 and 100 to use when starting playback
- public Player(string path, bool generateMissingToc, bool loadHiddenTracks, bool loadDataTracks, bool autoPlay, int defaultVolume)
+ public void Init(string path, OpticalDiscOptions options, bool autoPlay)
{
- // Set the internal state for initialization
+ // Reset initialization
Initialized = false;
- _soundOutput = new SoundOutput();
- _soundOutput.SetDeEmphasis(false);
// Initalize the disc
- _opticalDisc = OpticalDiscFactory.GenerateFromPath(path, generateMissingToc, loadHiddenTracks, loadDataTracks, autoPlay);
+ _opticalDisc = OpticalDiscFactory.GenerateFromPath(path, options, autoPlay);
if(_opticalDisc == null || !_opticalDisc.Initialized)
return;
@@ -214,7 +235,7 @@ namespace RedBookPlayer.Models.Hardware
_opticalDisc.PropertyChanged += OpticalDiscStateChanged;
// Initialize the sound output
- _soundOutput.Init(_opticalDisc, autoPlay, defaultVolume);
+ _soundOutput.Init(_opticalDisc, autoPlay);
if(_soundOutput == null || !_soundOutput.Initialized)
return;
@@ -240,12 +261,12 @@ namespace RedBookPlayer.Models.Hardware
return;
else if(_soundOutput == null)
return;
- else if(_soundOutput.Playing)
+ else if(_soundOutput.PlayerState != PlayerState.Paused && _soundOutput.PlayerState != PlayerState.Stopped)
return;
_soundOutput.Play();
_opticalDisc.SetTotalIndexes();
- Playing = true;
+ PlayerState = PlayerState.Playing;
}
///
@@ -257,11 +278,11 @@ namespace RedBookPlayer.Models.Hardware
return;
else if(_soundOutput == null)
return;
- else if(!_soundOutput.Playing)
+ else if(_soundOutput.PlayerState != PlayerState.Playing)
return;
- _soundOutput?.Stop();
- Playing = false;
+ _soundOutput?.Pause();
+ PlayerState = PlayerState.Paused;
}
///
@@ -269,10 +290,20 @@ namespace RedBookPlayer.Models.Hardware
///
public void TogglePlayback()
{
- if(Playing == true)
- Pause();
- else
- Play();
+ switch(PlayerState)
+ {
+ case PlayerState.NoDisc:
+ break;
+ case PlayerState.Stopped:
+ Play();
+ break;
+ case PlayerState.Paused:
+ Play();
+ break;
+ case PlayerState.Playing:
+ Pause();
+ break;
+ }
}
///
@@ -284,12 +315,12 @@ namespace RedBookPlayer.Models.Hardware
return;
else if(_soundOutput == null)
return;
- else if(!_soundOutput.Playing)
+ else if(_soundOutput.PlayerState != PlayerState.Playing && _soundOutput.PlayerState != PlayerState.Paused)
return;
- _soundOutput?.Stop();
+ _soundOutput.Stop();
_opticalDisc.LoadFirstTrack();
- Playing = null;
+ PlayerState = PlayerState.Stopped;
}
///
@@ -300,14 +331,16 @@ namespace RedBookPlayer.Models.Hardware
if(_opticalDisc == null || !_opticalDisc.Initialized)
return;
- bool? wasPlaying = Playing;
- if(wasPlaying == true) Pause();
+ PlayerState wasPlaying = PlayerState;
+ if(wasPlaying == PlayerState.Playing)
+ Pause();
_opticalDisc.NextTrack();
if(_opticalDisc is CompactDisc compactDisc)
_soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis);
- if(wasPlaying == true) Play();
+ if(wasPlaying == PlayerState.Playing)
+ Play();
}
///
@@ -318,14 +351,16 @@ namespace RedBookPlayer.Models.Hardware
if(_opticalDisc == null || !_opticalDisc.Initialized)
return;
- bool? wasPlaying = Playing;
- if(wasPlaying == true) Pause();
+ PlayerState wasPlaying = PlayerState;
+ if(wasPlaying == PlayerState.Playing)
+ Pause();
_opticalDisc.PreviousTrack();
if(_opticalDisc is CompactDisc compactDisc)
_soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis);
- if(wasPlaying == true) Play();
+ if(wasPlaying == PlayerState.Playing)
+ Play();
}
///
@@ -337,14 +372,16 @@ namespace RedBookPlayer.Models.Hardware
if(_opticalDisc == null || !_opticalDisc.Initialized)
return;
- bool? wasPlaying = Playing;
- if(wasPlaying == true) Pause();
+ PlayerState wasPlaying = PlayerState;
+ if(wasPlaying == PlayerState.Playing)
+ Pause();
_opticalDisc.NextIndex(changeTrack);
if(_opticalDisc is CompactDisc compactDisc)
_soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis);
- if(wasPlaying == true) Play();
+ if(wasPlaying == PlayerState.Playing)
+ Play();
}
///
@@ -356,37 +393,38 @@ namespace RedBookPlayer.Models.Hardware
if(_opticalDisc == null || !_opticalDisc.Initialized)
return;
- bool? wasPlaying = Playing;
- if(wasPlaying == true) Pause();
+ PlayerState wasPlaying = PlayerState;
+ if(wasPlaying == PlayerState.Playing)
+ Pause();
_opticalDisc.PreviousIndex(changeTrack);
if(_opticalDisc is CompactDisc compactDisc)
_soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis);
- if(wasPlaying == true) Play();
+ if(wasPlaying == PlayerState.Playing)
+ Play();
}
///
- /// Fast-forward playback by 75 sectors, if possible
+ /// Fast-forward playback by 75 sectors
///
public void FastForward()
{
if(_opticalDisc == null || !_opticalDisc.Initialized)
return;
- _opticalDisc.SetCurrentSector(Math.Min(_opticalDisc.TotalSectors, _opticalDisc.CurrentSector + 75));
+ _opticalDisc.SetCurrentSector(_opticalDisc.CurrentSector + 75);
}
///
- /// Rewind playback by 75 sectors, if possible
+ /// Rewind playback by 75 sectors
///
public void Rewind()
{
if(_opticalDisc == null || !_opticalDisc.Initialized)
return;
- if(_opticalDisc.CurrentSector >= 75)
- _opticalDisc.SetCurrentSector(_opticalDisc.CurrentSector - 75);
+ _opticalDisc.SetCurrentSector(_opticalDisc.CurrentSector - 75);
}
#endregion
@@ -456,13 +494,13 @@ namespace RedBookPlayer.Models.Hardware
#region Helpers
///
- /// Set the value for loading data tracks [CompactDisc only]
+ /// Set data playback method [CompactDisc only]
///
- /// True to enable loading data tracks, false otherwise
- public void SetLoadDataTracks(bool load)
+ /// New playback value
+ public void SetDataPlayback(DataPlayback dataPlayback)
{
if(_opticalDisc is CompactDisc compactDisc)
- compactDisc.LoadDataTracks = load;
+ compactDisc.DataPlayback = dataPlayback;
}
///
@@ -508,7 +546,7 @@ namespace RedBookPlayer.Models.Hardware
///
private void SoundOutputStateChanged(object sender, PropertyChangedEventArgs e)
{
- Playing = _soundOutput.Playing;
+ PlayerState = _soundOutput.PlayerState;
ApplyDeEmphasis = _soundOutput.ApplyDeEmphasis;
Volume = _soundOutput.Volume;
}
diff --git a/RedBookPlayer.Models/Hardware/SoundOutput.cs b/RedBookPlayer.Models/Hardware/SoundOutput.cs
index 743c35b..663e7c9 100644
--- a/RedBookPlayer.Models/Hardware/SoundOutput.cs
+++ b/RedBookPlayer.Models/Hardware/SoundOutput.cs
@@ -16,15 +16,19 @@ namespace RedBookPlayer.Models.Hardware
///
/// Indicate if the output is ready to be used
///
- public bool Initialized { get; private set; } = false;
+ public bool Initialized
+ {
+ get => _initialized;
+ private set => this.RaiseAndSetIfChanged(ref _initialized, value);
+ }
///
- /// Indicate if the output is playing
+ /// Indicates the current player state
///
- public bool Playing
+ public PlayerState PlayerState
{
- get => _playing;
- private set => this.RaiseAndSetIfChanged(ref _playing, value);
+ get => _playerState;
+ private set => this.RaiseAndSetIfChanged(ref _playerState, value);
}
///
@@ -54,7 +58,8 @@ namespace RedBookPlayer.Models.Hardware
}
}
- private bool _playing;
+ private bool _initialized;
+ private PlayerState _playerState;
private bool _applyDeEmphasis;
private int _volume;
@@ -102,13 +107,18 @@ namespace RedBookPlayer.Models.Hardware
#endregion
+ ///
+ /// Constructor
+ ///
+ /// Default volume between 0 and 100 to use when starting playback
+ public SoundOutput(int defaultVolume = 100) => Volume = defaultVolume;
+
///
/// Initialize the output with a given image
///
/// OpticalDisc to load from
/// True if playback should begin immediately, false otherwise
- /// Default volume between 0 and 100 to use when starting playback
- public void Init(OpticalDiscBase opticalDisc, bool autoPlay = false, int defaultVolume = 100)
+ public void Init(OpticalDiscBase opticalDisc, bool autoPlay)
{
// If we have an unusable disc, just return
if(opticalDisc == null || !opticalDisc.Initialized)
@@ -117,9 +127,6 @@ namespace RedBookPlayer.Models.Hardware
// Save a reference to the disc
_opticalDisc = opticalDisc;
- // Set the initial playback volume
- Volume = defaultVolume;
-
// Enable de-emphasis for CDs, if necessary
if(opticalDisc is CompactDisc compactDisc)
ApplyDeEmphasis = compactDisc.TrackHasEmphasis;
@@ -136,6 +143,7 @@ namespace RedBookPlayer.Models.Hardware
// Mark the output as ready
Initialized = true;
+ PlayerState = PlayerState.Stopped;
// Begin loading data
_source.Start();
@@ -163,57 +171,14 @@ namespace RedBookPlayer.Models.Hardware
// Determine how many sectors we can read
DetermineReadAmount(count, out ulong sectorsToRead, out 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(ArgumentOutOfRangeException)
- {
- _opticalDisc.LoadFirstTrack();
- }
- }
-
- return zeroSectors;
- }
- });
-
- // Wait 100ms at longest for the read to occur
- if(readSectorTask.Wait(TimeSpan.FromMilliseconds(100)))
- {
- audioData = readSectorTask.Result;
- }
- else
+ // Get data to return
+ byte[] audioDataSegment = ReadData(count, sectorsToRead, zeroSectorsAmount);
+ if(audioDataSegment == null)
{
Array.Clear(buffer, offset, 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
Array.Copy(audioDataSegment, 0, buffer, offset, count);
@@ -221,6 +186,7 @@ namespace RedBookPlayer.Models.Hardware
_currentSectorReadPosition += count;
if(_currentSectorReadPosition >= _opticalDisc.BytesPerSector)
{
+ int currentTrack = _opticalDisc.CurrentTrackNumber;
_opticalDisc.SetCurrentSector(_opticalDisc.CurrentSector + (ulong)(_currentSectorReadPosition / _opticalDisc.BytesPerSector));
_currentSectorReadPosition %= _opticalDisc.BytesPerSector;
}
@@ -235,10 +201,10 @@ namespace RedBookPlayer.Models.Hardware
///
public void Play()
{
- if (_soundOut.PlaybackState != PlaybackState.Playing)
+ if(_soundOut.PlaybackState != PlaybackState.Playing)
_soundOut.Play();
- Playing = _soundOut.PlaybackState == PlaybackState.Playing;
+ PlayerState = PlayerState.Playing;
}
///
@@ -249,7 +215,7 @@ namespace RedBookPlayer.Models.Hardware
if(_soundOut.PlaybackState != PlaybackState.Paused)
_soundOut.Pause();
- Playing = _soundOut.PlaybackState == PlaybackState.Playing;
+ PlayerState = PlayerState.Paused;
}
///
@@ -260,7 +226,7 @@ namespace RedBookPlayer.Models.Hardware
if(_soundOut.PlaybackState != PlaybackState.Stopped)
_soundOut.Stop();
- Playing = _soundOut.PlaybackState == PlaybackState.Playing;
+ PlayerState = PlayerState.Stopped;
}
#endregion
@@ -270,7 +236,7 @@ namespace RedBookPlayer.Models.Hardware
///
/// Set de-emphasis status
///
- ///
+ /// New de-emphasis status
public void SetDeEmphasis(bool apply) => ApplyDeEmphasis = apply;
///
@@ -287,30 +253,19 @@ namespace RedBookPlayer.Models.Hardware
/// Number of zeroed sectors to concatenate
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
- sectorsToRead = ((ulong)count / (ulong)_opticalDisc.BytesPerSector) + 2;
- zeroSectorsAmount = 0;
+ ulong oldSectorsToRead = sectorsToRead;
+ sectorsToRead = _opticalDisc.TotalSectors - _opticalDisc.CurrentSector;
- // Avoid overreads by padding with 0-byte data at the end
- if(_opticalDisc.CurrentSector + sectorsToRead > _opticalDisc.TotalSectors)
- {
- ulong oldSectorsToRead = sectorsToRead;
- sectorsToRead = _opticalDisc.TotalSectors - _opticalDisc.CurrentSector;
-
- int tempZeroSectorCount = (int)(oldSectorsToRead - sectorsToRead);
- zeroSectorsAmount = (ulong)(tempZeroSectorCount < 0 ? 0 : tempZeroSectorCount);
- }
-
- // 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);
+ int tempZeroSectorCount = (int)(oldSectorsToRead - sectorsToRead);
+ zeroSectorsAmount = (ulong)(tempZeroSectorCount < 0 ? 0 : tempZeroSectorCount);
+ }
}
///
@@ -333,6 +288,58 @@ namespace RedBookPlayer.Models.Hardware
ByteConverter.FromFloats16Bit(floatAudioData, audioData);
}
+ ///
+ /// Read the requested amount of data from an input
+ ///
+ /// Number of bytes to load
+ /// Number of sectors to read
+ /// Number of zeroed sectors to concatenate
+ /// The requested amount of data, if possible
+ private byte[] ReadData(int count, ulong sectorsToRead, ulong zeroSectorsAmount)
+ {
+ // Create padding data for overreads
+ byte[] zeroSectors = new byte[(int)zeroSectorsAmount * _opticalDisc.BytesPerSector];
+ byte[] audioData;
+
+ // Attempt to read the required number of sectors
+ var readSectorTask = Task.Run(() =>
+ {
+ lock(_readingImage)
+ {
+ for(int i = 0; i < 4; i++)
+ {
+ try
+ {
+ return _opticalDisc.ReadSectors((uint)sectorsToRead).Concat(zeroSectors).ToArray();
+ }
+ catch { }
+ }
+
+ return zeroSectors;
+ }
+ });
+
+ // Wait 100ms at longest for the read to occur
+ if(readSectorTask.Wait(TimeSpan.FromMilliseconds(100)))
+ audioData = readSectorTask.Result;
+ else
+ return null;
+
+ // Load only the requested audio segment
+ byte[] audioDataSegment = new byte[count];
+ int copyAmount = Math.Min(count, audioData.Length - _currentSectorReadPosition);
+ if(Math.Max(0, copyAmount) == 0)
+ return null;
+
+ Array.Copy(audioData, _currentSectorReadPosition, audioDataSegment, 0, copyAmount);
+
+ // Apply de-emphasis filtering, only if enabled
+ if(ApplyDeEmphasis)
+ ProcessDeEmphasis(audioDataSegment);
+
+ return audioDataSegment;
+ }
+
///
/// Sets or resets the de-emphasis filters
///
@@ -369,4 +376,4 @@ namespace RedBookPlayer.Models.Hardware
#endregion
}
-}
+}
\ No newline at end of file