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 ReactiveUI;
using static Aaru.Decoders.CD.FullTOC;
namespace RedBookPlayer.Common.Discs
{
public class CompactDisc : OpticalDisc, IReactiveObject
{
#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;
// Check if we're incrementing or decrementing the track
bool increment = cachedValue >= _currentTrackNumber;
do
{
// 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++;
}
// 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 = GetTrack(cachedTrackNumber);
// 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 || _loadDataTracks)
break;
// If we're not playing the track, skip
if(increment)
cachedValue++;
else
cachedValue--;
}
while(cachedValue != _currentTrackNumber);
this.RaiseAndSetIfChanged(ref _currentTrackNumber, 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 = GetTrack(CurrentTrackNumber);
// Ensure that the value is valid, wrapping around if necessary
ushort fixedValue = value;
if(value > track.Indexes.Keys.Max())
fixedValue = track.Indexes.Keys.Min();
else if(value < track.Indexes.Keys.Min())
fixedValue = track.Indexes.Keys.Max();
this.RaiseAndSetIfChanged(ref _currentTrackIndex, fixedValue);
// Set new index-specific data
SectionStartSector = (ulong)track.Indexes[CurrentTrackIndex];
TotalTime = track.TrackEndSector - track.TrackStartSector;
}
}
///
public override ulong CurrentSector
{
get => _currentSector;
protected set
{
// Unset image means we can't do anything
if(_image == null)
return;
// Cache the current track for easy access
Track track = GetTrack(CurrentTrackNumber);
this.RaiseAndSetIfChanged(ref _currentSector, value);
if((CurrentTrackNumber < _image.Tracks.Count - 1 && CurrentSector >= GetTrack(CurrentTrackNumber + 1).TrackStartSector)
|| (CurrentTrackNumber > 0 && CurrentSector < track.TrackStartSector))
{
foreach(Track trackData in _image.Tracks.ToArray().Reverse())
{
if(CurrentSector >= trackData.TrackStartSector)
{
CurrentTrackNumber = (int)trackData.TrackSequence;
break;
}
}
}
foreach((ushort key, int i) in track.Indexes.Reverse())
{
if((int)CurrentSector >= i)
{
CurrentTrackIndex = key;
return;
}
}
CurrentTrackIndex = 0;
}
}
///
public override int BytesPerSector => GetTrack(CurrentTrackNumber).TrackRawBytesPerSector;
///
/// Represents the 4CH flag
///
public bool QuadChannel
{
get => _quadChannel;
private set => this.RaiseAndSetIfChanged(ref _quadChannel, value);
}
///
/// Represents the DATA flag
///
public bool IsDataTrack
{
get => _isDataTrack;
private set => this.RaiseAndSetIfChanged(ref _isDataTrack, value);
}
///
/// Represents the DCP flag
///
public bool CopyAllowed
{
get => _copyAllowed;
private set => this.RaiseAndSetIfChanged(ref _copyAllowed, value);
}
///
/// Represents the PRE flag
///
public bool TrackHasEmphasis
{
get => _trackHasEmphasis;
private set => this.RaiseAndSetIfChanged(ref _trackHasEmphasis, value);
}
private bool _quadChannel;
private bool _isDataTrack;
private bool _copyAllowed;
private bool _trackHasEmphasis;
#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;
///
/// Indicate if a TOC should be generated if missing
///
private readonly bool _generateMissingToc = false;
///
/// Indicate if data tracks should be loaded
///
private bool _loadDataTracks = false;
///
/// Indicate if hidden tracks should be loaded
///
private bool _loadHiddenTracks = false;
///
/// Current disc table of contents
///
private CDFullTOC _toc;
#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)
{
// 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 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;
// Cache the current track for easy access
Track track = GetTrack(CurrentTrackNumber);
// 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)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)track.Indexes[++CurrentTrackIndex];
}
return false;
}
///
public override bool PreviousIndex(bool changeTrack)
{
if(_image == null)
return false;
// Cache the current track for easy access
Track track = GetTrack(CurrentTrackNumber);
// 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)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)track.Indexes[--CurrentTrackIndex];
}
return false;
}
#endregion
#region Helpers
///
public override void LoadFirstTrack()
{
CurrentTrackNumber = 1;
LoadTrack(CurrentTrackNumber);
}
///
/// Set the value for loading data tracks
///
/// True to enable loading data tracks, false otherwise
public void SetLoadDataTracks(bool load) => _loadDataTracks = load;
///
/// Set the value for loading hidden tracks
///
/// True to enable loading hidden tracks, false otherwise
public void SetLoadHiddenTracks(bool load) => _loadHiddenTracks = load;
///
public override void SetTotalIndexes()
{
if(_image == null)
return;
TotalIndexes = _image.Tracks[CurrentTrackNumber].Indexes.Keys.Max();
}
///
protected override void LoadTrack(int trackNumber)
{
if(_image == null)
return;
// If the track number is invalid, just return
if(trackNumber < _image.Tracks.Min(t => t.TrackSequence) || trackNumber > _image.Tracks.Max(t => t.TrackSequence))
return;
// 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;
}
}
///
/// 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(!_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