Files
RedBookPlayer/RedBookPlayer.Models/Discs/CompactDisc.cs

678 lines
23 KiB
C#
Raw Normal View History

using System;
using System.Collections.Generic;
2021-08-24 22:11:25 -07:00
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;
2021-08-24 22:11:25 -07:00
using CSCore.Codecs.WAV;
2021-07-04 23:36:09 -07:00
using ReactiveUI;
using static Aaru.Decoders.CD.FullTOC;
2021-07-12 15:40:56 -07:00
namespace RedBookPlayer.Models.Discs
{
2021-07-12 10:52:50 -07:00
public class CompactDisc : OpticalDiscBase, IReactiveObject
{
#region Public Fields
/// <inheritdoc/>
public override int CurrentTrackNumber
{
get => _currentTrackNumber;
protected set
{
// Unset image means we can't do anything
if(_image == null)
return;
2021-07-06 09:54:43 -07:00
// Data tracks only and flag disabled means we can't do anything
2021-08-05 21:05:20 -07:00
if(_image.Tracks.All(t => t.TrackType != TrackType.Audio) && DataPlayback == DataPlayback.Skip)
2021-07-06 09:54:43 -07:00
return;
// Cache the value and the current track number
int cachedValue = value;
2021-07-04 23:36:09 -07:00
int cachedTrackNumber;
// Check if we're incrementing or decrementing the track
bool increment = cachedValue >= _currentTrackNumber;
do
{
2021-07-05 22:13:00 -07:00
// 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);
2021-07-12 10:57:52 -07:00
if(cachedValue == 0 && !LoadHiddenTracks)
2021-07-05 22:13:00 -07:00
cachedValue++;
}
// If we're under the first track and we're not loading hidden tracks, wrap around
2021-07-12 10:57:52 -07:00
else if(cachedValue < 1 && !LoadHiddenTracks)
2021-07-05 22:13:00 -07:00
{
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);
}
2021-07-04 23:36:09 -07:00
cachedTrackNumber = cachedValue;
// Cache the current track for easy access
2021-07-05 22:13:00 -07:00
Track track = GetTrack(cachedTrackNumber);
2021-07-06 09:54:43 -07:00
if(track == null)
return;
// Set track flags from subchannel data, if possible
SetTrackFlags(track);
// If the track is playable, just return
2021-08-24 22:11:25 -07:00
if((TrackType == TrackType.Audio || DataPlayback != DataPlayback.Skip)
&& (SessionHandling == SessionHandling.AllSessions || track.TrackSession == 1))
2021-08-05 21:05:20 -07:00
{
2021-07-04 23:36:09 -07:00
break;
2021-08-05 21:05:20 -07:00
}
// If we're not playing the track, skip
if(increment)
cachedValue++;
else
cachedValue--;
}
2021-07-04 23:36:09 -07:00
while(cachedValue != _currentTrackNumber);
2021-07-06 09:54:43 -07:00
// If we looped around, ensure it reloads
if(cachedValue == _currentTrackNumber)
{
this.RaiseAndSetIfChanged(ref _currentTrackNumber, -1);
Track track = GetTrack(cachedValue);
if(track == null)
return;
SetTrackFlags(track);
}
this.RaiseAndSetIfChanged(ref _currentTrackNumber, cachedValue);
Track cachedTrack = GetTrack(cachedValue);
if(cachedTrack == null)
return;
2021-07-05 22:39:08 -07:00
2021-07-06 09:54:43 -07:00
TotalIndexes = cachedTrack.Indexes.Keys.Max();
CurrentTrackIndex = cachedTrack.Indexes.Keys.Min();
2021-08-24 22:11:25 -07:00
CurrentTrackSession = cachedTrack.TrackSession;
}
}
/// <inheritdoc/>
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
2021-07-05 22:13:00 -07:00
Track track = GetTrack(CurrentTrackNumber);
2021-07-06 09:54:43 -07:00
if(track == null)
return;
// Ensure that the value is valid, wrapping around if necessary
2021-07-04 23:36:09 -07:00
ushort fixedValue = value;
if(value > track.Indexes.Keys.Max())
2021-07-04 23:36:09 -07:00
fixedValue = track.Indexes.Keys.Min();
else if(value < track.Indexes.Keys.Min())
2021-07-04 23:36:09 -07:00
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;
}
}
2021-08-24 22:11:25 -07:00
/// <inheritdoc/>
public override ushort CurrentTrackSession
{
get => _currentTrackSession;
protected set => this.RaiseAndSetIfChanged(ref _currentTrackSession, value);
}
/// <inheritdoc/>
public override ulong CurrentSector
{
get => _currentSector;
2021-07-05 16:30:38 -07:00
protected set
{
// Unset image means we can't do anything
if(_image == null)
return;
2021-08-05 21:05:20 -07:00
// If the sector is over the end of the image, then loop
ulong tempSector = value;
if(tempSector > _image.Info.Sectors)
tempSector = 0;
else if(tempSector < 0)
tempSector = _image.Info.Sectors - 1;
// Cache the current track for easy access
2021-07-05 22:13:00 -07:00
Track track = GetTrack(CurrentTrackNumber);
2021-07-06 09:54:43 -07:00
if(track == null)
return;
2021-08-05 21:05:20 -07:00
this.RaiseAndSetIfChanged(ref _currentSector, tempSector);
2021-08-05 21:05:20 -07:00
// If the current sector is outside of the last known track, seek to the right one
if(CurrentSector < track.TrackStartSector || CurrentSector > track.TrackEndSector)
{
2021-08-05 21:05:20 -07:00
track = _image.Tracks.Last(t => CurrentSector >= t.TrackStartSector);
CurrentTrackNumber = (int)track.TrackSequence;
}
2021-08-05 21:05:20 -07:00
// Set the new index, if necessary
foreach((ushort key, int i) in track.Indexes.Reverse())
{
if((int)CurrentSector >= i)
{
CurrentTrackIndex = key;
return;
}
}
CurrentTrackIndex = 0;
}
}
2021-07-05 22:13:00 -07:00
/// <inheritdoc/>
2021-07-06 09:54:43 -07:00
public override int BytesPerSector => GetTrack(CurrentTrackNumber)?.TrackRawBytesPerSector ?? 0;
2021-07-05 22:13:00 -07:00
/// <summary>
/// Represents the 4CH flag
/// </summary>
2021-07-04 23:36:09 -07:00
public bool QuadChannel
{
get => _quadChannel;
private set => this.RaiseAndSetIfChanged(ref _quadChannel, value);
}
/// <summary>
/// Represents the DATA flag
/// </summary>
2021-07-04 23:36:09 -07:00
public bool IsDataTrack
{
get => _isDataTrack;
private set => this.RaiseAndSetIfChanged(ref _isDataTrack, value);
}
/// <summary>
/// Represents the DCP flag
/// </summary>
2021-07-04 23:36:09 -07:00
public bool CopyAllowed
{
get => _copyAllowed;
private set => this.RaiseAndSetIfChanged(ref _copyAllowed, value);
}
/// <summary>
/// Represents the PRE flag
/// </summary>
2021-07-04 23:36:09 -07:00
public bool TrackHasEmphasis
{
get => _trackHasEmphasis;
private set => this.RaiseAndSetIfChanged(ref _trackHasEmphasis, value);
}
2021-07-12 10:57:52 -07:00
/// <summary>
2021-08-05 21:05:20 -07:00
/// Indicate how data tracks should be handled
2021-07-12 10:57:52 -07:00
/// </summary>
2021-08-05 21:05:20 -07:00
public DataPlayback DataPlayback { get; set; } = DataPlayback.Skip;
2021-07-12 10:57:52 -07:00
/// <summary>
/// Indicate if hidden tracks should be loaded
/// </summary>
public bool LoadHiddenTracks { get; set; } = false;
2021-08-24 22:11:25 -07:00
/// <summary>
/// Indicates how tracks on different session should be handled
/// </summary>
public SessionHandling SessionHandling { get; set; } = SessionHandling.AllSessions;
2021-07-04 23:36:09 -07:00
private bool _quadChannel;
private bool _isDataTrack;
private bool _copyAllowed;
private bool _trackHasEmphasis;
#endregion
#region Private State Variables
/// <summary>
/// Current track number
/// </summary>
private int _currentTrackNumber = 0;
/// <summary>
/// Current track index
/// </summary>
private ushort _currentTrackIndex = 0;
2021-08-24 22:11:25 -07:00
/// <summary>
/// Current track session
/// </summary>
private ushort _currentTrackSession = 0;
/// <summary>
/// Current sector number
/// </summary>
private ulong _currentSector = 0;
/// <summary>
/// Indicate if a TOC should be generated if missing
/// </summary>
private readonly bool _generateMissingToc = false;
/// <summary>
/// Current disc table of contents
/// </summary>
private CDFullTOC _toc;
#endregion
/// <summary>
/// Constructor
/// </summary>
2021-08-05 21:05:20 -07:00
/// <param name="options">Set of options for a new disc</param>
public CompactDisc(OpticalDiscOptions options)
{
2021-08-05 21:05:20 -07:00
DataPlayback = options.DataPlayback;
_generateMissingToc = options.GenerateMissingToc;
LoadHiddenTracks = options.LoadHiddenTracks;
2021-08-24 22:11:25 -07:00
SessionHandling = options.SessionHandling;
}
/// <inheritdoc/>
2021-10-05 10:40:10 -07:00
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
2021-10-05 10:40:10 -07:00
ImagePath = path;
_image = image;
// Attempt to load the TOC
2021-06-21 21:50:50 -07:00
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 = (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 Seeking
2021-07-05 22:13:00 -07:00
/// <inheritdoc/>
public override void NextTrack()
{
if(_image == null)
return;
CurrentTrackNumber++;
LoadTrack(CurrentTrackNumber);
}
/// <inheritdoc/>
public override void PreviousTrack()
{
if(_image == null)
return;
CurrentTrackNumber--;
LoadTrack(CurrentTrackNumber);
}
/// <inheritdoc/>
public override bool NextIndex(bool changeTrack)
{
if(_image == null)
return false;
2021-07-05 22:13:00 -07:00
// Cache the current track for easy access
Track track = GetTrack(CurrentTrackNumber);
2021-07-06 09:54:43 -07:00
if(track == null)
return false;
2021-07-05 22:13:00 -07:00
// If the index is greater than the highest index, change tracks if needed
if(CurrentTrackIndex + 1 > track.Indexes.Keys.Max())
{
if(changeTrack)
{
NextTrack();
2021-07-05 22:13:00 -07:00
CurrentSector = (ulong)GetTrack(CurrentTrackNumber).Indexes.Values.Min();
return true;
}
}
2021-07-05 22:13:00 -07:00
// 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
{
2021-07-05 22:13:00 -07:00
CurrentSector = (ulong)track.Indexes[++CurrentTrackIndex];
}
return false;
}
/// <inheritdoc/>
2021-07-05 22:13:00 -07:00
public override bool PreviousIndex(bool changeTrack)
{
if(_image == null)
return false;
2021-07-05 22:13:00 -07:00
// Cache the current track for easy access
Track track = GetTrack(CurrentTrackNumber);
2021-07-06 09:54:43 -07:00
if(track == null)
return false;
2021-07-05 22:13:00 -07:00
// 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
2021-08-05 21:05:20 -07:00
else if(track.Indexes[(ushort)(CurrentTrackIndex - 1)] < 0)
{
if(changeTrack)
{
2021-07-05 22:13:00 -07:00
PreviousTrack();
CurrentSector = (ulong)GetTrack(CurrentTrackNumber).Indexes.Values.Max();
return true;
}
}
2021-08-05 21:05:20 -07:00
2021-07-05 22:13:00 -07:00
// Otherwise, just move to the previous index
else
{
2021-07-05 22:13:00 -07:00
CurrentSector = (ulong)track.Indexes[--CurrentTrackIndex];
}
return false;
}
#endregion
#region Helpers
2021-08-24 22:11:25 -07:00
/// <inheritdoc/>
public override void ExtractTrackToWav(uint trackNumber, string outputDirectory)
{
if(_image == null)
return;
// Get the track with that value, if possible
Track track = _image.Tracks.FirstOrDefault(t => t.TrackSequence == trackNumber);
// If the track isn't valid, we can't do anything
if(track == null || !(DataPlayback != DataPlayback.Skip || track.TrackType == TrackType.Audio))
2021-08-24 22:11:25 -07:00
return;
// Get the number of sectors to read
2021-08-24 22:11:25 -07:00
uint length = (uint)(track.TrackEndSector - track.TrackStartSector);
// Read in the track data to a buffer
byte[] buffer = ReadSectors(track.TrackStartSector, length);
2021-08-24 22:11:25 -07:00
// 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?
2021-08-24 22:11:25 -07:00
// Write out to the file
waveWriter.Write(buffer, 0, buffer.Length);
}
}
/// <inheritdoc/>
public override void ExtractAllTracksToWav(string outputDirectory)
{
if(_image == null)
return;
foreach(Track track in _image.Tracks)
{
ExtractTrackToWav(track.TrackSequence, outputDirectory);
}
}
2021-08-05 21:05:20 -07:00
/// <inheritdoc/>
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);
}
/// <inheritdoc/>
public override void LoadFirstTrack()
{
2021-07-05 22:13:00 -07:00
CurrentTrackNumber = 1;
LoadTrack(CurrentTrackNumber);
}
/// <inheritdoc/>
public override byte[] ReadSectors(uint sectorsToRead) => ReadSectors(CurrentSector, sectorsToRead);
/// <summary>
/// Read sector data from the base image starting from the specified sector
/// </summary>
/// <param name="startSector">Sector to start at for reading</param>
/// <param name="sectorsToRead">Current number of sectors to read</param>
/// <returns>Byte array representing the read sectors, if possible</returns>
private byte[] ReadSectors(ulong startSector, uint sectorsToRead)
{
2021-08-05 21:05:20 -07:00
if(TrackType == TrackType.Audio || DataPlayback == DataPlayback.Play)
{
return _image.ReadSectors(startSector, sectorsToRead);
}
2021-08-05 21:05:20 -07:00
else if(DataPlayback == DataPlayback.Blank)
{
byte[] sectors = _image.ReadSectors(startSector, sectorsToRead);
Array.Clear(sectors, 0, sectors.Length);
return sectors;
}
2021-08-05 21:05:20 -07:00
else
{
2021-08-05 21:05:20 -07:00
return new byte[0];
}
}
/// <inheritdoc/>
2021-08-05 21:05:20 -07:00
public override void SetTotalIndexes()
{
if(_image == null)
return;
2021-08-05 21:05:20 -07:00
TotalIndexes = GetTrack(CurrentTrackNumber)?.Indexes.Keys.Max() ?? 0;
2021-07-05 22:13:00 -07:00
}
/// <summary>
/// Get the track with the given sequence value, if possible
/// </summary>
/// <param name="trackNumber">Track number to retrieve</param>
/// <returns>Track object for the requested sequence, null on error</returns>
private Track GetTrack(int trackNumber)
{
try
{
return _image.Tracks.FirstOrDefault(t => t.TrackSequence == trackNumber);
}
catch
{
return null;
}
}
/// <summary>
/// Load TOC for the current disc image
/// </summary>
/// <returns>True if the TOC could be loaded, false otherwise</returns>
2021-06-21 21:50:50 -07:00
private bool LoadTOC()
{
2021-06-29 16:42:28 -07:00
// If the image is invalide, we can't load or generate a TOC
if(_image == null)
return false;
2021-06-21 21:50:50 -07:00
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");
2021-06-29 16:42:28 -07:00
// Get the list of tracks and flags to create the TOC with
List<Track> tracks = _image.Tracks.OrderBy(t => t.TrackSession).ThenBy(t => t.TrackSequence).ToList();
Dictionary<byte, byte> trackFlags = new Dictionary<byte, byte>();
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
{
2021-06-29 16:42:28 -07:00
_toc = Create(tracks, trackFlags);
Console.WriteLine(Prettify(_toc));
return true;
}
2021-06-29 16:42:28 -07:00
catch
{
Console.WriteLine("Full TOC not found or generated");
return false;
}
}
2021-06-21 21:50:50 -07:00
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;
}
2021-06-21 21:50:50 -07:00
var nullableToc = Decode(tocBytes);
if(nullableToc == null)
{
Console.WriteLine("Error decoding TOC");
return false;
}
_toc = nullableToc.Value;
Console.WriteLine(Prettify(_toc));
return true;
}
/// <summary>
/// Set default track flags for the current track
/// </summary>
/// <param name="track">Track object to read from</param>
private void SetDefaultTrackFlags(Track track)
{
TrackType = track.TrackType;
2021-07-04 23:36:09 -07:00
QuadChannel = false;
IsDataTrack = track.TrackType != TrackType.Audio;
CopyAllowed = false;
TrackHasEmphasis = false;
}
/// <summary>
/// Set track flags from the current track
/// </summary>
/// <param name="track">Track object to read from</param>
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;
2021-07-04 23:36:09 -07:00
IsDataTrack = (flags & (byte)TocControl.DataTrack) == (byte)TocControl.DataTrack;
QuadChannel = (flags & (byte)TocControl.FourChanNoPreEmph) == (byte)TocControl.FourChanNoPreEmph;
2021-07-04 23:36:09 -07:00
TrackType = IsDataTrack ? TrackType.Data : TrackType.Audio;
return;
}
catch(Exception)
{
SetDefaultTrackFlags(track);
}
}
#endregion
}
2021-08-05 21:05:20 -07:00
}