diff --git a/.gitmodules b/.gitmodules
index 38b3a5b..59d2a97 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -3,4 +3,5 @@
url = https://github.com/aaru-dps/Aaru.git
[submodule "cscore"]
path = cscore
- url = https://github.com/filoe/cscore.git
+ url = https://github.com/sk-zk/cscore.git
+ branch = netstandard
diff --git a/RedBookPlayer/App.xaml b/RedBookPlayer.GUI/App.xaml
similarity index 100%
rename from RedBookPlayer/App.xaml
rename to RedBookPlayer.GUI/App.xaml
diff --git a/RedBookPlayer/App.xaml.cs b/RedBookPlayer.GUI/App.xaml.cs
similarity index 97%
rename from RedBookPlayer/App.xaml.cs
rename to RedBookPlayer.GUI/App.xaml.cs
index 03c77fd..20e6a9d 100644
--- a/RedBookPlayer/App.xaml.cs
+++ b/RedBookPlayer.GUI/App.xaml.cs
@@ -6,6 +6,7 @@ using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using RedBookPlayer.GUI;
+using RedBookPlayer.GUI.Views;
namespace RedBookPlayer
{
diff --git a/RedBookPlayer/Assets/-.png b/RedBookPlayer.GUI/Assets/-.png
similarity index 100%
rename from RedBookPlayer/Assets/-.png
rename to RedBookPlayer.GUI/Assets/-.png
diff --git a/RedBookPlayer/Assets/0.png b/RedBookPlayer.GUI/Assets/0.png
similarity index 100%
rename from RedBookPlayer/Assets/0.png
rename to RedBookPlayer.GUI/Assets/0.png
diff --git a/RedBookPlayer/Assets/1.png b/RedBookPlayer.GUI/Assets/1.png
similarity index 100%
rename from RedBookPlayer/Assets/1.png
rename to RedBookPlayer.GUI/Assets/1.png
diff --git a/RedBookPlayer/Assets/2.png b/RedBookPlayer.GUI/Assets/2.png
similarity index 100%
rename from RedBookPlayer/Assets/2.png
rename to RedBookPlayer.GUI/Assets/2.png
diff --git a/RedBookPlayer/Assets/3.png b/RedBookPlayer.GUI/Assets/3.png
similarity index 100%
rename from RedBookPlayer/Assets/3.png
rename to RedBookPlayer.GUI/Assets/3.png
diff --git a/RedBookPlayer/Assets/4.png b/RedBookPlayer.GUI/Assets/4.png
similarity index 100%
rename from RedBookPlayer/Assets/4.png
rename to RedBookPlayer.GUI/Assets/4.png
diff --git a/RedBookPlayer/Assets/5.png b/RedBookPlayer.GUI/Assets/5.png
similarity index 100%
rename from RedBookPlayer/Assets/5.png
rename to RedBookPlayer.GUI/Assets/5.png
diff --git a/RedBookPlayer/Assets/6.png b/RedBookPlayer.GUI/Assets/6.png
similarity index 100%
rename from RedBookPlayer/Assets/6.png
rename to RedBookPlayer.GUI/Assets/6.png
diff --git a/RedBookPlayer/Assets/7.png b/RedBookPlayer.GUI/Assets/7.png
similarity index 100%
rename from RedBookPlayer/Assets/7.png
rename to RedBookPlayer.GUI/Assets/7.png
diff --git a/RedBookPlayer/Assets/8.png b/RedBookPlayer.GUI/Assets/8.png
similarity index 100%
rename from RedBookPlayer/Assets/8.png
rename to RedBookPlayer.GUI/Assets/8.png
diff --git a/RedBookPlayer/Assets/9.png b/RedBookPlayer.GUI/Assets/9.png
similarity index 100%
rename from RedBookPlayer/Assets/9.png
rename to RedBookPlayer.GUI/Assets/9.png
diff --git a/RedBookPlayer/Assets/colon.png b/RedBookPlayer.GUI/Assets/colon.png
similarity index 100%
rename from RedBookPlayer/Assets/colon.png
rename to RedBookPlayer.GUI/Assets/colon.png
diff --git a/RedBookPlayer/Program.cs b/RedBookPlayer.GUI/Program.cs
similarity index 95%
rename from RedBookPlayer/Program.cs
rename to RedBookPlayer.GUI/Program.cs
index 731908b..b1919b6 100644
--- a/RedBookPlayer/Program.cs
+++ b/RedBookPlayer.GUI/Program.cs
@@ -5,7 +5,7 @@ using System.Runtime.InteropServices;
using Avalonia;
using Avalonia.Logging.Serilog;
-namespace RedBookPlayer
+namespace RedBookPlayer.GUI
{
internal class Program
{
diff --git a/RedBookPlayer/RedBookPlayer.csproj b/RedBookPlayer.GUI/RedBookPlayer.GUI.csproj
similarity index 58%
rename from RedBookPlayer/RedBookPlayer.csproj
rename to RedBookPlayer.GUI/RedBookPlayer.GUI.csproj
index bc7ee1e..36d6bb4 100644
--- a/RedBookPlayer/RedBookPlayer.csproj
+++ b/RedBookPlayer.GUI/RedBookPlayer.GUI.csproj
@@ -2,8 +2,8 @@
WinExe
netcoreapp3.1
- true
win-x64;linux-x64
+ linux-x64
embedded
@@ -21,17 +21,9 @@
-
-
-
-
-
-
-
-
-
+
diff --git a/RedBookPlayer/Settings.cs b/RedBookPlayer.GUI/Settings.cs
similarity index 93%
rename from RedBookPlayer/Settings.cs
rename to RedBookPlayer.GUI/Settings.cs
index 36e95fb..df14848 100644
--- a/RedBookPlayer/Settings.cs
+++ b/RedBookPlayer.GUI/Settings.cs
@@ -2,9 +2,9 @@ using System;
using System.IO;
using System.Text.Json;
using Avalonia.Input;
-using RedBookPlayer.GUI;
+using RedBookPlayer.GUI.Views;
-namespace RedBookPlayer
+namespace RedBookPlayer.GUI
{
public class Settings
{
@@ -21,9 +21,14 @@ namespace RedBookPlayer
public bool IndexButtonChangeTrack { get; set; } = false;
///
- /// Indicates if the index 0 of track 1 is treated like a hidden track
+ /// Indicates if hidden tracks should be played
///
- public bool AllowSkipHiddenTrack { get; set; } = false;
+ ///
+ /// Hidden tracks can be one of the following:
+ /// - TrackSequence == 0
+ /// - Larget pregap of track 1 (> 150 sectors)
+ ///
+ public bool PlayHiddenTracks { get; set; } = false;
///
/// Indicates if data tracks should be played like old, non-compliant players
diff --git a/RedBookPlayer.GUI/ViewModels/PlayerViewModel.cs b/RedBookPlayer.GUI/ViewModels/PlayerViewModel.cs
new file mode 100644
index 0000000..41edc50
--- /dev/null
+++ b/RedBookPlayer.GUI/ViewModels/PlayerViewModel.cs
@@ -0,0 +1,672 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.IO;
+using System.Linq;
+using System.Reactive;
+using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media.Imaging;
+using Avalonia.Platform;
+using Avalonia.Threading;
+using ReactiveUI;
+using RedBookPlayer.GUI.Views;
+using RedBookPlayer.Models.Hardware;
+
+namespace RedBookPlayer.GUI.ViewModels
+{
+ public class PlayerViewModel : ReactiveObject
+ {
+ ///
+ /// Player representing the internal state
+ ///
+ private Player _player;
+
+ ///
+ /// Set of images representing the digits for the UI
+ ///
+ private Image[] _digits;
+
+ #region Player Passthrough
+
+ #region OpticalDisc Passthrough
+
+ ///
+ /// Current track number
+ ///
+ public int CurrentTrackNumber
+ {
+ get => _currentTrackNumber;
+ private set => this.RaiseAndSetIfChanged(ref _currentTrackNumber, value);
+ }
+
+ ///
+ /// Current track index
+ ///
+ public ushort CurrentTrackIndex
+ {
+ get => _currentTrackIndex;
+ private set => this.RaiseAndSetIfChanged(ref _currentTrackIndex, value);
+ }
+
+ ///
+ /// Current sector number
+ ///
+ public ulong CurrentSector
+ {
+ get => _currentSector;
+ private set => this.RaiseAndSetIfChanged(ref _currentSector, value);
+ }
+
+ ///
+ /// Represents the sector starting the section
+ ///
+ public ulong SectionStartSector
+ {
+ get => _sectionStartSector;
+ private set => this.RaiseAndSetIfChanged(ref _sectionStartSector, value);
+ }
+
+ ///
+ /// Represents if the disc has a hidden track
+ ///
+ public bool HiddenTrack
+ {
+ get => _hasHiddenTrack;
+ private set => this.RaiseAndSetIfChanged(ref _hasHiddenTrack, value);
+ }
+
+ ///
+ /// Represents the 4CH flag [CompactDisc only]
+ ///
+ public bool QuadChannel
+ {
+ get => _quadChannel;
+ private set => this.RaiseAndSetIfChanged(ref _quadChannel, value);
+ }
+
+ ///
+ /// Represents the DATA flag [CompactDisc only]
+ ///
+ public bool IsDataTrack
+ {
+ get => _isDataTrack;
+ private set => this.RaiseAndSetIfChanged(ref _isDataTrack, value);
+ }
+
+ ///
+ /// Represents the DCP flag [CompactDisc only]
+ ///
+ public bool CopyAllowed
+ {
+ get => _copyAllowed;
+ private set => this.RaiseAndSetIfChanged(ref _copyAllowed, value);
+ }
+
+ ///
+ /// Represents the PRE flag [CompactDisc only]
+ ///
+ public bool TrackHasEmphasis
+ {
+ get => _trackHasEmphasis;
+ private set => this.RaiseAndSetIfChanged(ref _trackHasEmphasis, value);
+ }
+
+ ///
+ /// Represents the total tracks on the disc
+ ///
+ public int TotalTracks => _player.TotalTracks;
+
+ ///
+ /// Represents the total indices on the disc
+ ///
+ public int TotalIndexes => _player.TotalIndexes;
+
+ ///
+ /// Total sectors in the image
+ ///
+ public ulong TotalSectors => _player.TotalSectors;
+
+ ///
+ /// Represents the time adjustment offset for the disc
+ ///
+ public ulong TimeOffset => _player.TimeOffset;
+
+ ///
+ /// Represents the total playing time for the disc
+ ///
+ public ulong TotalTime => _player.TotalTime;
+
+ private int _currentTrackNumber;
+ private ushort _currentTrackIndex;
+ private ulong _currentSector;
+ private ulong _sectionStartSector;
+
+ private bool _hasHiddenTrack;
+ private bool _quadChannel;
+ private bool _isDataTrack;
+ private bool _copyAllowed;
+ private bool _trackHasEmphasis;
+
+ #endregion
+
+ #region SoundOutput Passthrough
+
+ ///
+ /// Indicate if the model is ready to be used
+ ///
+ public bool Initialized => _player?.Initialized ?? false;
+
+ ///
+ /// Indicate if the output is playing
+ ///
+ public bool? Playing
+ {
+ get => _playing;
+ private set => this.RaiseAndSetIfChanged(ref _playing, value);
+ }
+
+ ///
+ /// Indicates if de-emphasis should be applied
+ ///
+ public bool ApplyDeEmphasis
+ {
+ get => _applyDeEmphasis;
+ private set => this.RaiseAndSetIfChanged(ref _applyDeEmphasis, value);
+ }
+
+ ///
+ /// Current playback volume
+ ///
+ public int Volume
+ {
+ get => _volume;
+ private set => this.RaiseAndSetIfChanged(ref _volume, value);
+ }
+
+ private bool? _playing;
+ private bool _applyDeEmphasis;
+ private int _volume;
+
+ #endregion
+
+ #endregion
+
+ #region Commands
+
+ ///
+ /// Command for loading a disc
+ ///
+ public ReactiveCommand LoadCommand { get; }
+
+ #region Playback
+
+ ///
+ /// Command for beginning playback
+ ///
+ public ReactiveCommand PlayCommand { get; }
+
+ ///
+ /// Command for pausing playback
+ ///
+ public ReactiveCommand PauseCommand { get; }
+
+ ///
+ /// Command for pausing playback
+ ///
+ public ReactiveCommand TogglePlayPauseCommand { get; }
+
+ ///
+ /// Command for stopping playback
+ ///
+ public ReactiveCommand StopCommand { get; }
+
+ ///
+ /// Command for moving to the next track
+ ///
+ public ReactiveCommand NextTrackCommand { get; }
+
+ ///
+ /// Command for moving to the previous track
+ ///
+ public ReactiveCommand PreviousTrackCommand { get; }
+
+ ///
+ /// Command for moving to the next index
+ ///
+ public ReactiveCommand NextIndexCommand { get; }
+
+ ///
+ /// Command for moving to the previous index
+ ///
+ public ReactiveCommand PreviousIndexCommand { get; }
+
+ ///
+ /// Command for fast forwarding
+ ///
+ public ReactiveCommand FastForwardCommand { get; }
+
+ ///
+ /// Command for rewinding
+ ///
+ public ReactiveCommand RewindCommand { get; }
+
+ #endregion
+
+ #region Volume
+
+ ///
+ /// Command for incrementing volume
+ ///
+ public ReactiveCommand VolumeUpCommand { get; }
+
+ ///
+ /// Command for decrementing volume
+ ///
+ public ReactiveCommand VolumeDownCommand { get; }
+
+ ///
+ /// Command for toggling mute
+ ///
+ public ReactiveCommand ToggleMuteCommand { get; }
+
+ #endregion
+
+ #region Emphasis
+
+ ///
+ /// Command for enabling de-emphasis
+ ///
+ public ReactiveCommand EnableDeEmphasisCommand { get; }
+
+ ///
+ /// Command for disabling de-emphasis
+ ///
+ public ReactiveCommand DisableDeEmphasisCommand { get; }
+
+ ///
+ /// Command for toggling de-emphasis
+ ///
+ public ReactiveCommand ToggleDeEmphasisCommand { get; }
+
+ #endregion
+
+ #endregion
+
+ ///
+ /// Constructor
+ ///
+ public PlayerViewModel()
+ {
+ LoadCommand = ReactiveCommand.Create(ExecuteLoad);
+
+ PlayCommand = ReactiveCommand.Create(ExecutePlay);
+ PauseCommand = ReactiveCommand.Create(ExecutePause);
+ TogglePlayPauseCommand = ReactiveCommand.Create(ExecuteTogglePlayPause);
+ StopCommand = ReactiveCommand.Create(ExecuteStop);
+ NextTrackCommand = ReactiveCommand.Create(ExecuteNextTrack);
+ PreviousTrackCommand = ReactiveCommand.Create(ExecutePreviousTrack);
+ NextIndexCommand = ReactiveCommand.Create(ExecuteNextIndex);
+ PreviousIndexCommand = ReactiveCommand.Create(ExecutePreviousIndex);
+ FastForwardCommand = ReactiveCommand.Create(ExecuteFastForward);
+ RewindCommand = ReactiveCommand.Create(ExecuteRewind);
+
+ VolumeUpCommand = ReactiveCommand.Create(ExecuteVolumeUp);
+ VolumeDownCommand = ReactiveCommand.Create(ExecuteVolumeDown);
+ ToggleMuteCommand = ReactiveCommand.Create(ExecuteToggleMute);
+
+ EnableDeEmphasisCommand = ReactiveCommand.Create(ExecuteEnableDeEmphasis);
+ DisableDeEmphasisCommand = ReactiveCommand.Create(ExecuteDisableDeEmphasis);
+ ToggleDeEmphasisCommand = ReactiveCommand.Create(ExecuteToggleDeEmphasis);
+ }
+
+ ///
+ /// 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]
+ /// 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)
+ {
+ // Stop current playback, if necessary
+ if(Playing != null) ExecuteStop();
+
+ // Create and attempt to initialize new Player
+ _player = new Player(path, generateMissingToc, loadHiddenTracks, loadDataTracks, autoPlay, defaultVolume);
+ if(Initialized)
+ {
+ _player.PropertyChanged += PlayerStateChanged;
+ PlayerStateChanged(this, null);
+ }
+ }
+
+ #region Playback
+
+ ///
+ /// Begin playback
+ ///
+ public void ExecutePlay() => _player?.Play();
+
+ ///
+ /// Pause current playback
+ ///
+ public void ExecutePause() => _player?.Pause();
+
+ ///
+ /// Toggle playback
+ ///
+ public void ExecuteTogglePlayPause() => _player?.TogglePlayback();
+
+ ///
+ /// Stop current playback
+ ///
+ public void ExecuteStop() => _player?.Stop();
+
+ ///
+ /// Move to the next playable track
+ ///
+ public void ExecuteNextTrack() => _player?.NextTrack();
+
+ ///
+ /// Move to the previous playable track
+ ///
+ public void ExecutePreviousTrack() => _player?.PreviousTrack();
+
+ ///
+ /// Move to the next index
+ ///
+ public void ExecuteNextIndex() => _player?.NextIndex(App.Settings.IndexButtonChangeTrack);
+
+ ///
+ /// Move to the previous index
+ ///
+ public void ExecutePreviousIndex() => _player?.PreviousIndex(App.Settings.IndexButtonChangeTrack);
+
+ ///
+ /// Fast-forward playback by 75 sectors, if possible
+ ///
+ public void ExecuteFastForward() => _player?.FastForward();
+
+ ///
+ /// Rewind playback by 75 sectors, if possible
+ ///
+ public void ExecuteRewind() => _player?.Rewind();
+
+ #endregion
+
+ #region Volume
+
+ ///
+ /// Increment the volume value
+ ///
+ public void ExecuteVolumeUp() => _player?.VolumeUp();
+
+ ///
+ /// Decrement the volume value
+ ///
+ public void ExecuteVolumeDown() => _player?.VolumeDown();
+
+ ///
+ /// Set the value for the volume
+ ///
+ /// New volume value
+ public void ExecuteSetVolume(int volume) => _player?.SetVolume(volume);
+
+ ///
+ /// Temporarily mute playback
+ ///
+ public void ExecuteToggleMute() => _player?.ToggleMute();
+
+ #endregion
+
+ #region Emphasis
+
+ ///
+ /// Enable de-emphasis
+ ///
+ public void ExecuteEnableDeEmphasis() => _player?.EnableDeEmphasis();
+
+ ///
+ /// Disable de-emphasis
+ ///
+ public void ExecuteDisableDeEmphasis() => _player?.DisableDeEmphasis();
+
+ ///
+ /// Toggle de-emphasis
+ ///
+ public void ExecuteToggleDeEmphasis() => _player?.ToggleDeEmphasis();
+
+ #endregion
+
+ #region Helpers
+
+ ///
+ /// Load a disc image from a selection box
+ ///
+ public async void ExecuteLoad()
+ {
+ string path = await GetPath();
+ if(path == null)
+ return;
+
+ await LoadImage(path);
+ }
+
+ ///
+ /// Initialize the displayed digits array
+ ///
+ public void InitializeDigits()
+ {
+ PlayerView playerView = MainWindow.Instance.ContentControl.Content as PlayerView;
+
+ _digits = new Image[]
+ {
+ playerView.FindControl("TrackDigit1"),
+ playerView.FindControl("TrackDigit2"),
+
+ playerView.FindControl("IndexDigit1"),
+ playerView.FindControl("IndexDigit2"),
+
+ playerView.FindControl("TimeDigit1"),
+ playerView.FindControl("TimeDigit2"),
+ playerView.FindControl("TimeDigit3"),
+ playerView.FindControl("TimeDigit4"),
+ playerView.FindControl("TimeDigit5"),
+ playerView.FindControl("TimeDigit6"),
+
+ playerView.FindControl("TotalTracksDigit1"),
+ playerView.FindControl("TotalTracksDigit2"),
+
+ playerView.FindControl("TotalIndexesDigit1"),
+ playerView.FindControl("TotalIndexesDigit2"),
+
+ playerView.FindControl("TotalTimeDigit1"),
+ playerView.FindControl("TotalTimeDigit2"),
+ playerView.FindControl("TotalTimeDigit3"),
+ playerView.FindControl("TotalTimeDigit4"),
+ playerView.FindControl("TotalTimeDigit5"),
+ playerView.FindControl("TotalTimeDigit6"),
+ };
+ }
+
+ ///
+ /// Load an image from the path
+ ///
+ /// Path to the image to load
+ public async Task LoadImage(string path)
+ {
+ return await Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ Init(path, App.Settings.GenerateMissingTOC, App.Settings.PlayHiddenTracks, App.Settings.PlayDataTracks, App.Settings.AutoPlay, App.Settings.Volume);
+ if(Initialized)
+ MainWindow.Instance.Title = "RedBookPlayer - " + path.Split('/').Last().Split('\\').Last();
+
+ return Initialized;
+ });
+ }
+
+ ///
+ /// Set the value for loading data tracks [CompactDisc only]
+ ///
+ /// True to enable loading data tracks, false otherwise
+ public void SetLoadDataTracks(bool load) => _player?.SetLoadDataTracks(load);
+
+ ///
+ /// Set the value for loading hidden tracks [CompactDisc only]
+ ///
+ /// True to enable loading hidden tracks, false otherwise
+ public void SetLoadHiddenTracks(bool load) => _player?.SetLoadHiddenTracks(load);
+
+ ///
+ /// Generate the digit string to be interpreted by the frontend
+ ///
+ /// String representing the digits for the frontend
+ private string GenerateDigitString()
+ {
+ // If the disc isn't initialized, return all '-' characters
+ if(Initialized != true)
+ return string.Empty.PadLeft(20, '-');
+
+ int usableTrackNumber = CurrentTrackNumber;
+ if(usableTrackNumber < 0)
+ usableTrackNumber = 0;
+ else if(usableTrackNumber > 99)
+ usableTrackNumber = 99;
+
+ // Otherwise, take the current time into account
+ ulong sectorTime = GetCurrentSectorTime();
+
+ int[] numbers = new int[]
+ {
+ usableTrackNumber,
+ CurrentTrackIndex,
+
+ (int)(sectorTime / (75 * 60)),
+ (int)(sectorTime / 75 % 60),
+ (int)(sectorTime % 75),
+
+ TotalTracks,
+ TotalIndexes,
+
+ (int)(TotalTime / (75 * 60)),
+ (int)(TotalTime / 75 % 60),
+ (int)(TotalTime % 75),
+ };
+
+ return string.Join("", numbers.Select(i => i.ToString().PadLeft(2, '0').Substring(0, 2)));
+ }
+
+ ///
+ /// Load the png image for a given character based on the theme
+ ///
+ /// Character to load the image for
+ /// Bitmap representing the loaded image
+ private Bitmap GetBitmap(char character)
+ {
+ 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);
+ }
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ ///
+ /// Get current sector time, accounting for offsets
+ ///
+ /// ulong representing the current sector time
+ private ulong GetCurrentSectorTime()
+ {
+ ulong sectorTime = CurrentSector;
+ if(SectionStartSector != 0)
+ sectorTime -= SectionStartSector;
+ else if(CurrentTrackNumber > 0)
+ sectorTime += TimeOffset;
+
+ return sectorTime;
+ }
+
+ ///
+ /// Generate a path selection dialog box
+ ///
+ /// User-selected path, if possible
+ private async Task GetPath()
+ {
+ return await Dispatcher.UIThread.InvokeAsync(async () =>
+ {
+ var dialog = new OpenFileDialog { AllowMultiple = false };
+ List knownExtensions = new Aaru.DiscImages.AaruFormat().KnownExtensions.ToList();
+ dialog.Filters.Add(new FileDialogFilter()
+ {
+ Name = "Aaru Image Format (*" + string.Join(", *", knownExtensions) + ")",
+ Extensions = knownExtensions.ConvertAll(e => e.TrimStart('.'))
+ });
+
+ return (await dialog.ShowAsync(MainWindow.Instance))?.FirstOrDefault();
+ });
+ }
+
+ ///
+ /// Update the view-model from the Player
+ ///
+ private void PlayerStateChanged(object sender, PropertyChangedEventArgs e)
+ {
+ if(_player?.Initialized != true)
+ return;
+
+ CurrentTrackNumber = _player.CurrentTrackNumber;
+ CurrentTrackIndex = _player.CurrentTrackIndex;
+ CurrentSector = _player.CurrentSector;
+ SectionStartSector = _player.SectionStartSector;
+
+ HiddenTrack = _player.HiddenTrack;
+
+ QuadChannel = _player.QuadChannel;
+ IsDataTrack = _player.IsDataTrack;
+ CopyAllowed = _player.CopyAllowed;
+ TrackHasEmphasis = _player.TrackHasEmphasis;
+
+ Playing = _player.Playing;
+ ApplyDeEmphasis = _player.ApplyDeEmphasis;
+ Volume = _player.Volume;
+
+ UpdateDigits();
+ }
+
+ ///
+ /// Update UI
+ ///
+ private void UpdateDigits()
+ {
+ Dispatcher.UIThread.Post(() =>
+ {
+ string digitString = GenerateDigitString() ?? string.Empty.PadLeft(20, '-');
+ for(int i = 0; i < _digits.Length; i++)
+ {
+ Bitmap digitImage = GetBitmap(digitString[i]);
+ if(_digits[i] != null && digitImage != null)
+ _digits[i].Source = digitImage;
+ }
+ });
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/RedBookPlayer/GUI/MainWindow.xaml b/RedBookPlayer.GUI/Views/MainWindow.xaml
similarity index 76%
rename from RedBookPlayer/GUI/MainWindow.xaml
rename to RedBookPlayer.GUI/Views/MainWindow.xaml
index 56dbf79..1a3e211 100644
--- a/RedBookPlayer/GUI/MainWindow.xaml
+++ b/RedBookPlayer.GUI/Views/MainWindow.xaml
@@ -1,7 +1,7 @@
\ No newline at end of file
diff --git a/RedBookPlayer/GUI/MainWindow.xaml.cs b/RedBookPlayer.GUI/Views/MainWindow.xaml.cs
similarity index 78%
rename from RedBookPlayer/GUI/MainWindow.xaml.cs
rename to RedBookPlayer.GUI/Views/MainWindow.xaml.cs
index 9c46297..3e5a26a 100644
--- a/RedBookPlayer/GUI/MainWindow.xaml.cs
+++ b/RedBookPlayer.GUI/Views/MainWindow.xaml.cs
@@ -5,8 +5,9 @@ using System.Xml;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
+using RedBookPlayer.GUI.ViewModels;
-namespace RedBookPlayer.GUI
+namespace RedBookPlayer.GUI.Views
{
public class MainWindow : Window
{
@@ -30,10 +31,13 @@ namespace RedBookPlayer.GUI
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();
+ Instance.ContentControl.Content = new PlayerView(pvm);
}
else
{
@@ -50,17 +54,19 @@ namespace RedBookPlayer.GUI
{
string xaml = File.ReadAllText(xamlPath);
xaml = xaml.Replace("Source=\"", $"Source=\"file://{themeDirectory}/");
- Instance.ContentControl.Content = new PlayerView(xaml);
+ 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();
+ Instance.ContentControl.Content = new PlayerView(pvm);
}
}
Instance.Width = ((PlayerView)Instance.ContentControl.Content).Width;
Instance.Height = ((PlayerView)Instance.ContentControl.Content).Height;
+
+ pvm.InitializeDigits();
}
///
@@ -78,6 +84,8 @@ namespace RedBookPlayer.GUI
ContentControl.Content = new PlayerView();
+ ((PlayerView)ContentControl.Content).PlayerViewModel.InitializeDigits();
+
CanResize = false;
KeyDown += OnKeyDown;
@@ -90,7 +98,7 @@ namespace RedBookPlayer.GUI
Closing += (e, f) =>
{
- ((PlayerView)ContentControl.Content).StopButton_Click(this, null);
+ ((PlayerView)ContentControl.Content).PlayerViewModel.ExecuteStop();
};
AddHandler(DragDrop.DropEvent, MainWindow_Drop);
@@ -107,7 +115,7 @@ namespace RedBookPlayer.GUI
IEnumerable fileNames = e.Data.GetFileNames();
foreach(string filename in fileNames)
{
- bool loaded = await playerView.LoadImage(filename);
+ bool loaded = await playerView?.PlayerViewModel?.LoadImage(filename);
if(loaded)
break;
}
@@ -121,61 +129,62 @@ namespace RedBookPlayer.GUI
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?.LoadButton_Click(this, null);
+ playerView?.PlayerViewModel?.ExecuteLoad();
}
// Toggle playback
else if(e.Key == App.Settings.TogglePlaybackKey || e.Key == Key.MediaPlayPause)
{
- playerView?.PlayPauseButton_Click(this, null);
+ playerView?.PlayerViewModel?.ExecuteTogglePlayPause();
}
// Stop playback
else if(e.Key == App.Settings.StopPlaybackKey || e.Key == Key.MediaStop)
{
- playerView?.StopButton_Click(this, null);
+ playerView?.PlayerViewModel?.ExecuteStop();
}
// Next Track
else if(e.Key == App.Settings.NextTrackKey || e.Key == Key.MediaNextTrack)
{
- playerView?.NextTrackButton_Click(this, null);
+ playerView?.PlayerViewModel?.ExecuteNextTrack();
}
// Previous Track
else if(e.Key == App.Settings.PreviousTrackKey || e.Key == Key.MediaPreviousTrack)
{
- playerView?.PreviousTrackButton_Click(this, null);
+ playerView?.PlayerViewModel?.ExecutePreviousTrack();
}
// Next Index
else if(e.Key == App.Settings.NextIndexKey)
{
- playerView?.NextIndexButton_Click(this, null);
+ playerView?.PlayerViewModel?.ExecuteNextIndex();
}
// Previous Index
else if(e.Key == App.Settings.PreviousIndexKey)
{
- playerView?.PreviousIndexButton_Click(this, null);
+ playerView?.PlayerViewModel?.ExecutePreviousIndex();
}
// Fast Foward
else if(e.Key == App.Settings.FastForwardPlaybackKey)
{
- playerView?.FastForwardButton_Click(this, null);
+ playerView?.PlayerViewModel?.ExecuteFastForward();
}
// Rewind
else if(e.Key == App.Settings.RewindPlaybackKey)
{
- playerView?.RewindButton_Click(this, null);
+ playerView?.PlayerViewModel?.ExecuteRewind();
}
// Volume Up
@@ -188,7 +197,7 @@ namespace RedBookPlayer.GUI
increment *= 5;
if(playerView?.PlayerViewModel?.Volume != null)
- playerView.PlayerViewModel.Volume += increment;
+ playerView.PlayerViewModel.ExecuteSetVolume(playerView.PlayerViewModel.Volume + increment);
}
// Volume Down
@@ -201,22 +210,28 @@ namespace RedBookPlayer.GUI
decrement *= 5;
if (playerView?.PlayerViewModel?.Volume != null)
- playerView.PlayerViewModel.Volume -= decrement;
+ playerView.PlayerViewModel.ExecuteSetVolume(playerView.PlayerViewModel.Volume - decrement);
}
// Mute Toggle
else if(e.Key == App.Settings.ToggleMuteKey || e.Key == Key.VolumeMute)
{
- playerView?.MuteToggleButton_Click(this, null);
+ playerView?.PlayerViewModel?.ExecuteToggleMute();
}
// Emphasis Toggle
else if(e.Key == App.Settings.ToggleDeEmphasisKey)
{
- playerView?.EnableDisableDeEmphasisButton_Click(this, null);
+ 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/PlayerView.xaml b/RedBookPlayer.GUI/Views/PlayerView.xaml
similarity index 81%
rename from RedBookPlayer/GUI/PlayerView.xaml
rename to RedBookPlayer.GUI/Views/PlayerView.xaml
index 9221ecd..941522e 100644
--- a/RedBookPlayer/GUI/PlayerView.xaml
+++ b/RedBookPlayer.GUI/Views/PlayerView.xaml
@@ -1,21 +1,21 @@
+ x:Class="RedBookPlayer.GUI.Views.PlayerView" Width="900" Height="400">
-
+
-
-
-
+
+
+
-
-
-
-
- Rewind
- Fast Forward
+
+
+
+
+ Rewind
+ Fast Forward
@@ -76,11 +76,11 @@
-
-
@@ -98,6 +98,7 @@
4CH
HIDDEN
HIDDEN
+
\ No newline at end of file
diff --git a/RedBookPlayer.GUI/Views/PlayerView.xaml.cs b/RedBookPlayer.GUI/Views/PlayerView.xaml.cs
new file mode 100644
index 0000000..253fd9b
--- /dev/null
+++ b/RedBookPlayer.GUI/Views/PlayerView.xaml.cs
@@ -0,0 +1,73 @@
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using RedBookPlayer.GUI.ViewModels;
+
+namespace RedBookPlayer.GUI.Views
+{
+ public class PlayerView : UserControl
+ {
+ ///
+ /// 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/SettingsWindow.xaml b/RedBookPlayer.GUI/Views/SettingsWindow.xaml
similarity index 96%
rename from RedBookPlayer/GUI/SettingsWindow.xaml
rename to RedBookPlayer.GUI/Views/SettingsWindow.xaml
index ba416ba..d64c388 100644
--- a/RedBookPlayer/GUI/SettingsWindow.xaml
+++ b/RedBookPlayer.GUI/Views/SettingsWindow.xaml
@@ -1,7 +1,7 @@
+ d:DesignHeight="450" x:Class="RedBookPlayer.GUI.Views.SettingsWindow" Title="Settings" SizeToContent="WidthAndHeight">
@@ -17,8 +17,8 @@
Index navigation can change track
-
- Treat index 0 of track 1 as track 0 (hidden track)
+
+ Play hidden tracks
diff --git a/RedBookPlayer/GUI/SettingsWindow.xaml.cs b/RedBookPlayer.GUI/Views/SettingsWindow.xaml.cs
similarity index 99%
rename from RedBookPlayer/GUI/SettingsWindow.xaml.cs
rename to RedBookPlayer.GUI/Views/SettingsWindow.xaml.cs
index 3f1d226..9b5c15d 100644
--- a/RedBookPlayer/GUI/SettingsWindow.xaml.cs
+++ b/RedBookPlayer.GUI/Views/SettingsWindow.xaml.cs
@@ -6,7 +6,7 @@ using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
-namespace RedBookPlayer.GUI
+namespace RedBookPlayer.GUI.Views
{
public class SettingsWindow : Window
{
diff --git a/RedBookPlayer/nuget.config b/RedBookPlayer.GUI/nuget.config
similarity index 100%
rename from RedBookPlayer/nuget.config
rename to RedBookPlayer.GUI/nuget.config
diff --git a/RedBookPlayer/Discs/CompactDisc.cs b/RedBookPlayer.Models/Discs/CompactDisc.cs
similarity index 54%
rename from RedBookPlayer/Discs/CompactDisc.cs
rename to RedBookPlayer.Models/Discs/CompactDisc.cs
index 21e8fc8..6690b6e 100644
--- a/RedBookPlayer/Discs/CompactDisc.cs
+++ b/RedBookPlayer.Models/Discs/CompactDisc.cs
@@ -6,11 +6,12 @@ using Aaru.CommonTypes.Interfaces;
using Aaru.CommonTypes.Structs;
using Aaru.Decoders.CD;
using Aaru.Helpers;
+using ReactiveUI;
using static Aaru.Decoders.CD.FullTOC;
-namespace RedBookPlayer.Discs
+namespace RedBookPlayer.Models.Discs
{
- public class CompactDisc : OpticalDisc
+ public class CompactDisc : OpticalDiscBase, IReactiveObject
{
#region Public Fields
@@ -24,35 +25,52 @@ namespace RedBookPlayer.Discs
if(_image == null)
return;
+ // Data tracks only and flag disabled means we can't do anything
+ if(_image.Tracks.All(t => t.TrackType != TrackType.Audio) && !LoadDataTracks)
+ return;
+
// Cache the value and the current track number
int cachedValue = value;
- int cachedTrackNumber = _currentTrackNumber;
+ int cachedTrackNumber;
// Check if we're incrementing or decrementing the track
bool increment = cachedValue >= _currentTrackNumber;
do
{
- // Ensure that the value is valid, wrapping around if necessary
- if(cachedValue >= _image.Tracks.Count)
- cachedValue = 0;
- else if(cachedValue < 0)
- cachedValue = _image.Tracks.Count - 1;
+ // If we're over the last track, wrap around
+ if(cachedValue > _image.Tracks.Max(t => t.TrackSequence))
+ {
+ cachedValue = (int)_image.Tracks.Min(t => t.TrackSequence);
+ if(cachedValue == 0 && !LoadHiddenTracks)
+ cachedValue++;
+ }
- _currentTrackNumber = cachedValue;
+ // If we're under the first track and we're not loading hidden tracks, wrap around
+ else if(cachedValue < 1 && !LoadHiddenTracks)
+ {
+ cachedValue = (int)_image.Tracks.Max(t => t.TrackSequence);
+ }
+
+ // If we're under the first valid track, wrap around
+ else if(cachedValue < _image.Tracks.Min(t => t.TrackSequence))
+ {
+ cachedValue = (int)_image.Tracks.Max(t => t.TrackSequence);
+ }
+
+ cachedTrackNumber = cachedValue;
// Cache the current track for easy access
- Track track = _image.Tracks[_currentTrackNumber];
+ Track track = GetTrack(cachedTrackNumber);
+ if(track == null)
+ return;
// Set track flags from subchannel data, if possible
SetTrackFlags(track);
- TotalIndexes = track.Indexes.Keys.Max();
- CurrentTrackIndex = track.Indexes.Keys.Min();
-
// If the track is playable, just return
- if(TrackType == TrackType.Audio || App.Settings.PlayDataTracks)
- return;
+ if(TrackType == TrackType.Audio || LoadDataTracks)
+ break;
// If we're not playing the track, skip
if(increment)
@@ -60,7 +78,28 @@ namespace RedBookPlayer.Discs
else
cachedValue--;
}
- while(cachedValue != cachedTrackNumber);
+ while(cachedValue != _currentTrackNumber);
+
+ // If we looped around, ensure it reloads
+ if(cachedValue == _currentTrackNumber)
+ {
+ this.RaiseAndSetIfChanged(ref _currentTrackNumber, -1);
+
+ Track track = GetTrack(cachedValue);
+ if(track == null)
+ return;
+
+ SetTrackFlags(track);
+ }
+
+ this.RaiseAndSetIfChanged(ref _currentTrackNumber, cachedValue);
+
+ Track cachedTrack = GetTrack(cachedValue);
+ if(cachedTrack == null)
+ return;
+
+ TotalIndexes = cachedTrack.Indexes.Keys.Max();
+ CurrentTrackIndex = cachedTrack.Indexes.Keys.Min();
}
}
@@ -75,15 +114,18 @@ namespace RedBookPlayer.Discs
return;
// Cache the current track for easy access
- Track track = _image.Tracks[CurrentTrackNumber];
+ Track track = GetTrack(CurrentTrackNumber);
+ if(track == null)
+ return;
// Ensure that the value is valid, wrapping around if necessary
+ ushort fixedValue = value;
if(value > track.Indexes.Keys.Max())
- _currentTrackIndex = track.Indexes.Keys.Min();
+ fixedValue = track.Indexes.Keys.Min();
else if(value < track.Indexes.Keys.Min())
- _currentTrackIndex = track.Indexes.Keys.Max();
- else
- _currentTrackIndex = value;
+ fixedValue = track.Indexes.Keys.Max();
+
+ this.RaiseAndSetIfChanged(ref _currentTrackIndex, fixedValue);
// Set new index-specific data
SectionStartSector = (ulong)track.Indexes[CurrentTrackIndex];
@@ -95,25 +137,27 @@ namespace RedBookPlayer.Discs
public override ulong CurrentSector
{
get => _currentSector;
- set
+ protected set
{
// Unset image means we can't do anything
if(_image == null)
return;
// Cache the current track for easy access
- Track track = _image.Tracks[CurrentTrackNumber];
+ Track track = GetTrack(CurrentTrackNumber);
+ if(track == null)
+ return;
- _currentSector = value;
+ this.RaiseAndSetIfChanged(ref _currentSector, value);
- if((CurrentTrackNumber < _image.Tracks.Count - 1 && CurrentSector >= _image.Tracks[CurrentTrackNumber + 1].TrackStartSector)
+ if((CurrentTrackNumber < _image.Tracks.Count - 1 && CurrentSector >= (GetTrack(CurrentTrackNumber + 1)?.TrackStartSector ?? 0))
|| (CurrentTrackNumber > 0 && CurrentSector < track.TrackStartSector))
{
foreach(Track trackData in _image.Tracks.ToArray().Reverse())
{
if(CurrentSector >= trackData.TrackStartSector)
{
- CurrentTrackNumber = (int)trackData.TrackSequence - 1;
+ CurrentTrackNumber = (int)trackData.TrackSequence;
break;
}
}
@@ -132,25 +176,59 @@ namespace RedBookPlayer.Discs
}
}
+ ///
+ public override int BytesPerSector => GetTrack(CurrentTrackNumber)?.TrackRawBytesPerSector ?? 0;
+
///
/// Represents the 4CH flag
///
- public bool QuadChannel { get; private set; } = false;
+ public bool QuadChannel
+ {
+ get => _quadChannel;
+ private set => this.RaiseAndSetIfChanged(ref _quadChannel, value);
+ }
///
/// Represents the DATA flag
///
- public bool IsDataTrack => TrackType != TrackType.Audio;
+ public bool IsDataTrack
+ {
+ get => _isDataTrack;
+ private set => this.RaiseAndSetIfChanged(ref _isDataTrack, value);
+ }
///
/// Represents the DCP flag
///
- public bool CopyAllowed { get; private set; } = false;
+ public bool CopyAllowed
+ {
+ get => _copyAllowed;
+ private set => this.RaiseAndSetIfChanged(ref _copyAllowed, value);
+ }
///
/// Represents the PRE flag
///
- public bool TrackHasEmphasis { get; private set; } = false;
+ public bool TrackHasEmphasis
+ {
+ get => _trackHasEmphasis;
+ private set => this.RaiseAndSetIfChanged(ref _trackHasEmphasis, value);
+ }
+
+ ///
+ /// Indicate if data tracks should be loaded
+ ///
+ public bool LoadDataTracks { get; set; } = false;
+
+ ///
+ /// Indicate if hidden tracks should be loaded
+ ///
+ public bool LoadHiddenTracks { get; set; } = false;
+
+ private bool _quadChannel;
+ private bool _isDataTrack;
+ private bool _copyAllowed;
+ private bool _trackHasEmphasis;
#endregion
@@ -171,6 +249,11 @@ namespace RedBookPlayer.Discs
///
private ulong _currentSector = 0;
+ ///
+ /// Indicate if a TOC should be generated if missing
+ ///
+ private readonly bool _generateMissingToc = false;
+
///
/// Current disc table of contents
///
@@ -178,8 +261,21 @@ namespace RedBookPlayer.Discs
#endregion
+ ///
+ /// Constructor
+ ///
+ /// Generate a TOC if the disc is missing one
+ /// Load hidden tracks for playback
+ /// Load data tracks for playback
+ public CompactDisc(bool generateMissingToc, bool loadHiddenTracks, bool loadDataTracks)
+ {
+ _generateMissingToc = generateMissingToc;
+ LoadHiddenTracks = loadHiddenTracks;
+ LoadDataTracks = loadDataTracks;
+ }
+
///
- public override void Init(IOpticalMediaImage image, bool autoPlay = false)
+ public override void Init(IOpticalMediaImage image, bool autoPlay)
{
// If the image is null, we can't do anything
if(image == null)
@@ -200,7 +296,7 @@ namespace RedBookPlayer.Discs
TotalIndexes = 0;
// Set the internal disc state
- TotalTracks = _image.Tracks.Count;
+ TotalTracks = (int)_image.Tracks.Max(t => t.TrackSequence);
TrackDataDescriptor firstTrack = _toc.TrackDescriptors.First(d => d.ADR == 1 && d.POINT == 1);
TimeOffset = (ulong)((firstTrack.PMIN * 60 * 75) + (firstTrack.PSEC * 75) + firstTrack.PFRAME);
TotalTime = TimeOffset + _image.Tracks.Last().TrackEndSector;
@@ -211,24 +307,63 @@ namespace RedBookPlayer.Discs
#region Seeking
+ ///
+ public override void NextTrack()
+ {
+ if(_image == null)
+ return;
+
+ CurrentTrackNumber++;
+ LoadTrack(CurrentTrackNumber);
+ }
+
+ ///
+ public override void PreviousTrack()
+ {
+ if(_image == null)
+ return;
+
+ CurrentTrackNumber--;
+ LoadTrack(CurrentTrackNumber);
+ }
+
///
public override bool NextIndex(bool changeTrack)
{
if(_image == null)
return false;
- if(CurrentTrackIndex + 1 > _image.Tracks[CurrentTrackNumber].Indexes.Keys.Max())
+ // Cache the current track for easy access
+ Track track = GetTrack(CurrentTrackNumber);
+ if(track == null)
+ return false;
+
+ // If the index is greater than the highest index, change tracks if needed
+ if(CurrentTrackIndex + 1 > track.Indexes.Keys.Max())
{
if(changeTrack)
{
NextTrack();
- CurrentSector = (ulong)_image.Tracks[CurrentTrackNumber].Indexes.Values.Min();
+ CurrentSector = (ulong)GetTrack(CurrentTrackNumber).Indexes.Values.Min();
return true;
}
}
+
+ // If the next index has an invalid offset, change tracks if needed
+ else if(track.Indexes[(ushort)(CurrentTrackIndex + 1)] < 0)
+ {
+ if(changeTrack)
+ {
+ NextTrack();
+ CurrentSector = (ulong)GetTrack(CurrentTrackNumber).Indexes.Values.Min();
+ return true;
+ }
+ }
+
+ // Otherwise, just move to the next index
else
{
- CurrentSector = (ulong)_image.Tracks[CurrentTrackNumber].Indexes[++CurrentTrackIndex];
+ CurrentSector = (ulong)track.Indexes[++CurrentTrackIndex];
}
return false;
@@ -240,18 +375,37 @@ namespace RedBookPlayer.Discs
if(_image == null)
return false;
- if(CurrentTrackIndex - 1 < _image.Tracks[CurrentTrackNumber].Indexes.Keys.Min())
+ // Cache the current track for easy access
+ Track track = GetTrack(CurrentTrackNumber);
+ if(track == null)
+ return false;
+
+ // If the index is less than the lowest index, change tracks if needed
+ if(CurrentTrackIndex - 1 < track.Indexes.Keys.Min())
{
if(changeTrack)
{
PreviousTrack();
- CurrentSector = (ulong)_image.Tracks[CurrentTrackNumber].Indexes.Values.Max();
+ CurrentSector = (ulong)GetTrack(CurrentTrackNumber).Indexes.Values.Max();
return true;
}
}
+
+ // If the previous index has an invalid offset, change tracks if needed
+ else if (track.Indexes[(ushort)(CurrentTrackIndex - 1)] < 0)
+ {
+ if(changeTrack)
+ {
+ PreviousTrack();
+ CurrentSector = (ulong)GetTrack(CurrentTrackNumber).Indexes.Values.Max();
+ return true;
+ }
+ }
+
+ // Otherwise, just move to the previous index
else
{
- CurrentSector = (ulong)_image.Tracks[CurrentTrackNumber].Indexes[--CurrentTrackIndex];
+ CurrentSector = (ulong)track.Indexes[--CurrentTrackIndex];
}
return false;
@@ -264,7 +418,7 @@ namespace RedBookPlayer.Discs
///
public override void LoadFirstTrack()
{
- CurrentTrackNumber = 0;
+ CurrentTrackNumber = 1;
LoadTrack(CurrentTrackNumber);
}
@@ -274,21 +428,41 @@ namespace RedBookPlayer.Discs
if(_image == null)
return;
- TotalIndexes = _image.Tracks[CurrentTrackNumber].Indexes.Keys.Max();
+ TotalIndexes = GetTrack(CurrentTrackNumber)?.Indexes.Keys.Max() ?? 0;
}
///
- protected override void LoadTrack(int track)
+ protected override void LoadTrack(int trackNumber)
{
if(_image == null)
return;
- if(track < 0 || track >= _image.Tracks.Count)
+ // If the track number is invalid, just return
+ if(trackNumber < _image.Tracks.Min(t => t.TrackSequence) || trackNumber > _image.Tracks.Max(t => t.TrackSequence))
return;
- ushort firstIndex = _image.Tracks[track].Indexes.Keys.Min();
- int firstSector = _image.Tracks[track].Indexes[firstIndex];
- CurrentSector = (ulong)(firstSector >= 0 ? firstSector : _image.Tracks[track].Indexes[1]);
+ // Cache the current track for easy access
+ Track track = GetTrack(trackNumber);
+
+ // Select the first index that has a sector offset greater than or equal to 0
+ CurrentSector = (ulong)(track?.Indexes.OrderBy(kvp => kvp.Key).First(kvp => kvp.Value >= 0).Value ?? 0);
+ }
+
+ ///
+ /// Get the track with the given sequence value, if possible
+ ///
+ /// Track number to retrieve
+ /// Track object for the requested sequence, null on error
+ private Track GetTrack(int trackNumber)
+ {
+ try
+ {
+ return _image.Tracks.FirstOrDefault(t => t.TrackSequence == trackNumber);
+ }
+ catch
+ {
+ return null;
+ }
}
///
@@ -304,7 +478,7 @@ namespace RedBookPlayer.Discs
if(_image.Info.ReadableMediaTags?.Contains(MediaTagType.CD_FullTOC) != true)
{
// Only generate the TOC if we have it set
- if(!App.Settings.GenerateMissingTOC)
+ if(!_generateMissingToc)
{
Console.WriteLine("Full TOC not found");
return false;
@@ -369,8 +543,9 @@ namespace RedBookPlayer.Discs
/// Track object to read from
private void SetDefaultTrackFlags(Track track)
{
- QuadChannel = false;
TrackType = track.TrackType;
+ QuadChannel = false;
+ IsDataTrack = track.TrackType != TrackType.Audio;
CopyAllowed = false;
TrackHasEmphasis = false;
}
@@ -390,9 +565,11 @@ namespace RedBookPlayer.Discs
byte flags = (byte)(descriptor.CONTROL & 0x0D);
TrackHasEmphasis = (flags & (byte)TocControl.TwoChanPreEmph) == (byte)TocControl.TwoChanPreEmph;
CopyAllowed = (flags & (byte)TocControl.CopyPermissionMask) == (byte)TocControl.CopyPermissionMask;
- TrackType = (flags & (byte)TocControl.DataTrack) == (byte)TocControl.DataTrack ? TrackType.Data : TrackType.Audio;
+ IsDataTrack = (flags & (byte)TocControl.DataTrack) == (byte)TocControl.DataTrack;
QuadChannel = (flags & (byte)TocControl.FourChanNoPreEmph) == (byte)TocControl.FourChanNoPreEmph;
+ TrackType = IsDataTrack ? TrackType.Data : TrackType.Audio;
+
return;
}
catch(Exception)
diff --git a/RedBookPlayer/Discs/OpticalDisc.cs b/RedBookPlayer.Models/Discs/OpticalDiscBase.cs
similarity index 80%
rename from RedBookPlayer/Discs/OpticalDisc.cs
rename to RedBookPlayer.Models/Discs/OpticalDiscBase.cs
index 817c626..f2ce155 100644
--- a/RedBookPlayer/Discs/OpticalDisc.cs
+++ b/RedBookPlayer.Models/Discs/OpticalDiscBase.cs
@@ -1,9 +1,10 @@
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Interfaces;
+using ReactiveUI;
-namespace RedBookPlayer.Discs
+namespace RedBookPlayer.Models.Discs
{
- public abstract class OpticalDisc
+ public abstract class OpticalDiscBase : ReactiveObject
{
#region Public Fields
@@ -25,17 +26,21 @@ namespace RedBookPlayer.Discs
///
/// Current sector number
///
- public abstract ulong CurrentSector { get; set; }
+ public abstract ulong CurrentSector { get; protected set; }
///
/// Represents the sector starting the section
///
- public ulong SectionStartSector { get; protected set; }
+ public ulong SectionStartSector
+ {
+ get => _sectionStartSector;
+ protected set => this.RaiseAndSetIfChanged(ref _sectionStartSector, value);
+ }
///
/// Number of bytes per sector for the current track
///
- public int BytesPerSector => _image.Tracks[CurrentTrackNumber].TrackBytesPerSector;
+ public abstract int BytesPerSector { get; }
///
/// Represents the track type
@@ -55,7 +60,7 @@ namespace RedBookPlayer.Discs
///
/// Total sectors in the image
///
- public ulong TotalSectors => _image.Info.Sectors;
+ public ulong TotalSectors => _image?.Info.Sectors ?? 0;
///
/// Represents the time adjustment offset for the disc
@@ -67,6 +72,8 @@ namespace RedBookPlayer.Discs
///
public ulong TotalTime { get; protected set; } = 0;
+ private ulong _sectionStartSector;
+
#endregion
#region Protected State Variables
@@ -83,42 +90,19 @@ namespace RedBookPlayer.Discs
///
/// Aaruformat image to load
/// True if playback should begin immediately, false otherwise
- public abstract void Init(IOpticalMediaImage image, bool autoPlay = false);
+ public abstract void Init(IOpticalMediaImage image, bool autoPlay);
#region Seeking
///
/// Try to move to the next track, wrapping around if necessary
///
- public void NextTrack()
- {
- if(_image == null)
- return;
-
- CurrentTrackNumber++;
- LoadTrack(CurrentTrackNumber);
- }
+ public abstract void NextTrack();
///
/// Try to move to the previous track, wrapping around if necessary
///
- public void PreviousTrack()
- {
- if(_image == null)
- return;
-
- if(CurrentSector < (ulong)_image.Tracks[CurrentTrackNumber].Indexes[1] + 75)
- {
- if(App.Settings.AllowSkipHiddenTrack && CurrentTrackNumber == 0 && CurrentSector >= 75)
- CurrentSector = 0;
- else
- CurrentTrackNumber--;
- }
- else
- CurrentTrackNumber--;
-
- LoadTrack(CurrentTrackNumber);
- }
+ public abstract void PreviousTrack();
///
/// Try to move to the next track index
@@ -155,6 +139,12 @@ namespace RedBookPlayer.Discs
///
public abstract void SetTotalIndexes();
+ ///
+ /// Set the current sector
+ ///
+ /// New sector number to use
+ public void SetCurrentSector(ulong sector) => CurrentSector = sector;
+
///
/// Load the desired track, if possible
///
diff --git a/RedBookPlayer/Discs/OpticalDiscFactory.cs b/RedBookPlayer.Models/Factories/OpticalDiscFactory.cs
similarity index 51%
rename from RedBookPlayer/Discs/OpticalDiscFactory.cs
rename to RedBookPlayer.Models/Factories/OpticalDiscFactory.cs
index 99d9257..d09beaf 100644
--- a/RedBookPlayer/Discs/OpticalDiscFactory.cs
+++ b/RedBookPlayer.Models/Factories/OpticalDiscFactory.cs
@@ -1,31 +1,72 @@
+using System.IO;
using Aaru.CommonTypes.Interfaces;
using Aaru.CommonTypes.Metadata;
+using Aaru.DiscImages;
+using Aaru.Filters;
+using RedBookPlayer.Models.Discs;
-namespace RedBookPlayer.Discs
+namespace RedBookPlayer.Models.Factories
{
public static class OpticalDiscFactory
{
+ ///
+ /// Generate an OpticalDisc from an input path
+ ///
+ /// Path to load the image 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]
+ /// True if the image should be playable immediately, false otherwise
+ /// Instantiated OpticalDisc, if possible
+ public static OpticalDiscBase GenerateFromPath(string path, bool generateMissingToc, bool loadHiddenTracks, bool loadDataTracks, bool autoPlay)
+ {
+ try
+ {
+ // Validate the image exists
+ if(string.IsNullOrWhiteSpace(path) || !File.Exists(path))
+ return null;
+
+ // Load the disc image to memory
+ // TODO: Assumes Aaruformat right now for all
+ var image = new AaruFormat();
+ var filter = new ZZZNoFilter();
+ filter.Open(path);
+ image.Open(filter);
+
+ // Generate and instantiate the disc
+ return GenerateFromImage(image, generateMissingToc, loadHiddenTracks, loadDataTracks, autoPlay);
+ }
+ catch
+ {
+ // All errors mean an invalid image in some way
+ return null;
+ }
+ }
+
///
/// Generate an OpticalDisc from an input IOpticalMediaImage
///
/// 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]
/// True if the image should be playable immediately, false otherwise
/// Instantiated OpticalDisc, if possible
- public static OpticalDisc GenerateFromImage(IOpticalMediaImage image, bool autoPlay)
+ public static OpticalDiscBase GenerateFromImage(IOpticalMediaImage image, bool generateMissingToc, bool loadHiddenTracks, bool loadDataTracks, bool autoPlay)
{
// If the image is not usable, we don't do anything
if(!IsUsableImage(image))
return null;
// Create the output object
- OpticalDisc opticalDisc;
+ OpticalDiscBase opticalDisc;
// Create the proper disc type
switch(GetMediaType(image))
{
case "Compact Disc":
case "GD":
- opticalDisc = new CompactDisc();
+ opticalDisc = new CompactDisc(generateMissingToc, loadHiddenTracks, loadDataTracks);
break;
default:
opticalDisc = null;
diff --git a/RedBookPlayer/Hardware/DeEmphasisFilter.cs b/RedBookPlayer.Models/Hardware/DeEmphasisFilter.cs
similarity index 97%
rename from RedBookPlayer/Hardware/DeEmphasisFilter.cs
rename to RedBookPlayer.Models/Hardware/DeEmphasisFilter.cs
index 546cbb3..5b74b1d 100644
--- a/RedBookPlayer/Hardware/DeEmphasisFilter.cs
+++ b/RedBookPlayer.Models/Hardware/DeEmphasisFilter.cs
@@ -1,7 +1,7 @@
using System;
using NWaves.Filters.BiQuad;
-namespace RedBookPlayer.Hardware
+namespace RedBookPlayer.Models.Hardware
{
///
/// Filter for applying de-emphasis to audio
diff --git a/RedBookPlayer.Models/Hardware/Player.cs b/RedBookPlayer.Models/Hardware/Player.cs
new file mode 100644
index 0000000..7db9f0c
--- /dev/null
+++ b/RedBookPlayer.Models/Hardware/Player.cs
@@ -0,0 +1,518 @@
+using System;
+using System.ComponentModel;
+using Aaru.CommonTypes.Enums;
+using ReactiveUI;
+using RedBookPlayer.Models.Discs;
+using RedBookPlayer.Models.Factories;
+
+namespace RedBookPlayer.Models.Hardware
+{
+ public class Player : ReactiveObject
+ {
+ ///
+ /// Indicate if the player is ready to be used
+ ///
+ public bool Initialized { get; private set; } = false;
+
+ #region OpticalDisc Passthrough
+
+ ///
+ /// Current track number
+ ///
+ public int CurrentTrackNumber
+ {
+ get => _currentTrackNumber;
+ private set => this.RaiseAndSetIfChanged(ref _currentTrackNumber, value);
+ }
+
+ ///
+ /// Current track index
+ ///
+ public ushort CurrentTrackIndex
+ {
+ get => _currentTrackIndex;
+ private set => this.RaiseAndSetIfChanged(ref _currentTrackIndex, value);
+ }
+
+ ///
+ /// Current sector number
+ ///
+ public ulong CurrentSector
+ {
+ get => _currentSector;
+ private set => this.RaiseAndSetIfChanged(ref _currentSector, value);
+ }
+
+ ///
+ /// Represents the sector starting the section
+ ///
+ public ulong SectionStartSector
+ {
+ get => _sectionStartSector;
+ protected set => this.RaiseAndSetIfChanged(ref _sectionStartSector, value);
+ }
+
+ ///
+ /// Represents if the disc has a hidden track
+ ///
+ public bool HiddenTrack
+ {
+ get => _hasHiddenTrack;
+ private set => this.RaiseAndSetIfChanged(ref _hasHiddenTrack, value);
+ }
+
+ ///
+ /// Represents the 4CH flag [CompactDisc only]
+ ///
+ public bool QuadChannel
+ {
+ get => _quadChannel;
+ private set => this.RaiseAndSetIfChanged(ref _quadChannel, value);
+ }
+
+ ///
+ /// Represents the DATA flag [CompactDisc only]
+ ///
+ public bool IsDataTrack
+ {
+ get => _isDataTrack;
+ private set => this.RaiseAndSetIfChanged(ref _isDataTrack, value);
+ }
+
+ ///
+ /// Represents the DCP flag [CompactDisc only]
+ ///
+ public bool CopyAllowed
+ {
+ get => _copyAllowed;
+ private set => this.RaiseAndSetIfChanged(ref _copyAllowed, value);
+ }
+
+ ///
+ /// Represents the PRE flag [CompactDisc only]
+ ///
+ public bool TrackHasEmphasis
+ {
+ get => _trackHasEmphasis;
+ private set => this.RaiseAndSetIfChanged(ref _trackHasEmphasis, value);
+ }
+
+ ///
+ /// Represents the total tracks on the disc
+ ///
+ public int TotalTracks => _opticalDisc.TotalTracks;
+
+ ///
+ /// Represents the total indices on the disc
+ ///
+ public int TotalIndexes => _opticalDisc.TotalIndexes;
+
+ ///
+ /// Total sectors in the image
+ ///
+ public ulong TotalSectors => _opticalDisc.TotalSectors;
+
+ ///
+ /// Represents the time adjustment offset for the disc
+ ///
+ public ulong TimeOffset => _opticalDisc.TimeOffset;
+
+ ///
+ /// Represents the total playing time for the disc
+ ///
+ public ulong TotalTime => _opticalDisc.TotalTime;
+
+ private int _currentTrackNumber;
+ private ushort _currentTrackIndex;
+ private ulong _currentSector;
+ private ulong _sectionStartSector;
+
+ private bool _hasHiddenTrack;
+ private bool _quadChannel;
+ private bool _isDataTrack;
+ private bool _copyAllowed;
+ private bool _trackHasEmphasis;
+
+ #endregion
+
+ #region SoundOutput Passthrough
+
+ ///
+ /// Indicate if the output is playing
+ ///
+ public bool? Playing
+ {
+ get => _playing;
+ private set => this.RaiseAndSetIfChanged(ref _playing, value);
+ }
+
+ ///
+ /// Indicates if de-emphasis should be applied
+ ///
+ public bool ApplyDeEmphasis
+ {
+ get => _applyDeEmphasis;
+ private set => this.RaiseAndSetIfChanged(ref _applyDeEmphasis, value);
+ }
+
+ ///
+ /// Current playback volume
+ ///
+ public int Volume
+ {
+ get => _volume;
+ private set => this.RaiseAndSetIfChanged(ref _volume, value);
+ }
+
+ private bool? _playing;
+ private bool _applyDeEmphasis;
+ private int _volume;
+
+ #endregion
+
+ #region Private State Variables
+
+ ///
+ /// Sound output handling class
+ ///
+ private readonly SoundOutput _soundOutput;
+
+ ///
+ /// OpticalDisc object
+ ///
+ private readonly OpticalDiscBase _opticalDisc;
+
+ ///
+ /// Last volume for mute toggling
+ ///
+ private int? _lastVolume = null;
+
+ #endregion
+
+ ///
+ /// Create a new 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]
+ /// 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)
+ {
+ // Set the internal state for initialization
+ Initialized = false;
+ _soundOutput = new SoundOutput();
+ _soundOutput.SetDeEmphasis(false);
+
+ // Initalize the disc
+ _opticalDisc = OpticalDiscFactory.GenerateFromPath(path, generateMissingToc, loadHiddenTracks, loadDataTracks, autoPlay);
+ if(_opticalDisc == null || !_opticalDisc.Initialized)
+ return;
+
+ // Add event handling for the optical disc
+ _opticalDisc.PropertyChanged += OpticalDiscStateChanged;
+
+ // Initialize the sound output
+ _soundOutput.Init(_opticalDisc, autoPlay, defaultVolume);
+ if(_soundOutput == null || !_soundOutput.Initialized)
+ return;
+
+ // Add event handling for the sound output
+ _soundOutput.PropertyChanged += SoundOutputStateChanged;
+
+ // Mark the player as ready
+ Initialized = true;
+
+ // Force a refresh of the state information
+ OpticalDiscStateChanged(this, null);
+ SoundOutputStateChanged(this, null);
+ }
+
+ #region Playback
+
+ ///
+ /// Begin playback
+ ///
+ public void Play()
+ {
+ if(_opticalDisc == null || !_opticalDisc.Initialized)
+ return;
+ else if(_soundOutput == null)
+ return;
+ else if(_soundOutput.Playing)
+ return;
+
+ _soundOutput.Play();
+ _opticalDisc.SetTotalIndexes();
+ Playing = true;
+ }
+
+ ///
+ /// Pause current playback
+ ///
+ public void Pause()
+ {
+ if(_opticalDisc == null || !_opticalDisc.Initialized)
+ return;
+ else if(_soundOutput == null)
+ return;
+ else if(!_soundOutput.Playing)
+ return;
+
+ _soundOutput?.Stop();
+ Playing = false;
+ }
+
+ ///
+ /// Toggle current playback
+ ///
+ public void TogglePlayback()
+ {
+ if(Playing == true)
+ Pause();
+ else
+ Play();
+ }
+
+ ///
+ /// Stop current playback
+ ///
+ public void Stop()
+ {
+ if(_opticalDisc == null || !_opticalDisc.Initialized)
+ return;
+ else if(_soundOutput == null)
+ return;
+ else if(!_soundOutput.Playing)
+ return;
+
+ _soundOutput?.Stop();
+ _opticalDisc.LoadFirstTrack();
+ Playing = null;
+ }
+
+ ///
+ /// Move to the next playable track
+ ///
+ public void NextTrack()
+ {
+ if(_opticalDisc == null || !_opticalDisc.Initialized)
+ return;
+
+ bool? wasPlaying = Playing;
+ if(wasPlaying == true) Pause();
+
+ _opticalDisc.NextTrack();
+ if(_opticalDisc is CompactDisc compactDisc)
+ _soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis);
+
+ if(wasPlaying == true) Play();
+ }
+
+ ///
+ /// Move to the previous playable track
+ ///
+ public void PreviousTrack()
+ {
+ if(_opticalDisc == null || !_opticalDisc.Initialized)
+ return;
+
+ bool? wasPlaying = Playing;
+ if(wasPlaying == true) Pause();
+
+ _opticalDisc.PreviousTrack();
+ if(_opticalDisc is CompactDisc compactDisc)
+ _soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis);
+
+ if(wasPlaying == true) Play();
+ }
+
+ ///
+ /// Move to the next index
+ ///
+ /// True if index changes can trigger a track change, false otherwise
+ public void NextIndex(bool changeTrack)
+ {
+ if(_opticalDisc == null || !_opticalDisc.Initialized)
+ return;
+
+ bool? wasPlaying = Playing;
+ if(wasPlaying == true) Pause();
+
+ _opticalDisc.NextIndex(changeTrack);
+ if(_opticalDisc is CompactDisc compactDisc)
+ _soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis);
+
+ if(wasPlaying == true) Play();
+ }
+
+ ///
+ /// Move to the previous index
+ ///
+ /// True if index changes can trigger a track change, false otherwise
+ public void PreviousIndex(bool changeTrack)
+ {
+ if(_opticalDisc == null || !_opticalDisc.Initialized)
+ return;
+
+ bool? wasPlaying = Playing;
+ if(wasPlaying == true) Pause();
+
+ _opticalDisc.PreviousIndex(changeTrack);
+ if(_opticalDisc is CompactDisc compactDisc)
+ _soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis);
+
+ if(wasPlaying == true) Play();
+ }
+
+ ///
+ /// Fast-forward playback by 75 sectors, if possible
+ ///
+ public void FastForward()
+ {
+ if(_opticalDisc == null || !_opticalDisc.Initialized)
+ return;
+
+ _opticalDisc.SetCurrentSector(Math.Min(_opticalDisc.TotalSectors, _opticalDisc.CurrentSector + 75));
+ }
+
+ ///
+ /// Rewind playback by 75 sectors, if possible
+ ///
+ public void Rewind()
+ {
+ if(_opticalDisc == null || !_opticalDisc.Initialized)
+ return;
+
+ if(_opticalDisc.CurrentSector >= 75)
+ _opticalDisc.SetCurrentSector(_opticalDisc.CurrentSector - 75);
+ }
+
+ #endregion
+
+ #region Volume
+
+ ///
+ /// Increment the volume value
+ ///
+ public void VolumeUp() => SetVolume(Volume + 1);
+
+ ///
+ /// Decrement the volume value
+ ///
+ public void VolumeDown() => SetVolume(Volume + 1);
+
+ ///
+ /// Set the value for the volume
+ ///
+ /// New volume value
+ public void SetVolume(int volume) => _soundOutput?.SetVolume(volume);
+
+ ///
+ /// Temporarily mute playback
+ ///
+ public void ToggleMute()
+ {
+ if(_lastVolume == null)
+ {
+ _lastVolume = Volume;
+ SetVolume(0);
+ }
+ else
+ {
+ SetVolume(_lastVolume.Value);
+ _lastVolume = null;
+ }
+ }
+
+ #endregion
+
+ #region Emphasis
+
+ ///
+ /// Enable de-emphasis
+ ///
+ public void EnableDeEmphasis() => SetDeEmphasis(true);
+
+ ///
+ /// Disable de-emphasis
+ ///
+ public void DisableDeEmphasis() => SetDeEmphasis(false);
+
+ ///
+ /// Toggle de-emphasis
+ ///
+ public void ToggleDeEmphasis() => SetDeEmphasis(!ApplyDeEmphasis);
+
+ ///
+ /// Set de-emphasis status
+ ///
+ ///
+ private void SetDeEmphasis(bool apply) => _soundOutput?.SetDeEmphasis(apply);
+
+ #endregion
+
+ #region Helpers
+
+ ///
+ /// Set the value for loading data tracks [CompactDisc only]
+ ///
+ /// True to enable loading data tracks, false otherwise
+ public void SetLoadDataTracks(bool load)
+ {
+ if(_opticalDisc is CompactDisc compactDisc)
+ compactDisc.LoadDataTracks = load;
+ }
+
+ ///
+ /// Set the value for loading hidden tracks [CompactDisc only]
+ ///
+ /// True to enable loading hidden tracks, false otherwise
+ public void SetLoadHiddenTracks(bool load)
+ {
+ if(_opticalDisc is CompactDisc compactDisc)
+ compactDisc.LoadHiddenTracks = load;
+ }
+
+ ///
+ /// Update the player from the current OpticalDisc
+ ///
+ private void OpticalDiscStateChanged(object sender, PropertyChangedEventArgs e)
+ {
+ CurrentTrackNumber = _opticalDisc.CurrentTrackNumber;
+ CurrentTrackIndex = _opticalDisc.CurrentTrackIndex;
+ CurrentSector = _opticalDisc.CurrentSector;
+ SectionStartSector = _opticalDisc.SectionStartSector;
+
+ HiddenTrack = TimeOffset > 150;
+
+ if(_opticalDisc is CompactDisc compactDisc)
+ {
+ QuadChannel = compactDisc.QuadChannel;
+ IsDataTrack = compactDisc.IsDataTrack;
+ CopyAllowed = compactDisc.CopyAllowed;
+ TrackHasEmphasis = compactDisc.TrackHasEmphasis;
+ }
+ else
+ {
+ QuadChannel = false;
+ IsDataTrack = _opticalDisc.TrackType != TrackType.Audio;
+ CopyAllowed = false;
+ TrackHasEmphasis = false;
+ }
+ }
+
+ ///
+ /// Update the player from the current SoundOutput
+ ///
+ private void SoundOutputStateChanged(object sender, PropertyChangedEventArgs e)
+ {
+ Playing = _soundOutput.Playing;
+ ApplyDeEmphasis = _soundOutput.ApplyDeEmphasis;
+ Volume = _soundOutput.Volume;
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/RedBookPlayer/Hardware/PlayerSource.cs b/RedBookPlayer.Models/Hardware/PlayerSource.cs
similarity index 96%
rename from RedBookPlayer/Hardware/PlayerSource.cs
rename to RedBookPlayer.Models/Hardware/PlayerSource.cs
index 17b4a5c..203ff4f 100644
--- a/RedBookPlayer/Hardware/PlayerSource.cs
+++ b/RedBookPlayer.Models/Hardware/PlayerSource.cs
@@ -2,7 +2,7 @@ using System;
using CSCore;
using WaveFormat = CSCore.WaveFormat;
-namespace RedBookPlayer.Hardware
+namespace RedBookPlayer.Models.Hardware
{
public class PlayerSource : IWaveSource
{
diff --git a/RedBookPlayer/Hardware/SoundOutput.cs b/RedBookPlayer.Models/Hardware/SoundOutput.cs
similarity index 68%
rename from RedBookPlayer/Hardware/SoundOutput.cs
rename to RedBookPlayer.Models/Hardware/SoundOutput.cs
index d31b68c..743c35b 100644
--- a/RedBookPlayer/Hardware/SoundOutput.cs
+++ b/RedBookPlayer.Models/Hardware/SoundOutput.cs
@@ -4,11 +4,12 @@ using System.Threading.Tasks;
using CSCore.SoundOut;
using NWaves.Audio;
using NWaves.Filters.BiQuad;
-using RedBookPlayer.Discs;
+using ReactiveUI;
+using RedBookPlayer.Models.Discs;
-namespace RedBookPlayer.Hardware
+namespace RedBookPlayer.Models.Hardware
{
- public class SoundOutput
+ public class SoundOutput : ReactiveObject
{
#region Public Fields
@@ -17,15 +18,23 @@ namespace RedBookPlayer.Hardware
///
public bool Initialized { get; private set; } = false;
- ///
- /// Indicates if de-emphasis should be applied
- ///
- public bool ApplyDeEmphasis { get; set; } = false;
-
///
/// Indicate if the output is playing
///
- public bool Playing => _soundOut.PlaybackState == PlaybackState.Playing;
+ public bool Playing
+ {
+ get => _playing;
+ private set => this.RaiseAndSetIfChanged(ref _playing, value);
+ }
+
+ ///
+ /// Indicates if de-emphasis should be applied
+ ///
+ public bool ApplyDeEmphasis
+ {
+ get => _applyDeEmphasis;
+ private set => this.RaiseAndSetIfChanged(ref _applyDeEmphasis, value);
+ }
///
/// Current playback volume
@@ -33,38 +42,33 @@ namespace RedBookPlayer.Hardware
public int Volume
{
get => _volume;
- set
+ private set
{
+ int tempVolume = value;
if(value > 100)
- _volume = 100;
+ tempVolume = 100;
else if(value < 0)
- _volume = 0;
- else
- _volume = value;
+ tempVolume = 0;
+
+ this.RaiseAndSetIfChanged(ref _volume, tempVolume);
}
}
+ private bool _playing;
+ private bool _applyDeEmphasis;
+ private int _volume;
+
#endregion
#region Private State Variables
- ///
- /// Current position in the sector
- ///
- private int _currentSectorReadPosition = 0;
-
///
/// OpticalDisc from the parent player for easy access
///
///
/// TODO: Can we remove the need for a local reference to OpticalDisc?
///
- private OpticalDisc _opticalDisc;
-
- ///
- /// Internal value for the volume
- ///
- private int _volume;
+ private OpticalDiscBase _opticalDisc;
///
/// Data provider for sound output
@@ -86,6 +90,11 @@ namespace RedBookPlayer.Hardware
///
private BiQuadFilter _deEmphasisFilterRight;
+ ///
+ /// Current position in the sector
+ ///
+ private int _currentSectorReadPosition = 0;
+
///
/// Lock object for reading track data
///
@@ -99,7 +108,7 @@ namespace RedBookPlayer.Hardware
/// 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(OpticalDisc opticalDisc, bool autoPlay = false, int defaultVolume = 100)
+ public void Init(OpticalDiscBase opticalDisc, bool autoPlay = false, int defaultVolume = 100)
{
// If we have an unusable disc, just return
if(opticalDisc == null || !opticalDisc.Initialized)
@@ -144,32 +153,15 @@ namespace RedBookPlayer.Hardware
// Set the current volume
_soundOut.Volume = (float)Volume / 100;
- // Determine how many sectors we can read
- ulong sectorsToRead;
- ulong zeroSectorsAmount;
- do
+ // If we have an unreadable track, just return
+ if(_opticalDisc.BytesPerSector <= 0)
{
- // Attempt to read 2 more sectors than requested
- sectorsToRead = ((ulong)count / (ulong)_opticalDisc.BytesPerSector) + 2;
- zeroSectorsAmount = 0;
+ Array.Clear(buffer, offset, count);
+ return count;
+ }
- // 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);
- }
-
- // TODO: Figure out when this value could be negative
- if(sectorsToRead <= 0)
- {
- _opticalDisc.LoadFirstTrack();
- _currentSectorReadPosition = 0;
- }
- } while(sectorsToRead <= 0);
+ // 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];
@@ -220,20 +212,7 @@ namespace RedBookPlayer.Hardware
// Apply de-emphasis filtering, only if enabled
if(ApplyDeEmphasis)
- {
- float[][] floatAudioData = new float[2][];
- floatAudioData[0] = new float[audioDataSegment.Length / 4];
- floatAudioData[1] = new float[audioDataSegment.Length / 4];
- ByteConverter.ToFloats16Bit(audioDataSegment, floatAudioData);
-
- for(int i = 0; i < floatAudioData[0].Length; i++)
- {
- floatAudioData[0][i] = _deEmphasisFilterLeft.Process(floatAudioData[0][i]);
- floatAudioData[1][i] = _deEmphasisFilterRight.Process(floatAudioData[1][i]);
- }
-
- ByteConverter.FromFloats16Bit(floatAudioData, audioDataSegment);
- }
+ ProcessDeEmphasis(audioDataSegment);
// Write out the audio data to the buffer
Array.Copy(audioDataSegment, 0, buffer, offset, count);
@@ -242,7 +221,7 @@ namespace RedBookPlayer.Hardware
_currentSectorReadPosition += count;
if(_currentSectorReadPosition >= _opticalDisc.BytesPerSector)
{
- _opticalDisc.CurrentSector += (ulong)(_currentSectorReadPosition / _opticalDisc.BytesPerSector);
+ _opticalDisc.SetCurrentSector(_opticalDisc.CurrentSector + (ulong)(_currentSectorReadPosition / _opticalDisc.BytesPerSector));
_currentSectorReadPosition %= _opticalDisc.BytesPerSector;
}
@@ -254,22 +233,105 @@ namespace RedBookPlayer.Hardware
///
/// Start audio playback
///
- public void Play() => _soundOut.Play();
+ public void Play()
+ {
+ if (_soundOut.PlaybackState != PlaybackState.Playing)
+ _soundOut.Play();
+
+ Playing = _soundOut.PlaybackState == PlaybackState.Playing;
+ }
+
+ ///
+ /// Pause audio playback
+ ///
+ public void Pause()
+ {
+ if(_soundOut.PlaybackState != PlaybackState.Paused)
+ _soundOut.Pause();
+
+ Playing = _soundOut.PlaybackState == PlaybackState.Playing;
+ }
///
/// Stop audio playback
///
- public void Stop() => _soundOut.Stop();
+ public void Stop()
+ {
+ if(_soundOut.PlaybackState != PlaybackState.Stopped)
+ _soundOut.Stop();
+
+ Playing = _soundOut.PlaybackState == PlaybackState.Playing;
+ }
#endregion
#region Helpers
///
- /// Toggle de-emphasis processing
+ /// Set de-emphasis status
///
- /// True to apply de-emphasis, false otherwise
- public void SetDeEmphasis(bool enable) => ApplyDeEmphasis = enable;
+ ///
+ public void SetDeEmphasis(bool apply) => ApplyDeEmphasis = apply;
+
+ ///
+ /// Set the value for the volume
+ ///
+ /// New volume value
+ public void SetVolume(int volume) => Volume = volume;
+
+ ///
+ /// Determine the number of real and zero sectors to read
+ ///
+ /// Number of requested bytes to read
+ /// Number of sectors to read
+ /// Number of zeroed sectors to concatenate
+ private void DetermineReadAmount(int count, out ulong sectorsToRead, out ulong zeroSectorsAmount)
+ {
+ do
+ {
+ // Attempt to read 2 more sectors than requested
+ sectorsToRead = ((ulong)count / (ulong)_opticalDisc.BytesPerSector) + 2;
+ zeroSectorsAmount = 0;
+
+ // Avoid overreads by padding with 0-byte data at the end
+ if(_opticalDisc.CurrentSector + sectorsToRead > _opticalDisc.TotalSectors)
+ {
+ ulong oldSectorsToRead = sectorsToRead;
+ sectorsToRead = _opticalDisc.TotalSectors - _opticalDisc.CurrentSector;
+
+ int tempZeroSectorCount = (int)(oldSectorsToRead - sectorsToRead);
+ zeroSectorsAmount = (ulong)(tempZeroSectorCount < 0 ? 0 : tempZeroSectorCount);
+ }
+
+ // 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);
+ }
+
+ ///
+ /// Process de-emphasis of audio data
+ ///
+ /// Audio data to process
+ private void ProcessDeEmphasis(byte[] audioData)
+ {
+ float[][] floatAudioData = new float[2][];
+ floatAudioData[0] = new float[audioData.Length / 4];
+ floatAudioData[1] = new float[audioData.Length / 4];
+ ByteConverter.ToFloats16Bit(audioData, floatAudioData);
+
+ for(int i = 0; i < floatAudioData[0].Length; i++)
+ {
+ floatAudioData[0][i] = _deEmphasisFilterLeft.Process(floatAudioData[0][i]);
+ floatAudioData[1][i] = _deEmphasisFilterRight.Process(floatAudioData[1][i]);
+ }
+
+ ByteConverter.FromFloats16Bit(floatAudioData, audioData);
+ }
///
/// Sets or resets the de-emphasis filters
diff --git a/RedBookPlayer.Models/RedBookPlayer.Models.csproj b/RedBookPlayer.Models/RedBookPlayer.Models.csproj
new file mode 100644
index 0000000..bfbe9f2
--- /dev/null
+++ b/RedBookPlayer.Models/RedBookPlayer.Models.csproj
@@ -0,0 +1,19 @@
+
+
+
+ netcoreapp3.1
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/RedBookPlayer.Models/nuget.config b/RedBookPlayer.Models/nuget.config
new file mode 100644
index 0000000..7c07e22
--- /dev/null
+++ b/RedBookPlayer.Models/nuget.config
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/RedBookPlayer.sln b/RedBookPlayer.sln
index a622fe1..3b35228 100644
--- a/RedBookPlayer.sln
+++ b/RedBookPlayer.sln
@@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.31321.278
MinimumVisualStudioVersion = 15.0.26124.0
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RedBookPlayer", "RedBookPlayer\RedBookPlayer.csproj", "{94944959-0352-4ABF-9C5C-19FF33747ECE}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RedBookPlayer.GUI", "RedBookPlayer.GUI\RedBookPlayer.GUI.csproj", "{94944959-0352-4ABF-9C5C-19FF33747ECE}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "cscore", "cscore", "{9A371299-4C59-4E46-9C3B-4FE024017491}"
EndProject
@@ -40,6 +40,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
README.md = README.md
EndProjectSection
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RedBookPlayer.Models", "RedBookPlayer.Models\RedBookPlayer.Models.csproj", "{462A3B8E-A5D4-4539-8469-1647B47AB2A8}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -194,6 +196,18 @@ Global
{ED8E11B7-786F-4EFF-9E4C-B937B7A2DE89}.Release|x64.Build.0 = Release|Any CPU
{ED8E11B7-786F-4EFF-9E4C-B937B7A2DE89}.Release|x86.ActiveCfg = Release|Any CPU
{ED8E11B7-786F-4EFF-9E4C-B937B7A2DE89}.Release|x86.Build.0 = Release|Any CPU
+ {462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Debug|x64.Build.0 = Debug|Any CPU
+ {462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Debug|x86.Build.0 = Debug|Any CPU
+ {462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Release|x64.ActiveCfg = Release|Any CPU
+ {462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Release|x64.Build.0 = Release|Any CPU
+ {462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Release|x86.ActiveCfg = Release|Any CPU
+ {462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/RedBookPlayer/GUI/PlayerView.xaml.cs b/RedBookPlayer/GUI/PlayerView.xaml.cs
deleted file mode 100644
index 7dabeae..0000000
--- a/RedBookPlayer/GUI/PlayerView.xaml.cs
+++ /dev/null
@@ -1,248 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading.Tasks;
-using System.Timers;
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Interactivity;
-using Avalonia.Markup.Xaml;
-using Avalonia.Media.Imaging;
-using Avalonia.Platform;
-using Avalonia.Threading;
-
-namespace RedBookPlayer.GUI
-{
- public class PlayerView : UserControl
- {
- ///
- /// Read-only access to the view model
- ///
- public PlayerViewModel PlayerViewModel => DataContext as PlayerViewModel;
-
- ///
- /// Set of images representing the digits for the UI
- ///
- ///
- /// TODO: Does it make sense to have this as an array?
- ///
- private Image[] _digits;
-
- ///
- /// Timer for performing UI updates
- ///
- private Timer _updateTimer;
-
- public PlayerView() => InitializeComponent(null);
-
- public PlayerView(string xaml) => InitializeComponent(xaml);
-
- #region Helpers
-
- ///
- /// Generate a path selection dialog box
- ///
- /// User-selected path, if possible
- public async Task GetPath()
- {
- var dialog = new OpenFileDialog { AllowMultiple = false };
- List knownExtensions = new Aaru.DiscImages.AaruFormat().KnownExtensions.ToList();
- dialog.Filters.Add(new FileDialogFilter()
- {
- Name = "Aaru Image Format (*" + string.Join(", *", knownExtensions) + ")",
- Extensions = knownExtensions.ConvertAll(e => e.TrimStart('.'))
- });
-
- return (await dialog.ShowAsync((Window)Parent.Parent))?.FirstOrDefault();
- }
-
- ///
- /// Load an image from the path
- ///
- /// Path to the image to load
- public async Task LoadImage(string path)
- {
- bool result = await Dispatcher.UIThread.InvokeAsync(() =>
- {
- PlayerViewModel.Init(path, App.Settings.AutoPlay, App.Settings.Volume);
- return PlayerViewModel.Initialized;
- });
-
- if(result)
- {
- await Dispatcher.UIThread.InvokeAsync(() =>
- {
- MainWindow.Instance.Title = "RedBookPlayer - " + path.Split('/').Last().Split('\\').Last();
- });
- }
-
- return result;
- }
-
- ///
- /// Load the png image for a given character based on the theme
- ///
- /// Character to load the image for
- /// Bitmap representing the loaded image
- ///
- /// TODO: Currently assumes that an image must always exist
- ///
- private Bitmap GetBitmap(char character)
- {
- 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);
- }
- }
-
- ///
- /// Initialize the UI based on the currently selected theme
- ///
- /// XAML data representing the theme, null for default
- private void InitializeComponent(string xaml)
- {
- DataContext = new PlayerViewModel();
-
- // Load the theme
- try
- {
- if(xaml != null)
- new AvaloniaXamlLoader().Load(xaml, null, this);
- else
- AvaloniaXamlLoader.Load(this);
- }
- catch
- {
- AvaloniaXamlLoader.Load(this);
- }
-
- InitializeDigits();
-
- _updateTimer = new Timer(1000 / 60);
-
- _updateTimer.Elapsed += (sender, e) =>
- {
- try
- {
- UpdateView(sender, e);
- }
- catch(Exception ex)
- {
- Console.WriteLine(ex);
- }
- };
-
- _updateTimer.AutoReset = true;
- _updateTimer.Start();
- }
-
- ///
- /// Initialize the displayed digits array
- ///
- private void InitializeDigits()
- {
- _digits = new Image[]
- {
- this.FindControl("TrackDigit1"),
- this.FindControl("TrackDigit2"),
-
- this.FindControl("IndexDigit1"),
- this.FindControl("IndexDigit2"),
-
- this.FindControl("TimeDigit1"),
- this.FindControl("TimeDigit2"),
- this.FindControl("TimeDigit3"),
- this.FindControl("TimeDigit4"),
- this.FindControl("TimeDigit5"),
- this.FindControl("TimeDigit6"),
-
- this.FindControl("TotalTracksDigit1"),
- this.FindControl("TotalTracksDigit2"),
-
- this.FindControl("TotalIndexesDigit1"),
- this.FindControl("TotalIndexesDigit2"),
-
- this.FindControl("TotalTimeDigit1"),
- this.FindControl("TotalTimeDigit2"),
- this.FindControl("TotalTimeDigit3"),
- this.FindControl("TotalTimeDigit4"),
- this.FindControl("TotalTimeDigit5"),
- this.FindControl("TotalTimeDigit6"),
- };
- }
-
- ///
- /// Update the UI with the most recent information from the Player
- ///
- private void UpdateView(object sender, ElapsedEventArgs e)
- {
- Dispatcher.UIThread.InvokeAsync(() =>
- {
- string digitString = PlayerViewModel.GenerateDigitString();
- for (int i = 0; i < _digits.Length; i++)
- {
- if (_digits[i] != null)
- _digits[i].Source = GetBitmap(digitString[i]);
- }
-
- PlayerViewModel?.UpdateView();
- });
- }
-
- #endregion
-
- #region Event Handlers
-
- public async void LoadButton_Click(object sender, RoutedEventArgs e)
- {
- string path = await GetPath();
- if (path == null)
- return;
-
- await LoadImage(path);
- }
-
- public void PlayButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.Playing = true;
-
- public void PauseButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.Playing = false;
-
- public void PlayPauseButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.Playing = !(PlayerViewModel.Playing ?? false);
-
- public void StopButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.Playing = null;
-
- public void NextTrackButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.NextTrack();
-
- public void PreviousTrackButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.PreviousTrack();
-
- public void NextIndexButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.NextIndex(App.Settings.IndexButtonChangeTrack);
-
- public void PreviousIndexButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.PreviousIndex(App.Settings.IndexButtonChangeTrack);
-
- public void FastForwardButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.FastForward();
-
- public void RewindButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.Rewind();
-
- public void VolumeUpButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.Volume++;
-
- public void VolumeDownButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.Volume--;
-
- public void MuteToggleButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.ToggleMute();
-
- public void EnableDeEmphasisButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.ApplyDeEmphasis = true;
-
- public void DisableDeEmphasisButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.ApplyDeEmphasis = false;
-
- public void EnableDisableDeEmphasisButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.ApplyDeEmphasis = !PlayerViewModel.ApplyDeEmphasis;
-
- #endregion
- }
-}
\ No newline at end of file
diff --git a/RedBookPlayer/GUI/PlayerViewModel.cs b/RedBookPlayer/GUI/PlayerViewModel.cs
deleted file mode 100644
index 4030ba5..0000000
--- a/RedBookPlayer/GUI/PlayerViewModel.cs
+++ /dev/null
@@ -1,270 +0,0 @@
-using System.Linq;
-using Aaru.CommonTypes.Enums;
-using ReactiveUI;
-using RedBookPlayer.Discs;
-using RedBookPlayer.Hardware;
-
-namespace RedBookPlayer.GUI
-{
- public class PlayerViewModel : ReactiveObject
- {
- ///
- /// Player representing the internal state
- ///
- private Player _player;
-
- ///
- /// Last volume for mute toggling
- ///
- private int? _lastVolume = null;
-
- #region Player Status
-
- ///
- /// Indicate if the model is ready to be used
- ///
- public bool Initialized => _player?.Initialized ?? false;
-
- ///
- /// Indicate the player state
- ///
- public bool? Playing
- {
- get => _player?.Playing ?? false;
- set
- {
- if(_player != null)
- _player.Playing = value;
- }
- }
-
- ///
- /// Indicate the current playback volume
- ///
- public int Volume
- {
- get => _player?.Volume ?? 100;
- set
- {
- if(_player != null)
- _player.Volume = value;
- }
- }
-
- ///
- /// Indicates if de-emphasis should be applied
- ///
- public bool ApplyDeEmphasis
- {
- get => _player?.ApplyDeEmphasis ?? false;
- set
- {
- if(_player != null)
- _player.ApplyDeEmphasis = value;
- }
- }
-
- #endregion
-
- #region Model-Provided Playback Information
-
- private ulong _currentSector;
- public ulong CurrentSector
- {
- get => _currentSector;
- set => this.RaiseAndSetIfChanged(ref _currentSector, value);
- }
-
- public int CurrentFrame => (int)(_currentSector / (75 * 60));
- public int CurrentSecond => (int)(_currentSector / 75 % 60);
- public int CurrentMinute => (int)(_currentSector % 75);
-
- private ulong _totalSectors;
- public ulong TotalSectors
- {
- get => _totalSectors;
- set => this.RaiseAndSetIfChanged(ref _totalSectors, value);
- }
-
- public int TotalFrames => (int)(_totalSectors / (75 * 60));
- public int TotalSeconds => (int)(_totalSectors / 75 % 60);
- public int TotalMinutes => (int)(_totalSectors % 75);
-
- #endregion
-
- #region Disc Flags
-
- private bool _quadChannel;
- public bool QuadChannel
- {
- get => _quadChannel;
- set => this.RaiseAndSetIfChanged(ref _quadChannel, value);
- }
-
- private bool _isDataTrack;
- public bool IsDataTrack
- {
- get => _isDataTrack;
- set => this.RaiseAndSetIfChanged(ref _isDataTrack, value);
- }
-
- private bool _copyAllowed;
- public bool CopyAllowed
- {
- get => _copyAllowed;
- set => this.RaiseAndSetIfChanged(ref _copyAllowed, value);
- }
-
- private bool _trackHasEmphasis;
- public bool TrackHasEmphasis
- {
- get => _trackHasEmphasis;
- set => this.RaiseAndSetIfChanged(ref _trackHasEmphasis, value);
- }
-
- private bool _hiddenTrack;
- public bool HiddenTrack
- {
- get => _hiddenTrack;
- set => this.RaiseAndSetIfChanged(ref _hiddenTrack, value);
- }
-
- #endregion
-
- ///
- /// Initialize the view model with a given image path
- ///
- /// Path to the disc image
- /// 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 autoPlay, int defaultVolume)
- {
- // Stop current playback, if necessary
- if(Playing != null) Playing = null;
-
- // Create and attempt to initialize new Player
- _player = new Player(path, autoPlay, defaultVolume);
- if(Initialized)
- UpdateView();
- }
-
- #region Playback
-
- ///
- /// Move to the next playable track
- ///
- public void NextTrack() => _player?.NextTrack();
-
- ///
- /// Move to the previous playable track
- ///
- public void PreviousTrack() => _player?.PreviousTrack();
-
- ///
- /// Move to the next index
- ///
- /// True if index changes can trigger a track change, false otherwise
- public void NextIndex(bool changeTrack) => _player?.NextIndex(changeTrack);
-
- ///
- /// Move to the previous index
- ///
- /// True if index changes can trigger a track change, false otherwise
- public void PreviousIndex(bool changeTrack) => _player?.PreviousIndex(changeTrack);
-
- ///
- /// Fast-forward playback by 75 sectors, if possible
- ///
- public void FastForward() => _player?.FastForward();
-
- ///
- /// Rewind playback by 75 sectors, if possible
- ///
- public void Rewind() => _player?.Rewind();
-
- #endregion
-
- #region Helpers
-
- ///
- /// Generate the digit string to be interpreted by the frontend
- ///
- /// String representing the digits for the frontend
- public string GenerateDigitString()
- {
- // If the disc isn't initialized, return all '-' characters
- if(_player?.OpticalDisc == null || !_player.OpticalDisc.Initialized)
- return string.Empty.PadLeft(20, '-');
-
- // Otherwise, take the current time into account
- ulong sectorTime = _player.GetCurrentSectorTime();
-
- int[] numbers = new int[]
- {
- _player.OpticalDisc.CurrentTrackNumber + 1,
- _player.OpticalDisc.CurrentTrackIndex,
-
- (int)(sectorTime / (75 * 60)),
- (int)(sectorTime / 75 % 60),
- (int)(sectorTime % 75),
-
- _player.OpticalDisc.TotalTracks,
- _player.OpticalDisc.TotalIndexes,
-
- (int)(_player.OpticalDisc.TotalTime / (75 * 60)),
- (int)(_player.OpticalDisc.TotalTime / 75 % 60),
- (int)(_player.OpticalDisc.TotalTime % 75),
- };
-
- return string.Join("", numbers.Select(i => i.ToString().PadLeft(2, '0').Substring(0, 2)));
- }
-
- ///
- /// Temporarily mute playback
- ///
- public void ToggleMute()
- {
- if(_lastVolume == null)
- {
- _lastVolume = Volume;
- Volume = 0;
- }
- else
- {
- Volume = _lastVolume.Value;
- _lastVolume = null;
- }
- }
-
- ///
- /// Update the UI from the internal player
- ///
- public void UpdateView()
- {
- if(_player?.Initialized != true)
- return;
-
- CurrentSector = _player.GetCurrentSectorTime();
- TotalSectors = _player.OpticalDisc.TotalTime;
-
- HiddenTrack = _player.OpticalDisc.TimeOffset > 150;
-
- if(_player.OpticalDisc is CompactDisc compactDisc)
- {
- QuadChannel = compactDisc.QuadChannel;
- IsDataTrack = compactDisc.IsDataTrack;
- CopyAllowed = compactDisc.CopyAllowed;
- TrackHasEmphasis = compactDisc.TrackHasEmphasis;
- }
- else
- {
- QuadChannel = false;
- IsDataTrack = _player.OpticalDisc.TrackType != TrackType.Audio;
- CopyAllowed = false;
- TrackHasEmphasis = false;
- }
- }
-
- #endregion
- }
-}
\ No newline at end of file
diff --git a/RedBookPlayer/Hardware/Player.cs b/RedBookPlayer/Hardware/Player.cs
deleted file mode 100644
index 2850f89..0000000
--- a/RedBookPlayer/Hardware/Player.cs
+++ /dev/null
@@ -1,258 +0,0 @@
-using System;
-using System.IO;
-using Aaru.DiscImages;
-using Aaru.Filters;
-using RedBookPlayer.Discs;
-
-namespace RedBookPlayer.Hardware
-{
- public class Player
- {
- #region Public Fields
-
- ///
- /// Indicate if the player is ready to be used
- ///
- public bool Initialized { get; private set; } = false;
-
- ///
- /// OpticalDisc object
- ///
- public OpticalDisc OpticalDisc { get; private set; }
-
- ///
- /// Indicate if the disc is playing
- ///
- public bool? Playing
- {
- get => _soundOutput?.Playing;
- set
- {
- if(OpticalDisc == null || !OpticalDisc.Initialized)
- return;
-
- // If the playing state has not changed, do nothing
- if(value == _soundOutput?.Playing)
- return;
-
- if(value == true)
- {
- _soundOutput.Play();
- OpticalDisc.SetTotalIndexes();
- }
- else if(value == false)
- {
- _soundOutput.Stop();
- }
- else
- {
- _soundOutput.Stop();
- OpticalDisc.LoadFirstTrack();
- }
- }
- }
-
- ///
- /// Indicate the current playback volume
- ///
- public int Volume
- {
- get => _soundOutput?.Volume ?? 100;
- set
- {
- if(_soundOutput != null)
- _soundOutput.Volume = value;
- }
- }
-
- ///
- /// Indicates if de-emphasis should be applied
- ///
- public bool ApplyDeEmphasis
- {
- get => _soundOutput?.ApplyDeEmphasis ?? false;
- set => _soundOutput?.SetDeEmphasis(value);
- }
-
- #endregion
-
- #region Private State Variables
-
- ///
- /// Sound output handling class
- ///
- public SoundOutput _soundOutput;
-
- #endregion
-
- ///
- /// Create a new Player from a given image path
- ///
- /// Path to the disc image
- /// True if playback should begin immediately, false otherwise
- /// Default volume between 0 and 100 to use when starting playback
- public Player(string path, bool autoPlay = false, int defaultVolume = 100)
- {
- // Set the internal state for initialization
- Initialized = false;
- _soundOutput = new SoundOutput();
- _soundOutput.ApplyDeEmphasis = false;
- OpticalDisc = null;
-
- try
- {
- // Validate the image exists
- if(string.IsNullOrWhiteSpace(path) || !File.Exists(path))
- return;
-
- // Load the disc image to memory
- var image = new AaruFormat();
- var filter = new ZZZNoFilter();
- filter.Open(path);
- image.Open(filter);
-
- // Generate and instantiate the disc
- OpticalDisc = OpticalDiscFactory.GenerateFromImage(image, autoPlay);
- }
- catch
- {
- // All errors mean an invalid image in some way
- return;
- }
-
- // Initialize the sound output
- _soundOutput.Init(OpticalDisc, autoPlay, defaultVolume);
- if(_soundOutput == null || !_soundOutput.Initialized)
- return;
-
- // Mark the player as ready
- Initialized = true;
- }
-
- #region Playback
-
- ///
- /// Move to the next playable track
- ///
- public void NextTrack()
- {
- if(OpticalDisc == null || !OpticalDisc.Initialized)
- return;
-
- bool? wasPlaying = Playing;
- if(wasPlaying == true) Playing = false;
-
- OpticalDisc.NextTrack();
- if(OpticalDisc is CompactDisc compactDisc)
- _soundOutput.ApplyDeEmphasis = compactDisc.TrackHasEmphasis;
-
- if(wasPlaying == true) Playing = true;
- }
-
- ///
- /// Move to the previous playable track
- ///
- public void PreviousTrack()
- {
- if(OpticalDisc == null || !OpticalDisc.Initialized)
- return;
-
- bool? wasPlaying = Playing;
- if(wasPlaying == true) Playing = false;
-
- OpticalDisc.PreviousTrack();
- if(OpticalDisc is CompactDisc compactDisc)
- _soundOutput.ApplyDeEmphasis = compactDisc.TrackHasEmphasis;
-
- if(wasPlaying == true) Playing = true;
- }
-
- ///
- /// Move to the next index
- ///
- /// True if index changes can trigger a track change, false otherwise
- public void NextIndex(bool changeTrack)
- {
- if(OpticalDisc == null || !OpticalDisc.Initialized)
- return;
-
- bool? wasPlaying = Playing;
- if(wasPlaying == true) Playing = false;
-
- OpticalDisc.NextIndex(changeTrack);
- if(OpticalDisc is CompactDisc compactDisc)
- _soundOutput.ApplyDeEmphasis = compactDisc.TrackHasEmphasis;
-
- if(wasPlaying == true) Playing = true;
- }
-
- ///
- /// Move to the previous index
- ///
- /// True if index changes can trigger a track change, false otherwise
- public void PreviousIndex(bool changeTrack)
- {
- if(OpticalDisc == null || !OpticalDisc.Initialized)
- return;
-
- bool? wasPlaying = Playing;
- if(wasPlaying == true) Playing = false;
-
- OpticalDisc.PreviousIndex(changeTrack);
- if(OpticalDisc is CompactDisc compactDisc)
- _soundOutput.ApplyDeEmphasis = compactDisc.TrackHasEmphasis;
-
- if(wasPlaying == true) Playing = true;
- }
-
- ///
- /// Fast-forward playback by 75 sectors, if possible
- ///
- public void FastForward()
- {
- if(OpticalDisc == null || !OpticalDisc.Initialized)
- return;
-
- OpticalDisc.CurrentSector = Math.Min(OpticalDisc.TotalSectors, OpticalDisc.CurrentSector + 75);
- }
-
- ///
- /// Rewind playback by 75 sectors, if possible
- ///
- public void Rewind()
- {
- if(OpticalDisc == null || !OpticalDisc.Initialized)
- return;
-
- if(OpticalDisc.CurrentSector >= 75)
- OpticalDisc.CurrentSector -= 75;
- }
-
- #endregion
-
- #region Helpers
-
- ///
- /// Get current sector time, accounting for offsets
- ///
- /// ulong representing the current sector time
- public ulong GetCurrentSectorTime()
- {
- ulong sectorTime = OpticalDisc.CurrentSector;
- if (OpticalDisc.SectionStartSector != 0)
- sectorTime -= OpticalDisc.SectionStartSector;
- else
- sectorTime += OpticalDisc.TimeOffset;
-
- return sectorTime;
- }
-
- ///
- /// Set if de-emphasis should be applied
- ///
- /// True to enable, false to disable
- public void SetDeEmphasis(bool apply) => _soundOutput?.SetDeEmphasis(apply);
-
- #endregion
- }
-}
\ No newline at end of file