Split PlayableDisc from Player

This change does the following:
- Splits out all image reading and manipulation into a new file called PlayableDisc
- Fixes some issues present in loading a disc with a data first track
- Fixes some initialization issues that could be present
- Removed the Player code from being able to directly manipulate most of the disc state
This commit is contained in:
Matt Nadareski
2021-06-21 21:35:09 -07:00
parent 28a7e470a2
commit 1715388e29
4 changed files with 725 additions and 663 deletions

View File

@@ -0,0 +1,661 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Structs;
using Aaru.DiscImages;
using Aaru.Helpers;
using static Aaru.Decoders.CD.FullTOC;
namespace RedBookPlayer
{
public class PlayableDisc
{
#region Public Fields
/// <summary>
/// Indicate if the disc is ready to be used
/// </summary>
public bool Initialized { get; private set; } = false;
/// <summary>
/// Current track number
/// </summary>
public int CurrentTrackNumber
{
get => _currentTrackNumber;
set
{
// Unset image means we can't do anything
if(_image == null)
return;
// Check if we're incrementing or decrementing the track
bool increment = value >= _currentTrackNumber;
// Ensure that the value is valid, wrapping around if necessary
if(value >= _image.Tracks.Count)
_currentTrackNumber = 0;
else if(value < 0)
_currentTrackNumber = _image.Tracks.Count - 1;
else
_currentTrackNumber = value;
// Cache the current track for easy access
Track track = _image.Tracks[CurrentTrackNumber];
// Set new track-specific data
byte[] flagsData = _image.ReadSectorTag(track.TrackSequence, SectorTagType.CdTrackFlags);
ApplyDeEmphasis = ((CdFlags)flagsData[0]).HasFlag(CdFlags.PreEmphasis);
try
{
byte[] subchannel = _image.ReadSectorTag(track.TrackStartSector, SectorTagType.CdSectorSubchannel);
if(!ApplyDeEmphasis)
ApplyDeEmphasis = (subchannel[3] & 0b01000000) != 0;
CopyAllowed = (subchannel[2] & 0b01000000) != 0;
TrackType = (subchannel[1] & 0b01000000) != 0 ? Aaru.CommonTypes.Enums.TrackType.Data : Aaru.CommonTypes.Enums.TrackType.Audio;
}
catch(ArgumentException)
{
TrackType = track.TrackType;
}
TrackHasEmphasis = ApplyDeEmphasis;
TotalIndexes = track.Indexes.Keys.Max();
CurrentTrackIndex = track.Indexes.Keys.Min();
// If we're not playing data tracks, skip
if(!App.Settings.PlayDataTracks && TrackType != Aaru.CommonTypes.Enums.TrackType.Audio)
{
if(increment)
NextTrack();
else
PreviousTrack();
}
}
}
/// <summary>
/// Current track index
/// </summary>
public ushort CurrentTrackIndex
{
get => _currentTrackIndex;
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;
}
}
/// <summary>
/// Current sector number
/// </summary>
public 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;
}
}
/// <summary>
/// Represents the pre-emphasis flag
/// </summary>
public bool TrackHasEmphasis { get; private set; } = false;
/// <summary>
/// Indicates if de-emphasis should be applied
/// </summary>
public bool ApplyDeEmphasis { get; private set; } = false;
/// <summary>
/// Represents the copy allowed flag
/// </summary>
public bool CopyAllowed { get; private set; } = false;
/// <summary>
/// Represents the track type
/// </summary>
public TrackType? TrackType { get; private set; }
/// <summary>
/// Represents the sector starting the section
/// </summary>
public ulong SectionStartSector { get; private set; }
/// <summary>
/// Represents the total tracks on the disc
/// </summary>
public int TotalTracks { get; private set; } = 0;
/// <summary>
/// Represents the total indices on the disc
/// </summary>
public int TotalIndexes { get; private set; } = 0;
/// <summary>
/// Total sectors in the image
/// </summary>
public ulong TotalSectors => _image.Info.Sectors;
/// <summary>
/// Represents the time adjustment offset for the disc
/// </summary>
public ulong TimeOffset { get; private set; } = 0;
/// <summary>
/// Represents the total playing time for the disc
/// </summary>
public ulong TotalTime { get; private set; } = 0;
#endregion
#region Private State Variables
/// <summary>
/// Currently loaded disc image
/// </summary>
private AaruFormat _image;
/// <summary>
/// Current track number
/// </summary>
private int _currentTrackNumber = 0;
/// <summary>
/// Current track index
/// </summary>
private ushort _currentTrackIndex = 0;
/// <summary>
/// Current sector number
/// </summary>
private ulong _currentSector = 0;
/// <summary>
/// Current disc table of contents
/// </summary>
private CDFullTOC _toc;
#endregion
/// <summary>
/// Initialize the disc with a given image
/// </summary>
/// <param name="image">Aaruformat image to load</param>
/// <param name="autoPlay">True if playback should begin immediately, false otherwise</param>
public async void Init(AaruFormat 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(!await 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
/// <summary>
/// Try to move to the next track, wrapping around if necessary
/// </summary>
public void NextTrack()
{
if(_image == null)
return;
CurrentTrackNumber++;
LoadTrack(CurrentTrackNumber);
}
/// <summary>
/// Try to move to the previous track, wrapping around if necessary
/// </summary>
public void PreviousTrack()
{
if(_image == null)
return;
if(CurrentSector < (ulong)_image.Tracks[CurrentTrackNumber].Indexes[1] + 75)
{
if(App.Settings.AllowSkipHiddenTrack && CurrentTrackNumber == 0 && CurrentSector >= 75)
CurrentSector = 0;
else
CurrentTrackNumber--;
}
LoadTrack(CurrentTrackNumber);
}
/// <summary>
/// Try to move to the next track index
/// </summary>
/// <param name="changeTrack">True if index changes can trigger a track change, false otherwise</param>
/// <returns>True if the track was changed, false otherwise</returns>
public 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;
}
/// <summary>
/// Try to move to the previous track index
/// </summary>
/// <param name="changeTrack">True if index changes can trigger a track change, false otherwise</param>
/// <returns>True if the track was changed, false otherwise</returns>
public 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;
}
/// <summary>
/// Fast-forward playback by 75 sectors, if possible
/// </summary>
public void FastForward()
{
if(_image == null)
return;
CurrentSector = Math.Min(_image.Info.Sectors - 1, CurrentSector + 75);
}
/// <summary>
/// Rewind playback by 75 sectors, if possible
/// </summary>
public void Rewind()
{
if(_image == null)
return;
if(CurrentSector >= 75)
CurrentSector -= 75;
}
/// <summary>
/// Toggle de-emphasis processing
/// </summary>
/// <param name="enable">True to apply de-emphasis, false otherwise</param>
public void ToggleDeEmphasis(bool enable) => ApplyDeEmphasis = enable;
#endregion
#region Helpers
/// <summary>
/// Load the first valid track in the image
/// </summary>
public void LoadFirstTrack()
{
CurrentTrackNumber = 0;
LoadTrack(CurrentTrackNumber);
}
/// <summary>
/// Read sector data from the base image starting from the current sector
/// </summary>
/// <param name="sectorsToRead">Current number of sectors to read</param>
/// <returns>Byte array representing the read sectors, if possible</returns>
public byte[] ReadSectors(uint sectorsToRead) => _image.ReadSectors(CurrentSector, sectorsToRead);
/// <summary>
/// Set the total indexes from the current track
/// </summary>
public void SetTotalIndexes()
{
if(_image == null)
return;
TotalIndexes = _image.Tracks[CurrentTrackNumber].Indexes.Keys.Max();
}
/// <summary>
/// Generate a CDFullTOC object from the current image
/// </summary>
/// <returns>CDFullTOC object, if possible</returns>
/// <remarks>Copied from <see cref="Aaru.DiscImages.CloneCd"/></remarks>
private bool GenerateTOC()
{
// Invalid image means we can't generate anything
if(_image == null)
return false;
_toc = new CDFullTOC();
Dictionary<byte, byte> _trackFlags = new Dictionary<byte, byte>();
Dictionary<byte, byte> sessionEndingTrack = new Dictionary<byte, byte>();
_toc.FirstCompleteSession = byte.MaxValue;
_toc.LastCompleteSession = byte.MinValue;
List<TrackDataDescriptor> trackDescriptors = new List<TrackDataDescriptor>();
byte currentTrack = 0;
foreach(Track track in _image.Tracks.OrderBy(t => t.TrackSession).ThenBy(t => t.TrackSequence))
{
byte[] trackFlags = _image.ReadSectorTag(track.TrackStartSector + 1, SectorTagType.CdTrackFlags);
if(trackFlags != null)
_trackFlags.Add((byte)track.TrackStartSector, trackFlags[0]);
}
foreach(Track track in _image.Tracks.OrderBy(t => t.TrackSession).ThenBy(t => t.TrackSequence))
{
if(track.TrackSession < _toc.FirstCompleteSession)
_toc.FirstCompleteSession = (byte)track.TrackSession;
if(track.TrackSession <= _toc.LastCompleteSession)
{
currentTrack = (byte)track.TrackSequence;
continue;
}
if(_toc.LastCompleteSession > 0)
sessionEndingTrack.Add(_toc.LastCompleteSession, currentTrack);
_toc.LastCompleteSession = (byte)track.TrackSession;
}
byte currentSession = 0;
foreach(Track track in _image.Tracks.OrderBy(t => t.TrackSession).ThenBy(t => t.TrackSequence))
{
_trackFlags.TryGetValue((byte)track.TrackSequence, out byte trackControl);
if(trackControl == 0 &&
track.TrackType != Aaru.CommonTypes.Enums.TrackType.Audio)
trackControl = (byte)CdFlags.DataTrack;
// Lead-Out
if(track.TrackSession > currentSession &&
currentSession != 0)
{
(byte minute, byte second, byte frame) leadoutAmsf = LbaToMsf(track.TrackStartSector - 150);
(byte minute, byte second, byte frame) leadoutPmsf =
LbaToMsf(_image.Tracks.OrderBy(t => t.TrackSession).ThenBy(t => t.TrackSequence).Last().
TrackStartSector);
// Lead-out
trackDescriptors.Add(new TrackDataDescriptor
{
SessionNumber = currentSession,
POINT = 0xB0,
ADR = 5,
CONTROL = 0,
HOUR = 0,
Min = leadoutAmsf.minute,
Sec = leadoutAmsf.second,
Frame = leadoutAmsf.frame,
PHOUR = 2,
PMIN = leadoutPmsf.minute,
PSEC = leadoutPmsf.second,
PFRAME = leadoutPmsf.frame
});
// This seems to be constant? It should not exist on CD-ROM but CloneCD creates them anyway
// Format seems like ATIP, but ATIP should not be as 0xC0 in TOC...
//trackDescriptors.Add(new TrackDataDescriptor
//{
// SessionNumber = currentSession,
// POINT = 0xC0,
// ADR = 5,
// CONTROL = 0,
// Min = 128,
// PMIN = 97,
// PSEC = 25
//});
}
// Lead-in
if(track.TrackSession > currentSession)
{
currentSession = (byte)track.TrackSession;
sessionEndingTrack.TryGetValue(currentSession, out byte endingTrackNumber);
(byte minute, byte second, byte frame) leadinPmsf =
LbaToMsf(_image.Tracks.FirstOrDefault(t => t.TrackSequence == endingTrackNumber)?.TrackEndSector ??
0 + 1);
// Starting track
trackDescriptors.Add(new TrackDataDescriptor
{
SessionNumber = currentSession,
POINT = 0xA0,
ADR = 1,
CONTROL = trackControl,
PMIN = (byte)track.TrackSequence
});
// Ending track
trackDescriptors.Add(new TrackDataDescriptor
{
SessionNumber = currentSession,
POINT = 0xA1,
ADR = 1,
CONTROL = trackControl,
PMIN = endingTrackNumber
});
// Lead-out start
trackDescriptors.Add(new TrackDataDescriptor
{
SessionNumber = currentSession,
POINT = 0xA2,
ADR = 1,
CONTROL = trackControl,
PHOUR = 0,
PMIN = leadinPmsf.minute,
PSEC = leadinPmsf.second,
PFRAME = leadinPmsf.frame
});
}
(byte minute, byte second, byte frame) pmsf = LbaToMsf(track.TrackStartSector);
// Track
trackDescriptors.Add(new TrackDataDescriptor
{
SessionNumber = (byte)track.TrackSession,
POINT = (byte)track.TrackSequence,
ADR = 1,
CONTROL = trackControl,
PHOUR = 0,
PMIN = pmsf.minute,
PSEC = pmsf.second,
PFRAME = pmsf.frame
});
}
_toc.TrackDescriptors = trackDescriptors.ToArray();
return true;
}
/// <summary>
/// Convert the sector to LBA values
/// </summary>
/// <param name="sector">Sector to convert</param>
/// <returns>LBA values for the sector number</returns>
/// <remarks>Copied from <see cref="Aaru.DiscImages.CloneCd"/></remarks>
private (byte minute, byte second, byte frame) LbaToMsf(ulong sector) =>
((byte)((sector + 150) / 75 / 60), (byte)((sector + 150) / 75 % 60), (byte)((sector + 150) % 75));
/// <summary>
/// Load TOC for the current disc image
/// </summary>
/// <returns>True if the TOC could be loaded, false otherwise</returns>
private async Task<bool> LoadTOC()
{
if(await Task.Run(() => _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");
if(GenerateTOC())
{
Console.WriteLine(Prettify(_toc));
return true;
}
else
{
Console.WriteLine("Full TOC not found or generated");
return false;
}
}
byte[] tocBytes = await Task.Run(() => _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 = await Task.Run(() => Decode(tocBytes));
if(nullableToc == null)
{
Console.WriteLine("Error decoding TOC");
return false;
}
_toc = nullableToc.Value;
Console.WriteLine(Prettify(_toc));
return true;
}
/// <summary>
/// Load the desired track, if possible
/// </summary>
/// <param name="track">Track number to load</param>
private 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]);
}
#endregion
}
}

