using System; using System.Collections.Generic; using System.IO; using System.Linq; using Aaru.CommonTypes.Enums; using Aaru.CommonTypes.Interfaces; using Aaru.CommonTypes.Structs; using Aaru.Decoders.CD; using Aaru.Helpers; using CSCore.Codecs.WAV; using ReactiveUI; using static Aaru.Decoders.CD.FullTOC; namespace RedBookPlayer.Models.Discs { public class CompactDisc : OpticalDiscBase, IReactiveObject { #region Public Fields /// public override int CurrentTrackNumber { get => _currentTrackNumber; protected set { // Unset image means we can't do anything if(_image == null) return; // Invalid value means we can't do anything if(value > _image.Tracks.Max(t => t.TrackSequence)) return; else if(value < _image.Tracks.Min(t => t.TrackSequence)) return; // Cache the current track for easy access Track track = GetTrack(value); if(track == null) return; // Set all track flags and values SetTrackFlags(track); TotalIndexes = track.Indexes.Keys.Max(); CurrentTrackIndex = track.Indexes.Keys.Min(); CurrentTrackSession = track.TrackSession; // Mark the track as changed this.RaiseAndSetIfChanged(ref _currentTrackNumber, value); } } /// 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); if(track == null) return; // Invalid value means we can't do anything if(value > track.Indexes.Keys.Max()) return; else if(value < track.Indexes.Keys.Min()) return; this.RaiseAndSetIfChanged(ref _currentTrackIndex, value); // Set new index-specific data SectionStartSector = (ulong)track.Indexes[CurrentTrackIndex]; TotalTime = track.TrackEndSector - track.TrackStartSector; } } /// public override ushort CurrentTrackSession { get => _currentTrackSession; protected set => this.RaiseAndSetIfChanged(ref _currentTrackSession, value); } /// public override ulong CurrentSector { get => _currentSector; protected set { // Unset image means we can't do anything if(_image == null) return; // Invalid value means we can't do anything if(value > _image.Info.Sectors) return; else if(value < 0) return; // Cache the current track for easy access Track track = GetTrack(CurrentTrackNumber); if(track == null) return; this.RaiseAndSetIfChanged(ref _currentSector, value); // If the current sector is outside of the last known track, seek to the right one if(CurrentSector < track.TrackStartSector || CurrentSector > track.TrackEndSector) { track = _image.Tracks.Last(t => CurrentSector >= t.TrackStartSector); CurrentTrackNumber = (int)track.TrackSequence; } // Set the new index, if necessary 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 ?? 0; /// /// Readonly list of all tracks in the image /// public List Tracks => _image?.Tracks; /// /// 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 track session /// private ushort _currentTrackSession = 0; /// /// Current sector number /// private ulong _currentSector = 0; /// /// Indicate if a TOC should be generated if missing /// private readonly bool _generateMissingToc = false; /// /// Current disc table of contents /// private CDFullTOC _toc; #endregion /// /// Constructor /// /// Set of options for a new disc public CompactDisc(OpticalDiscOptions options) => _generateMissingToc = options.GenerateMissingToc; /// public override void Init(string path, IOpticalMediaImage image, bool autoPlay) { // If the image is null, we can't do anything if(image == null) return; // Set the current disc image ImagePath = path; _image = image; // Attempt to load the TOC if(!LoadTOC()) return; // Load the first track by default CurrentTrackNumber = 1; LoadTrack(CurrentTrackNumber); // Reset total indexes if not in autoplay if(!autoPlay) TotalIndexes = 0; // Set the internal disc state TotalTracks = (int)_image.Tracks.Max(t => t.TrackSequence); 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 Helpers /// public override void ExtractTrackToWav(uint trackNumber, string outputDirectory) => ExtractTrackToWav(trackNumber, outputDirectory, DataPlayback.Skip); /// /// Extract a track to WAV /// /// Track number to extract /// Output path to write data to /// DataPlayback value indicating how to handle data tracks public void ExtractTrackToWav(uint trackNumber, string outputDirectory, DataPlayback dataPlayback) { if(_image == null) return; // Get the track with that value, if possible Track track = _image.Tracks.FirstOrDefault(t => t.TrackSequence == trackNumber); if(track == null) return; // Get the number of sectors to read uint length = (uint)(track.TrackEndSector - track.TrackStartSector); // Read in the track data to a buffer byte[] buffer = ReadSectors(track.TrackStartSector, length, dataPlayback); // Build the WAV output string filename = Path.Combine(outputDirectory, $"Track {trackNumber.ToString().PadLeft(2, '0')}.wav"); using(WaveWriter waveWriter = new WaveWriter(filename, new CSCore.WaveFormat())) { // TODO: This should also apply de-emphasis as on playback // Should this be configurable? Match the de-emphasis status? // Write out to the file waveWriter.Write(buffer, 0, buffer.Length); } } /// /// Get the track with the given sequence value, if possible /// /// Track number to retrieve /// Track object for the requested sequence, null on error public Track GetTrack(int trackNumber) { try { return _image.Tracks.FirstOrDefault(t => t.TrackSequence == trackNumber); } catch { return null; } } /// public 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); // Load and debug output uint sectorCount = (uint)(track.TrackEndSector - track.TrackStartSector); byte[] trackData = ReadSectors(sectorCount); Console.WriteLine($"DEBUG: Track {trackNumber} - {sectorCount} sectors / {trackData.Length} bytes"); } /// public override void LoadIndex(ushort index) { if(_image == null) return; // Cache the current track for easy access Track track = GetTrack(CurrentTrackNumber); if(track == null) return; // If the index is invalid, just return if(index < track.Indexes.Keys.Min() || index > track.Indexes.Keys.Max()) return; // Select the first index that has a sector offset greater than or equal to 0 CurrentSector = (ulong)track.Indexes[index]; } /// public override byte[] ReadSectors(uint sectorsToRead) => ReadSectors(CurrentSector, sectorsToRead, DataPlayback.Skip); /// /// Read sector data from the base image starting from the specified sector /// /// Current number of sectors to read /// DataPlayback value indicating how to handle data tracks /// Byte array representing the read sectors, if possible public byte[] ReadSectors(uint sectorsToRead, DataPlayback dataPlayback) => ReadSectors(CurrentSector, sectorsToRead, dataPlayback); /// /// Read subchannel data from the base image starting from the specified sector /// /// Current number of sectors to read /// Byte array representing the read subchannels, if possible public byte[] ReadSubchannels(uint sectorsToRead) => ReadSubchannels(CurrentSector, sectorsToRead); /// /// Read sector data from the base image starting from the specified sector /// /// Sector to start at for reading /// Current number of sectors to read /// DataPlayback value indicating how to handle data tracks /// Byte array representing the read sectors, if possible /// Should be a multiple of 96 bytes private byte[] ReadSectors(ulong startSector, uint sectorsToRead, DataPlayback dataPlayback) { if(TrackType == TrackType.Audio || dataPlayback == DataPlayback.Play) { return _image.ReadSectors(startSector, sectorsToRead); } else if(dataPlayback == DataPlayback.Blank) { byte[] sectors = _image.ReadSectors(startSector, sectorsToRead); Array.Clear(sectors, 0, sectors.Length); return sectors; } else { return new byte[0]; } } /// /// Read subchannel data from the base image starting from the specified sector /// /// Sector to start at for reading /// Current number of sectors to read /// Byte array representing the read subchannels, if possible /// Should be a multiple of 96 bytes private byte[] ReadSubchannels(ulong startSector, uint sectorsToRead) => _image.ReadSectorsTag(startSector, sectorsToRead, SectorTagType.CdSectorSubchannel); /// public override void SetTotalIndexes() { if(_image == null) return; TotalIndexes = GetTrack(CurrentTrackNumber)?.Indexes.Keys.Max() ?? 0; } /// /// 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 } }