diff --git a/Aaru b/Aaru index b41b167..2a6903f 160000 --- a/Aaru +++ b/Aaru @@ -1 +1 @@ -Subproject commit b41b1679117927df188b2f14bfaa5c2190af05d1 +Subproject commit 2a6903f866a29b0c858d37120cfb1c725ff17980 diff --git a/RedBookPlayer.sln b/RedBookPlayer.sln index a4fee71..a622fe1 100644 --- a/RedBookPlayer.sln +++ b/RedBookPlayer.sln @@ -33,6 +33,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CUETools.Codecs", "Aaru\cue EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CUETools.Codecs.Flake", "Aaru\cuetools.net\CUETools.Codecs.Flake\CUETools.Codecs.Flake.csproj", "{ED8E11B7-786F-4EFF-9E4C-B937B7A2DE89}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0531C157-8111-4BC9-8C65-A2FDDB0C96FD}" + ProjectSection(SolutionItems) = preProject + build.bat = build.bat + build.sh = build.sh + README.md = README.md + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/RedBookPlayer/App.xaml.cs b/RedBookPlayer/App.xaml.cs index 43a5898..03c77fd 100644 --- a/RedBookPlayer/App.xaml.cs +++ b/RedBookPlayer/App.xaml.cs @@ -5,6 +5,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; +using RedBookPlayer.GUI; namespace RedBookPlayer { diff --git a/RedBookPlayer/Discs/CompactDisc.cs b/RedBookPlayer/Discs/CompactDisc.cs new file mode 100644 index 0000000..87b670a --- /dev/null +++ b/RedBookPlayer/Discs/CompactDisc.cs @@ -0,0 +1,406 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Interfaces; +using Aaru.CommonTypes.Structs; +using Aaru.Decoders.CD; +using Aaru.Helpers; +using static Aaru.Decoders.CD.FullTOC; + +namespace RedBookPlayer.Discs +{ + public class CompactDisc : OpticalDisc + { + #region Public Fields + + /// + public override int CurrentTrackNumber + { + get => _currentTrackNumber; + protected set + { + // Unset image means we can't do anything + if(_image == null) + return; + + // Cache the value and the current track number + int cachedValue = value; + int cachedTrackNumber = _currentTrackNumber; + + // 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; + + _currentTrackNumber = cachedValue; + + // Cache the current track for easy access + Track track = _image.Tracks[_currentTrackNumber]; + + // 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 we're not playing the track, skip + if(increment) + cachedValue++; + else + cachedValue--; + } + while(cachedValue != cachedTrackNumber); + } + } + + /// + public override ushort CurrentTrackIndex + { + get => _currentTrackIndex; + 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]; + + // Ensure that the value is valid, wrapping around if necessary + if(value > track.Indexes.Keys.Max()) + _currentTrackIndex = 0; + else if(value < 0) + _currentTrackIndex = track.Indexes.Keys.Max(); + else + _currentTrackIndex = value; + + // Set new index-specific data + SectionStartSector = (ulong)track.Indexes[CurrentTrackIndex]; + TotalTime = track.TrackEndSector - track.TrackStartSector; + } + } + + /// + public override ulong CurrentSector + { + get => _currentSector; + 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]; + + _currentSector = value; + + if((CurrentTrackNumber < _image.Tracks.Count - 1 && CurrentSector >= _image.Tracks[CurrentTrackNumber + 1].TrackStartSector) + || (CurrentTrackNumber > 0 && CurrentSector < track.TrackStartSector)) + { + foreach(Track trackData in _image.Tracks.ToArray().Reverse()) + { + if(CurrentSector >= trackData.TrackStartSector) + { + CurrentTrackNumber = (int)trackData.TrackSequence - 1; + break; + } + } + } + + foreach((ushort key, int i) in track.Indexes.Reverse()) + { + if((int)CurrentSector >= i) + { + CurrentTrackIndex = key; + return; + } + } + + CurrentTrackIndex = 0; + } + } + + /// + /// Represents the 4CH flag + /// + public bool QuadChannel { get; private set; } = false; + + /// + /// Represents the DATA flag + /// + public bool IsDataTrack => TrackType != TrackType.Audio; + + /// + /// Represents the DCP flag + /// + public bool CopyAllowed { get; private set; } = false; + + /// + /// Represents the PRE flag + /// + public bool TrackHasEmphasis { get; private set; } = false; + + #endregion + + #region Private State Variables + + /// + /// Current track number + /// + private int _currentTrackNumber = 0; + + /// + /// Current track index + /// + private ushort _currentTrackIndex = 0; + + /// + /// Current sector number + /// + private ulong _currentSector = 0; + + /// + /// Current disc table of contents + /// + private CDFullTOC _toc; + + #endregion + + /// + public override void Init(IOpticalMediaImage image, bool autoPlay = false) + { + // If the image is null, we can't do anything + if(image == null) + return; + + // Set the current disc image + _image = image; + + // Attempt to load the TOC + if(!LoadTOC()) + return; + + // Load the first track + LoadFirstTrack(); + + // Reset total indexes if not in autoplay + if(!autoPlay) + TotalIndexes = 0; + + // Set the internal disc state + TotalTracks = _image.Tracks.Count; + 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; + + // Mark the disc as ready + Initialized = true; + } + + #region Seeking + + /// + public override bool NextIndex(bool changeTrack) + { + if(_image == null) + return false; + + if(CurrentTrackIndex + 1 > _image.Tracks[CurrentTrackNumber].Indexes.Keys.Max()) + { + if(changeTrack) + { + NextTrack(); + CurrentSector = (ulong)_image.Tracks[CurrentTrackNumber].Indexes.Values.Min(); + return true; + } + } + else + { + CurrentSector = (ulong)_image.Tracks[CurrentTrackNumber].Indexes[++CurrentTrackIndex]; + } + + return false; + } + + /// + public override bool PreviousIndex(bool changeTrack) + { + if(_image == null) + return false; + + if(CurrentTrackIndex - 1 < _image.Tracks[CurrentTrackNumber].Indexes.Keys.Min()) + { + if(changeTrack) + { + PreviousTrack(); + CurrentSector = (ulong)_image.Tracks[CurrentTrackNumber].Indexes.Values.Max(); + return true; + } + } + else + { + CurrentSector = (ulong)_image.Tracks[CurrentTrackNumber].Indexes[--CurrentTrackIndex]; + } + + return false; + } + + #endregion + + #region Helpers + + /// + public override void LoadFirstTrack() + { + CurrentTrackNumber = 0; + LoadTrack(CurrentTrackNumber); + } + + /// + public override void SetTotalIndexes() + { + if(_image == null) + return; + + TotalIndexes = _image.Tracks[CurrentTrackNumber].Indexes.Keys.Max(); + } + + /// + protected override void LoadTrack(int track) + { + if(_image == null) + return; + + if(track < 0 || track >= _image.Tracks.Count) + 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]); + } + + /// + /// Load TOC for the current disc image + /// + /// True if the TOC could be loaded, false otherwise + private bool LoadTOC() + { + // If the image is invalide, we can't load or generate a TOC + if(_image == null) + return false; + + if(_image.Info.ReadableMediaTags?.Contains(MediaTagType.CD_FullTOC) != true) + { + // Only generate the TOC if we have it set + if(!App.Settings.GenerateMissingTOC) + { + Console.WriteLine("Full TOC not found"); + return false; + } + + Console.WriteLine("Attempting to generate TOC"); + + // Get the list of tracks and flags to create the TOC with + List tracks = _image.Tracks.OrderBy(t => t.TrackSession).ThenBy(t => t.TrackSequence).ToList(); + Dictionary trackFlags = new Dictionary(); + foreach(Track track in tracks) + { + byte[] singleTrackFlags = _image.ReadSectorTag(track.TrackStartSector + 1, SectorTagType.CdTrackFlags); + if(singleTrackFlags != null) + trackFlags.Add((byte)track.TrackStartSector, singleTrackFlags[0]); + } + + try + { + _toc = Create(tracks, trackFlags); + Console.WriteLine(Prettify(_toc)); + return true; + } + catch + { + Console.WriteLine("Full TOC not found or generated"); + return false; + } + } + + byte[] tocBytes = _image.ReadDiskTag(MediaTagType.CD_FullTOC); + if(tocBytes == null || tocBytes.Length == 0) + { + Console.WriteLine("Error reading TOC from disc image"); + return false; + } + + if(Swapping.Swap(BitConverter.ToUInt16(tocBytes, 0)) + 2 != tocBytes.Length) + { + byte[] tmp = new byte[tocBytes.Length + 2]; + Array.Copy(tocBytes, 0, tmp, 2, tocBytes.Length); + tmp[0] = (byte)((tocBytes.Length & 0xFF00) >> 8); + tmp[1] = (byte)(tocBytes.Length & 0xFF); + tocBytes = tmp; + } + + var nullableToc = Decode(tocBytes); + if(nullableToc == null) + { + Console.WriteLine("Error decoding TOC"); + return false; + } + + _toc = nullableToc.Value; + Console.WriteLine(Prettify(_toc)); + return true; + } + + /// + /// Set default track flags for the current track + /// + /// Track object to read from + private void SetDefaultTrackFlags(Track track) + { + QuadChannel = false; + TrackType = track.TrackType; + CopyAllowed = false; + TrackHasEmphasis = false; + } + + /// + /// Set track flags from the current track + /// + /// Track object to read from + private void SetTrackFlags(Track track) + { + try + { + // Get the track descriptor from the TOC + TrackDataDescriptor descriptor = _toc.TrackDescriptors.First(d => d.POINT == track.TrackSequence); + + // Set the track flags from TOC data + 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; + QuadChannel = (flags & (byte)TocControl.FourChanNoPreEmph) == (byte)TocControl.FourChanNoPreEmph; + + return; + } + catch(Exception) + { + SetDefaultTrackFlags(track); + } + } + + #endregion + } +} diff --git a/RedBookPlayer/Discs/OpticalDisc.cs b/RedBookPlayer/Discs/OpticalDisc.cs new file mode 100644 index 0000000..817c626 --- /dev/null +++ b/RedBookPlayer/Discs/OpticalDisc.cs @@ -0,0 +1,166 @@ +using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Interfaces; + +namespace RedBookPlayer.Discs +{ + public abstract class OpticalDisc + { + #region Public Fields + + /// + /// Indicate if the disc is ready to be used + /// + public bool Initialized { get; protected set; } = false; + + /// + /// Current track number + /// + public abstract int CurrentTrackNumber { get; protected set; } + + /// + /// Current track index + /// + public abstract ushort CurrentTrackIndex { get; protected set; } + + /// + /// Current sector number + /// + public abstract ulong CurrentSector { get; set; } + + /// + /// Represents the sector starting the section + /// + public ulong SectionStartSector { get; protected set; } + + /// + /// Number of bytes per sector for the current track + /// + public int BytesPerSector => _image.Tracks[CurrentTrackNumber].TrackBytesPerSector; + + /// + /// Represents the track type + /// + public TrackType TrackType { get; protected set; } + + /// + /// Represents the total tracks on the disc + /// + public int TotalTracks { get; protected set; } = 0; + + /// + /// Represents the total indices on the disc + /// + public int TotalIndexes { get; protected set; } = 0; + + /// + /// Total sectors in the image + /// + public ulong TotalSectors => _image.Info.Sectors; + + /// + /// Represents the time adjustment offset for the disc + /// + public ulong TimeOffset { get; protected set; } = 0; + + /// + /// Represents the total playing time for the disc + /// + public ulong TotalTime { get; protected set; } = 0; + + #endregion + + #region Protected State Variables + + /// + /// Currently loaded disc image + /// + protected IOpticalMediaImage _image; + + #endregion + + /// + /// Initialize the disc with a given image + /// + /// Aaruformat image to load + /// True if playback should begin immediately, false otherwise + public abstract void Init(IOpticalMediaImage image, bool autoPlay = false); + + #region Seeking + + /// + /// Try to move to the next track, wrapping around if necessary + /// + public void NextTrack() + { + if(_image == null) + return; + + CurrentTrackNumber++; + LoadTrack(CurrentTrackNumber); + } + + /// + /// 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); + } + + /// + /// Try to move to the next track index + /// + /// True if index changes can trigger a track change, false otherwise + /// True if the track was changed, false otherwise + public abstract bool NextIndex(bool changeTrack); + + /// + /// Try to move to the previous track index + /// + /// True if index changes can trigger a track change, false otherwise + /// True if the track was changed, false otherwise + public abstract bool PreviousIndex(bool changeTrack); + + #endregion + + #region Helpers + + /// + /// Load the first valid track in the image + /// + public abstract void LoadFirstTrack(); + + /// + /// Read sector data from the base image starting from the current sector + /// + /// Current number of sectors to read + /// Byte array representing the read sectors, if possible + public byte[] ReadSectors(uint sectorsToRead) => _image.ReadSectors(CurrentSector, sectorsToRead); + + /// + /// Set the total indexes from the current track + /// + public abstract void SetTotalIndexes(); + + /// + /// Load the desired track, if possible + /// + /// Track number to load + protected abstract void LoadTrack(int track); + + #endregion + } +} diff --git a/RedBookPlayer/Discs/OpticalDiscFactory.cs b/RedBookPlayer/Discs/OpticalDiscFactory.cs new file mode 100644 index 0000000..99d9257 --- /dev/null +++ b/RedBookPlayer/Discs/OpticalDiscFactory.cs @@ -0,0 +1,80 @@ +using Aaru.CommonTypes.Interfaces; +using Aaru.CommonTypes.Metadata; + +namespace RedBookPlayer.Discs +{ + public static class OpticalDiscFactory + { + /// + /// Generate an OpticalDisc from an input IOpticalMediaImage + /// + /// IOpticalMediaImage to create from + /// True if the image should be playable immediately, false otherwise + /// Instantiated OpticalDisc, if possible + public static OpticalDisc GenerateFromImage(IOpticalMediaImage image, bool autoPlay) + { + // If the image is not usable, we don't do anything + if(!IsUsableImage(image)) + return null; + + // Create the output object + OpticalDisc opticalDisc; + + // Create the proper disc type + switch(GetMediaType(image)) + { + case "Compact Disc": + case "GD": + opticalDisc = new CompactDisc(); + break; + default: + opticalDisc = null; + break; + } + + // Null image means we don't do anything + if(opticalDisc == null) + return opticalDisc; + + // Instantiate the disc and return + opticalDisc.Init(image, autoPlay); + return opticalDisc; + } + + /// + /// Gets the human-readable media type from an image + /// + /// Media image to check + /// Type from the image, empty string on error + /// TODO: Can we be more granular with sub types? + private static string GetMediaType(IOpticalMediaImage image) + { + // Null image means we don't do anything + if(image == null) + return string.Empty; + + (string type, string _) = MediaType.MediaTypeToString(image.Info.MediaType); + return type; + } + + /// + /// Indicates if the image is considered "usable" or not + /// + /// Aaruformat image file + /// True if the image is playble, false otherwise + private static bool IsUsableImage(IOpticalMediaImage image) + { + // Invalid images can't be used + if(image == null) + return false; + + // Determine based on media type + return GetMediaType(image) switch + { + "Compact Disc" => true, + "GD" => true, // Requires TOC generation + _ => false, + }; + } + } +} diff --git a/RedBookPlayer/MainWindow.xaml b/RedBookPlayer/GUI/MainWindow.xaml similarity index 69% rename from RedBookPlayer/MainWindow.xaml rename to RedBookPlayer/GUI/MainWindow.xaml index eeab898..56dbf79 100644 --- a/RedBookPlayer/MainWindow.xaml +++ b/RedBookPlayer/GUI/MainWindow.xaml @@ -1,6 +1,7 @@ + x:Class="RedBookPlayer.GUI.MainWindow" Title="RedBookPlayer" SizeToContent="WidthAndHeight" + DragDrop.AllowDrop="True"> \ No newline at end of file diff --git a/RedBookPlayer/MainWindow.xaml.cs b/RedBookPlayer/GUI/MainWindow.xaml.cs similarity index 81% rename from RedBookPlayer/MainWindow.xaml.cs rename to RedBookPlayer/GUI/MainWindow.xaml.cs index f6166cc..474037b 100644 --- a/RedBookPlayer/MainWindow.xaml.cs +++ b/RedBookPlayer/GUI/MainWindow.xaml.cs @@ -1,11 +1,12 @@ using System; +using System.Collections.Generic; using System.IO; using System.Xml; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Markup.Xaml; -namespace RedBookPlayer +namespace RedBookPlayer.GUI { public class MainWindow : Window { @@ -62,15 +63,9 @@ namespace RedBookPlayer Instance.Height = ((PlayerView)Instance.ContentControl.Content).Height; } - public void OnKeyDown(object sender, KeyEventArgs e) - { - if(e.Key == Key.F1) - { - settingsWindow = new SettingsWindow(App.Settings); - settingsWindow.Show(); - } - } - + /// + /// Initialize the main window + /// void InitializeComponent() { AvaloniaXamlLoader.Load(this); @@ -97,6 +92,36 @@ namespace RedBookPlayer { PlayerView.Player.Stop(); }; + + AddHandler(DragDrop.DropEvent, MainWindow_Drop); } + + #region Event Handlers + + public async void MainWindow_Drop(object sender, DragEventArgs e) + { + PlayerView playerView = ContentControl.Content as PlayerView; + if(playerView == null) + return; + + IEnumerable fileNames = e.Data.GetFileNames(); + foreach(string filename in fileNames) + { + bool loaded = await playerView.LoadImage(filename); + if(loaded) + break; + } + } + + public void OnKeyDown(object sender, KeyEventArgs e) + { + if(e.Key == Key.F1) + { + settingsWindow = new SettingsWindow(App.Settings); + settingsWindow.Show(); + } + } + + #endregion } } \ No newline at end of file diff --git a/RedBookPlayer/PlayerView.xaml b/RedBookPlayer/GUI/PlayerView.xaml similarity index 96% rename from RedBookPlayer/PlayerView.xaml rename to RedBookPlayer/GUI/PlayerView.xaml index 2213eba..9221ecd 100644 --- a/RedBookPlayer/PlayerView.xaml +++ b/RedBookPlayer/GUI/PlayerView.xaml @@ -1,7 +1,7 @@ + x:Class="RedBookPlayer.GUI.PlayerView" Width="900" Height="400"> @@ -86,8 +86,8 @@ - AUDIO - AUDIO + AUDIO + AUDIO DATA DATA EMPHASIS diff --git a/RedBookPlayer/PlayerView.xaml.cs b/RedBookPlayer/GUI/PlayerView.xaml.cs similarity index 57% rename from RedBookPlayer/PlayerView.xaml.cs rename to RedBookPlayer/GUI/PlayerView.xaml.cs index 72dc4b3..533e076 100644 --- a/RedBookPlayer/PlayerView.xaml.cs +++ b/RedBookPlayer/GUI/PlayerView.xaml.cs @@ -4,10 +4,6 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using System.Timers; -using Aaru.CommonTypes.Enums; -using Aaru.CommonTypes.Interfaces; -using Aaru.DiscImages; -using Aaru.Filters; using Avalonia; using Avalonia.Controls; using Avalonia.Interactivity; @@ -15,8 +11,9 @@ using Avalonia.Markup.Xaml; using Avalonia.Media.Imaging; using Avalonia.Platform; using Avalonia.Threading; +using RedBookPlayer.Hardware; -namespace RedBookPlayer +namespace RedBookPlayer.GUI { public class PlayerView : UserControl { @@ -25,11 +22,6 @@ namespace RedBookPlayer /// public static Player Player = new Player(); - /// - /// Disc representing the loaded image - /// - public static PlayableDisc PlayableDisc = new PlayableDisc(); - /// /// Set of images representing the digits for the UI /// @@ -56,7 +48,7 @@ namespace RedBookPlayer public async Task GetPath() { var dialog = new OpenFileDialog { AllowMultiple = false }; - List knownExtensions = new AaruFormat().KnownExtensions.ToList(); + List knownExtensions = new Aaru.DiscImages.AaruFormat().KnownExtensions.ToList(); dialog.Filters.Add(new FileDialogFilter() { Name = "Aaru Image Format (*" + string.Join(", *", knownExtensions) + ")", @@ -67,40 +59,26 @@ namespace RedBookPlayer } /// - /// Generate the digit string to be interpreted by the UI + /// Load an image from the path /// - /// String representing the digits for the player - private string GenerateDigitString() + /// Path to the image to load + public async Task LoadImage(string path) { - // If the disc or player aren't initialized, return all '-' characters - if (!PlayableDisc.Initialized) - return string.Empty.PadLeft(20, '-'); - - // Otherwise, take the current time into account - ulong sectorTime = PlayableDisc.CurrentSector; - if (PlayableDisc.SectionStartSector != 0) - sectorTime -= PlayableDisc.SectionStartSector; - else - sectorTime += PlayableDisc.TimeOffset; - - int[] numbers = new int[] + bool result = await Task.Run(() => { - PlayableDisc.CurrentTrackNumber + 1, - PlayableDisc.CurrentTrackIndex, + Player.Init(path, App.Settings.AutoPlay); + return Player.Initialized; + }); - (int)(sectorTime / (75 * 60)), - (int)(sectorTime / 75 % 60), - (int)(sectorTime % 75), + if(result) + { + await Dispatcher.UIThread.InvokeAsync(() => + { + MainWindow.Instance.Title = "RedBookPlayer - " + path.Split('/').Last().Split('\\').Last(); + }); + } - PlayableDisc.TotalTracks, - PlayableDisc.TotalIndexes, - - (int)(PlayableDisc.TotalTime / (75 * 60)), - (int)(PlayableDisc.TotalTime / 75 % 60), - (int)(PlayableDisc.TotalTime % 75), - }; - - return string.Join("", numbers.Select(i => i.ToString().PadLeft(2, '0').Substring(0, 2))); + return result; } /// @@ -127,10 +105,44 @@ namespace RedBookPlayer } } + /// + /// 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 + if (xaml != null) + new AvaloniaXamlLoader().Load(xaml, null, this); + else + 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 Initialize() + private void InitializeDigits() { _digits = new Image[] { @@ -162,98 +174,6 @@ namespace RedBookPlayer }; } - /// - /// 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(); - - if (xaml != null) - new AvaloniaXamlLoader().Load(xaml, null, this); - else - AvaloniaXamlLoader.Load(this); - - Initialize(); - - _updateTimer = new Timer(1000 / 60); - - _updateTimer.Elapsed += (sender, e) => - { - try - { - UpdateView(sender, e); - } - catch(Exception ex) - { - Console.WriteLine(ex); - } - }; - - _updateTimer.AutoReset = true; - _updateTimer.Start(); - } - - /// - /// Indicates if the image is considered "playable" or not - /// - /// Aaruformat image file - /// True if the image is playble, false otherwise - private bool IsPlayableImage(IOpticalMediaImage image) - { - // Invalid images can't be played - if (image == null) - return false; - - // Determine based on media type - // TODO: Can we be more granular with sub types? - (string type, string _) = Aaru.CommonTypes.Metadata.MediaType.MediaTypeToString(image.Info.MediaType); - return type switch - { - "Compact Disc" => true, - "GD" => true, // Requires TOC generation - _ => false, - }; - } - - /// - /// Load an image from the path - /// - /// Path to the image to load - private async void LoadImage(string path) - { - bool result = await Task.Run(() => - { - var image = new AaruFormat(); - var filter = new ZZZNoFilter(); - filter.Open(path); - image.Open(filter); - - if(IsPlayableImage(image)) - { - PlayableDisc.Init(image, App.Settings.AutoPlay); - if(PlayableDisc.Initialized) - { - Player.Init(PlayableDisc, App.Settings.AutoPlay); - return true; - } - - return false; - } - else - return false; - }); - - if(result) - { - await Dispatcher.UIThread.InvokeAsync(() => - { - MainWindow.Instance.Title = "RedBookPlayer - " + path.Split('/').Last().Split('\\').Last(); - }); - } - } - /// /// Update the UI with the most recent information from the Player /// @@ -261,24 +181,14 @@ namespace RedBookPlayer { Dispatcher.UIThread.InvokeAsync(() => { - string digitString = GenerateDigitString(); + string digitString = Player.GenerateDigitString(); for (int i = 0; i < _digits.Length; i++) { if (_digits[i] != null) _digits[i].Source = GetBitmap(digitString[i]); } - if (Player.Initialized) - { - PlayerViewModel dataContext = (PlayerViewModel)DataContext; - dataContext.HiddenTrack = PlayableDisc.TimeOffset > 150; - dataContext.ApplyDeEmphasis = PlayableDisc.ApplyDeEmphasis; - dataContext.TrackHasEmphasis = PlayableDisc.TrackHasEmphasis; - dataContext.CopyAllowed = PlayableDisc.CopyAllowed; - dataContext.QuadChannel = PlayableDisc.QuadChannel; - dataContext.IsAudioTrack = PlayableDisc.TrackType == TrackType.Audio; - dataContext.IsDataTrack = PlayableDisc.TrackType != TrackType.Audio; - } + Player.UpdateDataContext(DataContext as PlayerViewModel); }); } @@ -295,51 +205,29 @@ namespace RedBookPlayer LoadImage(path); } - public void PlayButton_Click(object sender, RoutedEventArgs e) => Player.Play(); + public void PlayButton_Click(object sender, RoutedEventArgs e) => Player.TogglePlayPause(true); - public void PauseButton_Click(object sender, RoutedEventArgs e) => Player.Pause(); + public void PauseButton_Click(object sender, RoutedEventArgs e) => Player.TogglePlayPause(false); + + public void PlayPauseButton_Click(object sender, RoutedEventArgs e) => Player.TogglePlayPause(!Player.Playing); public void StopButton_Click(object sender, RoutedEventArgs e) => Player.Stop(); - public void NextTrackButton_Click(object sender, RoutedEventArgs e) - { - bool wasPlaying = Player.Playing; - if(wasPlaying) Player.Pause(); - PlayableDisc.NextTrack(); - if(wasPlaying) Player.Play(); - } + public void NextTrackButton_Click(object sender, RoutedEventArgs e) => Player.NextTrack(); - public void PreviousTrackButton_Click(object sender, RoutedEventArgs e) - { - bool wasPlaying = Player.Playing; - if(wasPlaying) Player.Pause(); - PlayableDisc.PreviousTrack(); - if(wasPlaying) Player.Play(); - } + public void PreviousTrackButton_Click(object sender, RoutedEventArgs e) => Player.PreviousTrack(); - public void NextIndexButton_Click(object sender, RoutedEventArgs e) - { - bool wasPlaying = Player.Playing; - if(wasPlaying) Player.Pause(); - PlayableDisc.NextIndex(App.Settings.IndexButtonChangeTrack); - if(wasPlaying) Player.Play(); - } + public void NextIndexButton_Click(object sender, RoutedEventArgs e) => Player.NextIndex(App.Settings.IndexButtonChangeTrack); - public void PreviousIndexButton_Click(object sender, RoutedEventArgs e) - { - bool wasPlaying = Player.Playing; - if(wasPlaying) Player.Pause(); - PlayableDisc.PreviousIndex(App.Settings.IndexButtonChangeTrack); - if(wasPlaying) Player.Play(); - } + public void PreviousIndexButton_Click(object sender, RoutedEventArgs e) => Player.PreviousIndex(App.Settings.IndexButtonChangeTrack); - public void FastForwardButton_Click(object sender, RoutedEventArgs e) => PlayableDisc.FastForward(); + public void FastForwardButton_Click(object sender, RoutedEventArgs e) => Player.FastForward(); - public void RewindButton_Click(object sender, RoutedEventArgs e) => PlayableDisc.Rewind(); + public void RewindButton_Click(object sender, RoutedEventArgs e) => Player.Rewind(); - public void EnableDeEmphasisButton_Click(object sender, RoutedEventArgs e) => PlayableDisc.ToggleDeEmphasis(true); + public void EnableDeEmphasisButton_Click(object sender, RoutedEventArgs e) => Player.ToggleDeEmphasis(true); - public void DisableDeEmphasisButton_Click(object sender, RoutedEventArgs e) => PlayableDisc.ToggleDeEmphasis(false); + public void DisableDeEmphasisButton_Click(object sender, RoutedEventArgs e) => Player.ToggleDeEmphasis(false); #endregion } diff --git a/RedBookPlayer/PlayerViewModel.cs b/RedBookPlayer/GUI/PlayerViewModel.cs similarity index 85% rename from RedBookPlayer/PlayerViewModel.cs rename to RedBookPlayer/GUI/PlayerViewModel.cs index 17e058f..5090377 100644 --- a/RedBookPlayer/PlayerViewModel.cs +++ b/RedBookPlayer/GUI/PlayerViewModel.cs @@ -1,6 +1,6 @@ using ReactiveUI; -namespace RedBookPlayer +namespace RedBookPlayer.GUI { public class PlayerViewModel : ReactiveObject { @@ -11,6 +11,27 @@ namespace RedBookPlayer set => this.RaiseAndSetIfChanged(ref _applyDeEmphasis, value); } + 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 { @@ -24,33 +45,5 @@ namespace RedBookPlayer get => _hiddenTrack; set => this.RaiseAndSetIfChanged(ref _hiddenTrack, value); } - - private bool _copyAllowed; - public bool CopyAllowed - { - get => _copyAllowed; - set => this.RaiseAndSetIfChanged(ref _copyAllowed, value); - } - - private bool _quadChannel; - public bool QuadChannel - { - get => _quadChannel; - set => this.RaiseAndSetIfChanged(ref _quadChannel, value); - } - - private bool _isAudioTrack; - public bool IsAudioTrack - { - get => _isAudioTrack; - set => this.RaiseAndSetIfChanged(ref _isAudioTrack, value); - } - - private bool _isDataTrack; - public bool IsDataTrack - { - get => _isDataTrack; - set => this.RaiseAndSetIfChanged(ref _isDataTrack, value); - } } } \ No newline at end of file diff --git a/RedBookPlayer/SettingsWindow.xaml b/RedBookPlayer/GUI/SettingsWindow.xaml similarity index 95% rename from RedBookPlayer/SettingsWindow.xaml rename to RedBookPlayer/GUI/SettingsWindow.xaml index 3f2ce4c..7f5c54a 100644 --- a/RedBookPlayer/SettingsWindow.xaml +++ b/RedBookPlayer/GUI/SettingsWindow.xaml @@ -1,7 +1,7 @@ + d:DesignHeight="450" x:Class="RedBookPlayer.GUI.SettingsWindow" Title="Settings" Width="450" Height="600"> Themes diff --git a/RedBookPlayer/SettingsWindow.xaml.cs b/RedBookPlayer/GUI/SettingsWindow.xaml.cs similarity index 66% rename from RedBookPlayer/SettingsWindow.xaml.cs rename to RedBookPlayer/GUI/SettingsWindow.xaml.cs index ae39c0f..cfe293e 100644 --- a/RedBookPlayer/SettingsWindow.xaml.cs +++ b/RedBookPlayer/GUI/SettingsWindow.xaml.cs @@ -4,7 +4,7 @@ using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; -namespace RedBookPlayer +namespace RedBookPlayer.GUI { public class SettingsWindow : Window { @@ -44,30 +44,42 @@ namespace RedBookPlayer void InitializeComponent() { AvaloniaXamlLoader.Load(this); - - _themeList = this.FindControl("ThemeList"); - _themeList.SelectionChanged += ThemeList_SelectionChanged; - - List items = new List(); - items.Add("default"); - - if(Directory.Exists("themes/")) - { - foreach(string dir in Directory.EnumerateDirectories("themes/")) - { - string themeName = dir.Split('/')[1]; - - if (!File.Exists($"themes/{themeName}/view.xaml")) - continue; - - items.Add(themeName); - } - } - - _themeList.Items = items; + + PopulateThemes(); this.FindControl