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 { // 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; cachedTrackNumber = cachedValue; // Cache the current track for easy access Track track = _image.Tracks[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 = _image.Tracks[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 = _image.Tracks[CurrentTrackNumber]; this.RaiseAndSetIfChanged(ref _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 => _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; /// /// Current disc table of contents /// private CDFullTOC _toc; #endregion /// /// Constructor /// /// Generate a TOC if the disc is missing one /// Load data tracks for playback public CompactDisc(bool generateMissingToc, bool loadDataTracks) { _generateMissingToc = generateMissingToc; _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 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, bool playHiddenTrack) { if(_image == null) return false; if(CurrentTrackIndex - 1 < _image.Tracks[CurrentTrackNumber].Indexes.Keys.Min()) { if(changeTrack) { PreviousTrack(playHiddenTrack); 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); } /// /// Set the value for loading data tracks /// /// True to enable loading data tracks, false otherwise public void SetLoadDataTracks(bool load) => _loadDataTracks = load; /// 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(!_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) { TrackType = track.TrackType; QuadChannel = false; IsDataTrack = track.TrackType != TrackType.Audio; 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; 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) { SetDefaultTrackFlags(track); } } #endregion } }