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