using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Reactive;
using System.Threading.Tasks;
using System.Xml;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using ReactiveUI;
using RedBookPlayer.Models;
using RedBookPlayer.Models.Discs;
using RedBookPlayer.Models.Hardware;
namespace RedBookPlayer.GUI.ViewModels
{
public class PlayerViewModel : ReactiveObject
{
///
/// Player representing the internal state
///
private readonly Player _player;
///
/// Set of images representing the digits for the UI
///
private Image[] _digits;
#region Player Passthrough
///
/// Currently selected disc
///
public int CurrentDisc
{
get => _currentDisc;
private set => this.RaiseAndSetIfChanged(ref _currentDisc, value);
}
private int _currentDisc;
#region OpticalDisc Passthrough
///
/// Path to the disc image
///
public string ImagePath
{
get => _imagePath;
private set => this.RaiseAndSetIfChanged(ref _imagePath, value);
}
///
/// 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 track session
///
public ushort CurrentTrackSession
{
get => _currentTrackSession;
private set => this.RaiseAndSetIfChanged(ref _currentTrackSession, 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 string _imagePath;
private int _currentTrackNumber;
private ushort _currentTrackIndex;
private ushort _currentTrackSession;
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
{
get => _initialized;
private set => this.RaiseAndSetIfChanged(ref _initialized, value);
}
///
/// Indicate if the output is playing
///
public PlayerState PlayerState
{
get => _playerState;
private set => this.RaiseAndSetIfChanged(ref _playerState, value);
}
///
/// Indicates how to handle playback of data tracks
///
public DataPlayback DataPlayback
{
get => _dataPlayback;
private set => this.RaiseAndSetIfChanged(ref _dataPlayback, value);
}
///
/// Indicates the repeat mode
///
public RepeatMode RepeatMode
{
get => _repeatMode;
private set => this.RaiseAndSetIfChanged(ref _repeatMode, 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 _initialized;
private PlayerState _playerState;
private DataPlayback _dataPlayback;
private RepeatMode _repeatMode;
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 ejecting the current disc
///
public ReactiveCommand EjectCommand { get; }
///
/// Command for moving to the next disc
///
public ReactiveCommand NextDiscCommand { get; }
///
/// Command for moving to the previous disc
///
public ReactiveCommand PreviousDiscCommand { 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()
{
// Initialize commands
LoadCommand = ReactiveCommand.Create(ExecuteLoad);
PlayCommand = ReactiveCommand.Create(ExecutePlay);
PauseCommand = ReactiveCommand.Create(ExecutePause);
TogglePlayPauseCommand = ReactiveCommand.Create(ExecuteTogglePlayPause);
StopCommand = ReactiveCommand.Create(ExecuteStop);
EjectCommand = ReactiveCommand.Create(ExecuteEject);
NextDiscCommand = ReactiveCommand.Create(ExecuteNextDisc);
PreviousDiscCommand = ReactiveCommand.Create(ExecutePreviousDisc);
NextTrackCommand = ReactiveCommand.Create(ExecuteNextTrack);
PreviousTrackCommand = ReactiveCommand.Create(ExecutePreviousTrack);
NextIndexCommand = ReactiveCommand.Create(ExecuteNextIndex);
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 Player
_player = new Player(App.Settings.NumberOfDiscs, App.Settings.Volume);
PlayerState = PlayerState.NoDisc;
}
///
/// Initialize the view model with a given image path
///
/// Path to the disc image
/// Options to pass to the player
/// Options to pass to the optical disc factory
/// True if playback should begin immediately, false otherwise
public void Init(string path, PlayerOptions playerOptions, OpticalDiscOptions opticalDiscOptions, bool autoPlay)
{
// Stop current playback, if necessary
if(PlayerState != PlayerState.NoDisc)
ExecuteStop();
// Attempt to initialize Player
_player.Init(path, playerOptions, opticalDiscOptions, autoPlay);
if(_player.Initialized)
{
_player.PropertyChanged += PlayerStateChanged;
PlayerStateChanged(this, null);
}
}
#region Playback (UI)
///
/// Begin playback
///
public void ExecutePlay() => _player?.Play();
///
/// Pause current playback
///
public void ExecutePause() => _player?.Pause();
///
/// Toggle playback
///
public void ExecuteTogglePlayPause() => _player?.TogglePlayback();
///
/// Shuffle the current track list
///
public void ExecuteShuffle() => _player?.ShuffleTracks();
///
/// Stop current playback
///
public void ExecuteStop() => _player?.Stop();
///
/// Eject the currently loaded disc
///
public void ExecuteEject() => _player?.Eject();
///
/// Move to the next disc
///
public void ExecuteNextDisc() => _player?.NextDisc();
///
/// Move to the previous disc
///
public void ExecutePreviousDisc() => _player?.PreviousDisc();
///
/// 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 Playback (Internal)
///
/// Select a particular disc by number
///
/// Disc number to attempt to load
public void SelectDisc(int discNumber) => _player?.SelectDisc(discNumber);
///
/// Select a particular index by number
///
/// Track index to attempt to load
/// True if index changes can trigger a track change, false otherwise
public void SelectIndex(ushort index, bool changeTrack) => _player?.SelectIndex(index, changeTrack);
///
/// Select a particular track by number
///
/// Track number to attempt to load
public void SelectTrack(int trackNumber) => _player?.SelectTrack(trackNumber);
#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 Extraction
///
/// Extract a single track from the image to WAV
///
///
/// Output path to write data to
public void ExtractSingleTrackToWav(uint trackNumber, string outputDirectory) => _player?.ExtractSingleTrackToWav(trackNumber, outputDirectory);
///
/// Extract all tracks from the image to WAV
///
/// Output path to write data to
public void ExtractAllTracksToWav(string outputDirectory) => _player?.ExtractAllTracksToWav(outputDirectory);
#endregion
#region Setters
///
/// Set data playback method [CompactDisc only]
///
/// New playback value
public void SetDataPlayback(DataPlayback dataPlayback) => _player?.SetDataPlayback(dataPlayback);
///
/// Set disc handling method
///
/// New playback value
public void SetDiscHandling(DiscHandling discHandling) => _player?.SetDiscHandling(discHandling);
///
/// 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);
///
/// Set repeat mode
///
/// New repeat mode value
public void SetRepeatMode(RepeatMode repeatMode) => _player?.SetRepeatMode(repeatMode);
///
/// Set session handling
///
/// New session handling value
public void SetSessionHandling(SessionHandling sessionHandling) => _player?.SetSessionHandling(sessionHandling);
#endregion
#region State Change Event Handlers
///
/// Update the view-model from the Player
///
private void PlayerStateChanged(object sender, PropertyChangedEventArgs e)
{
if(_player == null)
return;
if(!_player.Initialized)
{
Dispatcher.UIThread.InvokeAsync(() =>
{
App.MainWindow.Title = "RedBookPlayer";
});
}
ImagePath = _player.ImagePath;
Initialized = _player.Initialized;
if(!string.IsNullOrWhiteSpace(ImagePath) && Initialized)
{
Dispatcher.UIThread.InvokeAsync(() =>
{
App.MainWindow.Title = "RedBookPlayer - " + ImagePath.Split('/').Last().Split('\\').Last();
});
}
CurrentDisc = _player.CurrentDisc;
CurrentTrackNumber = _player.CurrentTrackNumber;
CurrentTrackIndex = _player.CurrentTrackIndex;
CurrentTrackSession = _player.CurrentTrackSession;
CurrentSector = _player.CurrentSector;
SectionStartSector = _player.SectionStartSector;
HiddenTrack = _player.HiddenTrack;
QuadChannel = _player.QuadChannel;
IsDataTrack = _player.IsDataTrack;
CopyAllowed = _player.CopyAllowed;
TrackHasEmphasis = _player.TrackHasEmphasis;
PlayerState = _player.PlayerState;
DataPlayback = _player.DataPlayback;
RepeatMode = _player.RepeatMode;
ApplyDeEmphasis = _player.ApplyDeEmphasis;
Volume = _player.Volume;
UpdateDigits();
}
#endregion
#region Helpers
///
/// Apply a custom theme to the player
///
/// Path to the theme under the themes directory
public void ApplyTheme(string theme)
{
// If the PlayerView isn't set, don't do anything
if(App.PlayerView == null)
return;
// If no theme path is provided, we can ignore
if(string.IsNullOrWhiteSpace(theme))
return;
string themeDirectory = $"{Directory.GetCurrentDirectory()}/themes/{theme}";
string xamlPath = $"{themeDirectory}/view.xaml";
if(!File.Exists(xamlPath))
{
Console.WriteLine("Warning: specified theme doesn't exist, reverting to default");
return;
}
try
{
string xaml = File.ReadAllText(xamlPath);
xaml = xaml.Replace("Source=\"", $"Source=\"file://{themeDirectory}/");
LoadTheme(xaml);
}
catch(XmlException ex)
{
Console.WriteLine($"Error: invalid theme XAML ({ex.Message}), reverting to default");
LoadTheme(null);
}
App.MainWindow.Width = App.PlayerView.Width;
App.MainWindow.Height = App.PlayerView.Height;
InitializeDigits();
}
///
/// Load a disc image from a selection box
///
public async void ExecuteLoad()
{
string[] paths = await GetPaths();
if(paths == null || paths.Length == 0)
{
return;
}
else if(paths.Length == 1)
{
await LoadImage(paths[0]);
}
else
{
int lastDisc = CurrentDisc;
foreach(string path in paths)
{
await LoadImage(path);
if(Initialized)
ExecuteNextDisc();
}
SelectDisc(lastDisc);
}
}
///
/// Initialize the displayed digits array
///
public void InitializeDigits()
{
if(App.PlayerView == null)
return;
_digits = new Image[]
{
App.PlayerView.FindControl("TrackDigit1"),
App.PlayerView.FindControl("TrackDigit2"),
App.PlayerView.FindControl("IndexDigit1"),
App.PlayerView.FindControl("IndexDigit2"),
App.PlayerView.FindControl("TimeDigit1"),
App.PlayerView.FindControl("TimeDigit2"),
App.PlayerView.FindControl("TimeDigit3"),
App.PlayerView.FindControl("TimeDigit4"),
App.PlayerView.FindControl("TimeDigit5"),
App.PlayerView.FindControl("TimeDigit6"),
App.PlayerView.FindControl("TotalTracksDigit1"),
App.PlayerView.FindControl("TotalTracksDigit2"),
App.PlayerView.FindControl("TotalIndexesDigit1"),
App.PlayerView.FindControl("TotalIndexesDigit2"),
App.PlayerView.FindControl("TotalTimeDigit1"),
App.PlayerView.FindControl("TotalTimeDigit2"),
App.PlayerView.FindControl("TotalTimeDigit3"),
App.PlayerView.FindControl("TotalTimeDigit4"),
App.PlayerView.FindControl("TotalTimeDigit5"),
App.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(() =>
{
PlayerOptions playerOptions = new PlayerOptions
{
DataPlayback = App.Settings.DataPlayback,
DiscHandling = App.Settings.DiscHandling,
LoadHiddenTracks = App.Settings.PlayHiddenTracks,
RepeatMode = App.Settings.RepeatMode,
SessionHandling = App.Settings.SessionHandling,
};
OpticalDiscOptions opticalDiscOptions = new OpticalDiscOptions
{
GenerateMissingToc = App.Settings.GenerateMissingTOC,
};
// Ensure the context and view model are set
App.PlayerView.DataContext = this;
App.PlayerView.ViewModel = this;
Init(path, playerOptions, opticalDiscOptions, App.Settings.AutoPlay);
if(Initialized)
App.MainWindow.Title = "RedBookPlayer - " + path.Split('/').Last().Split('\\').Last();
return Initialized;
});
}
///
/// Refresh the view model from the current settings
///
public void RefreshFromSettings()
{
SetDataPlayback(App.Settings.DataPlayback);
SetDiscHandling(App.Settings.DiscHandling);
SetLoadHiddenTracks(App.Settings.PlayHiddenTracks);
SetRepeatMode(App.Settings.RepeatMode);
SetSessionHandling(App.Settings.SessionHandling);
}
///
/// 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
{
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 paths, if possible
private async Task GetPaths()
{
return await Dispatcher.UIThread.InvokeAsync(async () =>
{
var dialog = new OpenFileDialog { AllowMultiple = true };
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(App.MainWindow));
});
}
///
/// Load the theme from a XAML, if possible
///
/// XAML data representing the theme, null for default
private void LoadTheme(string xaml)
{
// If the view is null, we can't load the theme
if(App.PlayerView == null)
return;
try
{
if(xaml != null)
new AvaloniaXamlLoader().Load(xaml, null, App.PlayerView);
else
AvaloniaXamlLoader.Load(App.PlayerView);
}
catch(Exception ex)
{
Console.Error.WriteLine(ex);
}
// Ensure the context and view model are set
App.PlayerView.DataContext = this;
App.PlayerView.ViewModel = this;
UpdateDigits();
}
///
/// Update UI
///
private void UpdateDigits()
{
// Ensure the digits
if(_digits == null)
InitializeDigits();
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
}
}