View File

@@ -1,15 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Structs;
using Aaru.DiscImages;
using Aaru.Helpers;
using CSCore.SoundOut;
using NWaves.Audio;
using NWaves.Filters.BiQuad;
using static Aaru.Decoders.CD.FullTOC;
namespace RedBookPlayer
{
@@ -22,248 +16,19 @@ namespace RedBookPlayer
/// </summary>
public bool Initialized { get; private set; } = false;
/// <summary>
/// Currently loaded disc image
/// </summary>
public AaruFormat Image { get; private set; }
/// <summary>
/// Current track number
/// </summary>
public int CurrentTrack
{
get => _currentTrack;
private set
{
// Unset image means we can't do anything
if(Image == null)
return;
// If the value is the same, don't do anything
if(value == _currentTrack)
return;
// Check if we're incrementing or decrementing the track
bool increment = value > _currentTrack;
// Ensure that the value is valid, wrapping around if necessary
if(value >= Image.Tracks.Count)
_currentTrack = 0;
else if(value < 0)
_currentTrack = Image.Tracks.Count - 1;
else
_currentTrack = value;
// Cache the current track for easy access
Track track = Image.Tracks[CurrentTrack];
// Set new track-specific data
byte[] flagsData = Image.ReadSectorTag(track.TrackSequence, SectorTagType.CdTrackFlags);
ApplyDeEmphasis = ((CdFlags)flagsData[0]).HasFlag(CdFlags.PreEmphasis);
try
{
byte[] subchannel = Image.ReadSectorTag(track.TrackStartSector, SectorTagType.CdSectorSubchannel);
if(!ApplyDeEmphasis)
ApplyDeEmphasis = (subchannel[3] & 0b01000000) != 0;
CopyAllowed = (subchannel[2] & 0b01000000) != 0;
TrackType = (subchannel[1] & 0b01000000) != 0 ? Aaru.CommonTypes.Enums.TrackType.Data : Aaru.CommonTypes.Enums.TrackType.Audio;
}
catch(ArgumentException)
{
TrackType = track.TrackType;
}
TrackHasEmphasis = ApplyDeEmphasis;
TotalIndexes = track.Indexes.Keys.Max();
CurrentIndex = track.Indexes.Keys.Min();
// If we're not playing data tracks, skip
if(!App.Settings.PlayDataTracks && TrackType != Aaru.CommonTypes.Enums.TrackType.Audio)
{
if(increment)
NextTrack();
else
PreviousTrack();
}
}
}
/// <summary>
/// Current track index
/// </summary>
public ushort CurrentIndex
{
get => _currentIndex;
private set
{
// Unset image means we can't do anything
if(Image == null)
return;
// If the value is the same, don't do anything
if(value == _currentIndex)
return;
// Cache the current track for easy access
Track track = Image.Tracks[CurrentTrack];
// Ensure that the value is valid, wrapping around if necessary
if(value > track.Indexes.Keys.Max())
_currentIndex = 0;
else if(value < 0)
_currentIndex = track.Indexes.Keys.Max();
else
_currentIndex = value;
// Set new index-specific data
SectionStartSector = (ulong)track.Indexes[CurrentIndex];
TotalTime = track.TrackEndSector - track.TrackStartSector;
}
}
/// <summary>
/// Current sector number
/// </summary>
public ulong CurrentSector
{
get => _currentSector;
private set
{
// Unset image means we can't do anything
if(Image == null)
return;
// If the value is the same, don't do anything
if(value == _currentSector)
return;
// Cache the current track for easy access
Track track = Image.Tracks[CurrentTrack];
_currentSector = value;
if((CurrentTrack < Image.Tracks.Count - 1 && CurrentSector >= Image.Tracks[CurrentTrack + 1].TrackStartSector)
|| (CurrentTrack > 0 && CurrentSector < track.TrackStartSector))
{
foreach(Track trackData in Image.Tracks.ToArray().Reverse())
{
if(CurrentSector >= trackData.TrackStartSector)
{
CurrentTrack = (int)trackData.TrackSequence - 1;
break;
}
}
}
foreach((ushort key, int i) in track.Indexes.Reverse())
{
if((int)CurrentSector >= i)
{
CurrentIndex = key;
return;
}
}
CurrentIndex = 0;
}
}
/// <summary>
/// Represents the pre-emphasis flag
/// </summary>
public bool TrackHasEmphasis { get; private set; } = false;
/// <summary>
/// Indicates if de-emphasis should be applied
/// </summary>
public bool ApplyDeEmphasis { get; private set; } = false;
/// <summary>
/// Represents the copy allowed flag
/// </summary>
public bool CopyAllowed { get; private set; } = false;
/// <summary>
/// Represents the track type
/// </summary>
public TrackType? TrackType { get; private set; }
/// <summary>
/// Represents the sector starting the section
/// </summary>
public ulong SectionStartSector { get; private set; }
/// <summary>
/// Represents the total tracks on the disc
/// </summary>
public int TotalTracks { get; private set; } = 0;
/// <summary>
/// Represents the total indices on the disc
/// </summary>
public int TotalIndexes { get; private set; } = 0;
/// <summary>
/// Represents the time adjustment offset for the disc
/// </summary>
public ulong TimeOffset { get; private set; } = 0;
/// <summary>
/// Represents the total playing time for the disc
/// </summary>
public ulong TotalTime { get; private set; } = 0;
/// <summary>
/// Represents the current play volume between 0 and 100
/// </summary>
public int Volume
{
get => _volume;
set
{
if(value >= 0 &&
value <= 100)
_volume = value;
}
}
#endregion
#region Private State Variables
/// <summary>
/// Current track number
/// </summary>
private int _currentTrack = 0;
/// <summary>
/// Current track index
/// </summary>
private ushort _currentIndex = 0;
/// <summary>
/// Current sector number
/// </summary>
private ulong _currentSector = 0;
/// <summary>
/// Current position in the sector
/// </summary>
private int _currentSectorReadPosition = 0;
/// <summary>
/// Current play volume between 0 and 100
/// PlaybableDisc object
/// </summary>
private int _volume = 100;
/// <summary>
/// Current disc table of contents
/// </summary>
private CDFullTOC _toc;
private PlayableDisc _playableDisc;
/// <summary>
/// Data provider for sound output
@@ -295,20 +60,16 @@ namespace RedBookPlayer
/// <summary>
/// Initialize the player with a given image
/// </summary>
/// <param name="image">Aaruformat image to load for playback</param>
/// <param name="disc">Initialized disc image</param>
/// <param name="autoPlay">True if playback should begin immediately, false otherwise</param>
public async void Init(AaruFormat image, bool autoPlay = false)
public void Init(PlayableDisc disc, bool autoPlay = false)
{
// If the image is null, we can't do anything
if(image == null)
// If the disc is not initalized, we can't do anything
if(!disc.Initialized)
return;
// Set the current disc image
Image = image;
// Attempt to load the TOC
if(!await LoadTOC())
return;
// Set the internal reference to the disc
_playableDisc = disc;
// Setup the de-emphasis filters
SetupFilters();
@@ -316,24 +77,9 @@ namespace RedBookPlayer
// Setup the audio output
SetupAudio();
// Load the first track
CurrentTrack = 0;
LoadTrack(0);
// Initialize playback, if necessary
if(autoPlay)
_soundOut.Play();
else
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;
// Set the output volume from settings
Volume = App.Settings.Volume;
// Mark the player as ready
Initialized = true;
@@ -352,7 +98,7 @@ namespace RedBookPlayer
public int ProviderRead(byte[] buffer, int offset, int count)
{
// Set the current volume
_soundOut.Volume = (float)Volume / 100;
_soundOut.Volume = (float)App.Settings.Volume / 100;
// Determine how many sectors we can read
ulong sectorsToRead;
@@ -364,17 +110,17 @@ namespace RedBookPlayer
zeroSectorsAmount = 0;
// Avoid overreads by padding with 0-byte data at the end
if(CurrentSector + sectorsToRead > Image.Info.Sectors)
if(_playableDisc.CurrentSector + sectorsToRead > _playableDisc.TotalSectors)
{
ulong oldSectorsToRead = sectorsToRead;
sectorsToRead = Image.Info.Sectors - CurrentSector;
sectorsToRead = _playableDisc.TotalSectors - _playableDisc.CurrentSector;
zeroSectorsAmount = oldSectorsToRead - sectorsToRead;
}
// TODO: Figure out when this value could be negative
if(sectorsToRead <= 0)
{
LoadTrack(0);
_playableDisc.LoadFirstTrack();
_currentSectorReadPosition = 0;
}
} while(sectorsToRead <= 0);
@@ -390,12 +136,12 @@ namespace RedBookPlayer
{
try
{
return Image.ReadSectors(CurrentSector, (uint)sectorsToRead).Concat(zeroSectors).ToArray();
return _playableDisc.ReadSectors((uint)sectorsToRead).Concat(zeroSectors).ToArray();
}
catch(ArgumentOutOfRangeException)
{
LoadTrack(0);
return Image.ReadSectors(CurrentSector, (uint)sectorsToRead).Concat(zeroSectors).ToArray();
_playableDisc.LoadFirstTrack();
return _playableDisc.ReadSectors((uint)sectorsToRead).Concat(zeroSectors).ToArray();
}
}
});
@@ -416,7 +162,7 @@ namespace RedBookPlayer
Array.Copy(audioData, _currentSectorReadPosition, audioDataSegment, 0, Math.Min(count, audioData.Length - _currentSectorReadPosition));
// Apply de-emphasis filtering, only if enabled
if(ApplyDeEmphasis)
if(_playableDisc.ApplyDeEmphasis)
{
float[][] floatAudioData = new float[2][];
floatAudioData[0] = new float[audioDataSegment.Length / 4];
@@ -439,25 +185,25 @@ namespace RedBookPlayer
_currentSectorReadPosition += count;
if(_currentSectorReadPosition >= 2352)
{
CurrentSector += (ulong)_currentSectorReadPosition / 2352;
_playableDisc.CurrentSector += (ulong)_currentSectorReadPosition / 2352;
_currentSectorReadPosition %= 2352;
}
return count;
}
#region Player Controls
#region Playback
/// <summary>
/// Start audio playback
/// </summary>
public void Play()
{
if(Image == null)
if(!_playableDisc.Initialized)
return;
_soundOut.Play();
TotalIndexes = Image.Tracks[CurrentTrack].Indexes.Keys.Max();
_playableDisc.SetTotalIndexes();
}
/// <summary>
@@ -465,7 +211,7 @@ namespace RedBookPlayer
/// </summary>
public void Pause()
{
if(Image == null)
if(!_playableDisc.Initialized)
return;
_soundOut.Stop();
@@ -476,371 +222,17 @@ namespace RedBookPlayer
/// </summary>
public void Stop()
{
if(Image == null)
if(!_playableDisc.Initialized)
return;
_soundOut.Stop();
LoadTrack(CurrentTrack);
_playableDisc.LoadFirstTrack();
}
/// <summary>
/// Try to move to the next track, wrapping around if necessary
/// </summary>
public void NextTrack()
{
if(Image == null)
return;
CurrentTrack++;
LoadTrack(CurrentTrack);
}
/// <summary>
/// Try to move to the previous track, wrapping around if necessary
/// </summary>
public void PreviousTrack()
{
if(Image == null)
return;
if(CurrentSector < (ulong)Image.Tracks[CurrentTrack].Indexes[1] + 75)
{
if(App.Settings.AllowSkipHiddenTrack && CurrentTrack == 0 && CurrentSector >= 75)
CurrentSector = 0;
else
CurrentTrack--;
}
LoadTrack(CurrentTrack);
}
/// <summary>
/// Try to move to the next track index
/// </summary>
/// <param name="changeTrack">True if index changes can trigger a track change, false otherwise</param>
public void NextIndex(bool changeTrack)
{
if(Image == null)
return;
if(CurrentIndex + 1 > Image.Tracks[CurrentTrack].Indexes.Keys.Max())
{
if(changeTrack)
{
NextTrack();
CurrentSector = (ulong)Image.Tracks[CurrentTrack].Indexes.Values.Min();
}
}
else
{
CurrentSector = (ulong)Image.Tracks[CurrentTrack].Indexes[++CurrentIndex];
}
}
/// <summary>
/// Try to move to the previous track index
/// </summary>
/// <param name="changeTrack">True if index changes can trigger a track change, false otherwise</param>
public void PreviousIndex(bool changeTrack)
{
if(Image == null)
return;
if(CurrentIndex - 1 < Image.Tracks[CurrentTrack].Indexes.Keys.Min())
{
if(changeTrack)
{
PreviousTrack();
CurrentSector = (ulong)Image.Tracks[CurrentTrack].Indexes.Values.Max();
}
}
else
{
CurrentSector = (ulong)Image.Tracks[CurrentTrack].Indexes[--CurrentIndex];
}
}
/// <summary>
/// Fast-forward playback by 75 sectors, if possible
/// </summary>
public void FastForward()
{
if(Image == null)
return;
CurrentSector = Math.Min(Image.Info.Sectors - 1, CurrentSector + 75);
}
/// <summary>
/// Rewind playback by 75 sectors, if possible
/// </summary>
public void Rewind()
{
if(Image == null)
return;
if(CurrentSector >= 75)
CurrentSector -= 75;
}
/// <summary>
/// Toggle de-emphasis processing
/// </summary>
/// <param name="enable">True to apply de-emphasis, false otherwise</param>
public void ToggleDeEmphasis(bool enable) => ApplyDeEmphasis = enable;
#endregion
#region Helpers
/// <summary>
/// Generate a CDFullTOC object from the current image
/// </summary>
/// <returns>CDFullTOC object, if possible</returns>
/// <remarks>Copied from <see cref="Aaru.DiscImages.CloneCd"/></remarks>
private bool GenerateTOC()
{
// Invalid image means we can't generate anything
if(Image == null)
return false;
_toc = new CDFullTOC();
Dictionary<byte, byte> _trackFlags = new Dictionary<byte, byte>();
Dictionary<byte, byte> sessionEndingTrack = new Dictionary<byte, byte>();
_toc.FirstCompleteSession = byte.MaxValue;
_toc.LastCompleteSession = byte.MinValue;
List<TrackDataDescriptor> trackDescriptors = new List<TrackDataDescriptor>();
byte currentTrack = 0;
foreach(Track track in Image.Tracks.OrderBy(t => t.TrackSession).ThenBy(t => t.TrackSequence))
{
byte[] trackFlags = Image.ReadSectorTag(track.TrackStartSector + 1, SectorTagType.CdTrackFlags);
if(trackFlags != null)
_trackFlags.Add((byte)track.TrackStartSector, trackFlags[0]);
}
foreach(Track track in Image.Tracks.OrderBy(t => t.TrackSession).ThenBy(t => t.TrackSequence))
{
if(track.TrackSession < _toc.FirstCompleteSession)
_toc.FirstCompleteSession = (byte)track.TrackSession;
if(track.TrackSession <= _toc.LastCompleteSession)
{
currentTrack = (byte)track.TrackSequence;
continue;
}
if(_toc.LastCompleteSession > 0)
sessionEndingTrack.Add(_toc.LastCompleteSession, currentTrack);
_toc.LastCompleteSession = (byte)track.TrackSession;
}
byte currentSession = 0;
foreach(Track track in Image.Tracks.OrderBy(t => t.TrackSession).ThenBy(t => t.TrackSequence))
{
_trackFlags.TryGetValue((byte)track.TrackSequence, out byte trackControl);
if(trackControl == 0 &&
track.TrackType != Aaru.CommonTypes.Enums.TrackType.Audio)
trackControl = (byte)CdFlags.DataTrack;
// Lead-Out
if(track.TrackSession > currentSession &&
currentSession != 0)
{
(byte minute, byte second, byte frame) leadoutAmsf = LbaToMsf(track.TrackStartSector - 150);
(byte minute, byte second, byte frame) leadoutPmsf =
LbaToMsf(Image.Tracks.OrderBy(t => t.TrackSession).ThenBy(t => t.TrackSequence).Last().
TrackStartSector);
// Lead-out
trackDescriptors.Add(new TrackDataDescriptor
{
SessionNumber = currentSession,
POINT = 0xB0,
ADR = 5,
CONTROL = 0,
HOUR = 0,
Min = leadoutAmsf.minute,
Sec = leadoutAmsf.second,
Frame = leadoutAmsf.frame,
PHOUR = 2,
PMIN = leadoutPmsf.minute,
PSEC = leadoutPmsf.second,
PFRAME = leadoutPmsf.frame
});
// This seems to be constant? It should not exist on CD-ROM but CloneCD creates them anyway
// Format seems like ATIP, but ATIP should not be as 0xC0 in TOC...
//trackDescriptors.Add(new TrackDataDescriptor
//{
// SessionNumber = currentSession,
// POINT = 0xC0,
// ADR = 5,
// CONTROL = 0,
// Min = 128,
// PMIN = 97,
// PSEC = 25
//});
}
// Lead-in
if(track.TrackSession > currentSession)
{
currentSession = (byte)track.TrackSession;
sessionEndingTrack.TryGetValue(currentSession, out byte endingTrackNumber);
(byte minute, byte second, byte frame) leadinPmsf =
LbaToMsf(Image.Tracks.FirstOrDefault(t => t.TrackSequence == endingTrackNumber)?.TrackEndSector ??
0 + 1);
// Starting track
trackDescriptors.Add(new TrackDataDescriptor
{
SessionNumber = currentSession,
POINT = 0xA0,
ADR = 1,
CONTROL = trackControl,
PMIN = (byte)track.TrackSequence
});
// Ending track
trackDescriptors.Add(new TrackDataDescriptor
{
SessionNumber = currentSession,
POINT = 0xA1,
ADR = 1,
CONTROL = trackControl,
PMIN = endingTrackNumber
});
// Lead-out start
trackDescriptors.Add(new TrackDataDescriptor
{
SessionNumber = currentSession,
POINT = 0xA2,
ADR = 1,
CONTROL = trackControl,
PHOUR = 0,
PMIN = leadinPmsf.minute,
PSEC = leadinPmsf.second,
PFRAME = leadinPmsf.frame
});
}
(byte minute, byte second, byte frame) pmsf = LbaToMsf(track.TrackStartSector);
// Track
trackDescriptors.Add(new TrackDataDescriptor
{
SessionNumber = (byte)track.TrackSession,
POINT = (byte)track.TrackSequence,
ADR = 1,
CONTROL = trackControl,
PHOUR = 0,
PMIN = pmsf.minute,
PSEC = pmsf.second,
PFRAME = pmsf.frame
});
}
_toc.TrackDescriptors = trackDescriptors.ToArray();
return true;
}
/// <summary>
/// Convert the sector to LBA values
/// </summary>
/// <param name="sector">Sector to convert</param>
/// <returns>LBA values for the sector number</returns>
/// <remarks>Copied from <see cref="Aaru.DiscImages.CloneCd"/></remarks>
private (byte minute, byte second, byte frame) LbaToMsf(ulong sector) =>
((byte)((sector + 150) / 75 / 60), (byte)((sector + 150) / 75 % 60), (byte)((sector + 150) % 75));
/// <summary>
/// Load TOC for the current disc image
/// </summary>
/// <returns>True if the TOC could be loaded, false otherwise</returns>
private async Task<bool> LoadTOC()
{
if(await Task.Run(() => 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");
if(GenerateTOC())
{
Console.WriteLine(Prettify(_toc));
return true;
}
else
{
Console.WriteLine("Full TOC not found or generated");
return false;
}
}
byte[] tocBytes = await Task.Run(() => 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 = await Task.Run(() => Decode(tocBytes));
if(nullableToc == null)
{
Console.WriteLine("Error decoding TOC");
return false;
}
_toc = nullableToc.Value;
Console.WriteLine(Prettify(_toc));
return true;
}
/// <summary>
/// Load the track for a given track number, if possible
/// </summary>
/// <param name="index">Track number to load</param>
private void LoadTrack(int index)
{
// Save if audio is currently playing
bool oldRun = _source.Run;
// Stop playback if necessary
_source.Stop();
// If it is a valid index, seek to the first, non-negative sectored index for the track
if(index >= 0 && index < Image.Tracks.Count)
{
ushort firstIndex = Image.Tracks[index].Indexes.Keys.Min();
int firstSector = Image.Tracks[index].Indexes[firstIndex];
CurrentSector = (ulong)(firstSector >= 0 ? firstSector : Image.Tracks[index].Indexes[1]);
}
// Reset the playing state
_source.Run = oldRun;
}
/// <summary>
/// Sets or resets the de-emphasis filters
/// </summary>

View File

@@ -20,10 +20,15 @@ namespace RedBookPlayer
public class PlayerView : UserControl
{
/// <summary>
/// Player representing the internal state and loaded image
/// Player representing the internal state
/// </summary>
public static Player Player = new Player();
/// <summary>
/// Disc representing the loaded image
/// </summary>
public static PlayableDisc PlayableDisc = new PlayableDisc();
/// <summary>
/// Set of images representing the digits for the UI
/// </summary>
@@ -66,32 +71,32 @@ namespace RedBookPlayer
/// <returns>String representing the digits for the player</returns>
private string GenerateDigitString()
{
// If the player isn't initialized, return all '-' characters
if (!Player.Initialized)
// If the disc or player aren't initialized, return all '-' characters
if (!PlayableDisc.Initialized || !Player.Initialized)
return string.Empty.PadLeft(20, '-');
// Otherwise, take the current time into account
ulong sectorTime = Player.CurrentSector;
if (Player.SectionStartSector != 0)
sectorTime -= Player.SectionStartSector;
ulong sectorTime = PlayableDisc.CurrentSector;
if (PlayableDisc.SectionStartSector != 0)
sectorTime -= PlayableDisc.SectionStartSector;
else
sectorTime += Player.TimeOffset;
sectorTime += PlayableDisc.TimeOffset;
int[] numbers = new int[]
{
Player.CurrentTrack + 1,
Player.CurrentIndex,
PlayableDisc.CurrentTrackNumber + 1,
PlayableDisc.CurrentTrackIndex,
(int)(sectorTime / (75 * 60)),
(int)(sectorTime / 75 % 60),
(int)(sectorTime % 75),
Player.TotalTracks,
Player.TotalIndexes,
PlayableDisc.TotalTracks,
PlayableDisc.TotalIndexes,
(int)(Player.TotalTime / (75 * 60)),
(int)(Player.TotalTime / 75 % 60),
(int)(Player.TotalTime % 75),
(int)(PlayableDisc.TotalTime / (75 * 60)),
(int)(PlayableDisc.TotalTime / 75 % 60),
(int)(PlayableDisc.TotalTime % 75),
};
return string.Join("", numbers.Select(i => i.ToString().PadLeft(2, '0').Substring(0, 2)));
@@ -232,12 +237,12 @@ namespace RedBookPlayer
if (Player.Initialized)
{
PlayerViewModel dataContext = (PlayerViewModel)DataContext;
dataContext.HiddenTrack = Player.TimeOffset > 150;
dataContext.ApplyDeEmphasis = Player.ApplyDeEmphasis;
dataContext.TrackHasEmphasis = Player.TrackHasEmphasis;
dataContext.CopyAllowed = Player.CopyAllowed;
dataContext.IsAudioTrack = Player.TrackType == TrackType.Audio;
dataContext.IsDataTrack = Player.TrackType != TrackType.Audio;
dataContext.HiddenTrack = PlayableDisc.TimeOffset > 150;
dataContext.ApplyDeEmphasis = PlayableDisc.ApplyDeEmphasis;
dataContext.TrackHasEmphasis = PlayableDisc.TrackHasEmphasis;
dataContext.CopyAllowed = PlayableDisc.CopyAllowed;
dataContext.IsAudioTrack = PlayableDisc.TrackType == TrackType.Audio;
dataContext.IsDataTrack = PlayableDisc.TrackType != TrackType.Audio;
}
});
}
@@ -261,8 +266,14 @@ namespace RedBookPlayer
if (IsPlayableImage(image))
{
Player.Init(image, App.Settings.AutoPlay);
return true;
PlayableDisc.Init(image, App.Settings.AutoPlay);
if (PlayableDisc.Initialized)
{
Player.Init(PlayableDisc, App.Settings.AutoPlay);
return true;
}
return false;
}
else
return false;
@@ -283,21 +294,21 @@ namespace RedBookPlayer
public void StopButton_Click(object sender, RoutedEventArgs e) => Player.Stop();
public void NextTrackButton_Click(object sender, RoutedEventArgs e) => Player.NextTrack();
public void NextTrackButton_Click(object sender, RoutedEventArgs e) => PlayableDisc.NextTrack();
public void PreviousTrackButton_Click(object sender, RoutedEventArgs e) => Player.PreviousTrack();
public void PreviousTrackButton_Click(object sender, RoutedEventArgs e) => PlayableDisc.PreviousTrack();
public void NextIndexButton_Click(object sender, RoutedEventArgs e) => Player.NextIndex(App.Settings.IndexButtonChangeTrack);
public void NextIndexButton_Click(object sender, RoutedEventArgs e) => PlayableDisc.NextIndex(App.Settings.IndexButtonChangeTrack);
public void PreviousIndexButton_Click(object sender, RoutedEventArgs e) => Player.PreviousIndex(App.Settings.IndexButtonChangeTrack);
public void PreviousIndexButton_Click(object sender, RoutedEventArgs e) => PlayableDisc.PreviousIndex(App.Settings.IndexButtonChangeTrack);
public void FastForwardButton_Click(object sender, RoutedEventArgs e) => Player.FastForward();
public void FastForwardButton_Click(object sender, RoutedEventArgs e) => PlayableDisc.FastForward();
public void RewindButton_Click(object sender, RoutedEventArgs e) => Player.Rewind();
public void RewindButton_Click(object sender, RoutedEventArgs e) => PlayableDisc.Rewind();
public void EnableDeEmphasisButton_Click(object sender, RoutedEventArgs e) => Player.ToggleDeEmphasis(true);
public void EnableDeEmphasisButton_Click(object sender, RoutedEventArgs e) => PlayableDisc.ToggleDeEmphasis(true);
public void DisableDeEmphasisButton_Click(object sender, RoutedEventArgs e) => Player.ToggleDeEmphasis(false);
public void DisableDeEmphasisButton_Click(object sender, RoutedEventArgs e) => PlayableDisc.ToggleDeEmphasis(false);
#endregion
}

View File

@@ -36,8 +36,6 @@ namespace RedBookPlayer
MainWindow.ApplyTheme(_selectedTheme);
}
PlayerView.Player.Volume = _settings.Volume;
_settings.Save();
}