mirror of
https://github.com/aaru-dps/RedBookPlayer.git
synced 2025-12-16 19:24:41 +00:00
Merge pull request #23 from mnadareski/player-separation
Separate out internal code more cleanly
This commit is contained in:
2
Aaru
2
Aaru
Submodule Aaru updated: b41b167911...2a6903f866
@@ -33,6 +33,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CUETools.Codecs", "Aaru\cue
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CUETools.Codecs.Flake", "Aaru\cuetools.net\CUETools.Codecs.Flake\CUETools.Codecs.Flake.csproj", "{ED8E11B7-786F-4EFF-9E4C-B937B7A2DE89}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CUETools.Codecs.Flake", "Aaru\cuetools.net\CUETools.Codecs.Flake\CUETools.Codecs.Flake.csproj", "{ED8E11B7-786F-4EFF-9E4C-B937B7A2DE89}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0531C157-8111-4BC9-8C65-A2FDDB0C96FD}"
|
||||||
|
ProjectSection(SolutionItems) = preProject
|
||||||
|
build.bat = build.bat
|
||||||
|
build.sh = build.sh
|
||||||
|
README.md = README.md
|
||||||
|
EndProjectSection
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Avalonia;
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
|
using RedBookPlayer.GUI;
|
||||||
|
|
||||||
namespace RedBookPlayer
|
namespace RedBookPlayer
|
||||||
{
|
{
|
||||||
|
|||||||
406
RedBookPlayer/Discs/CompactDisc.cs
Normal file
406
RedBookPlayer/Discs/CompactDisc.cs
Normal file
@@ -0,0 +1,406 @@
|
|||||||
|
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 static Aaru.Decoders.CD.FullTOC;
|
||||||
|
|
||||||
|
namespace RedBookPlayer.Discs
|
||||||
|
{
|
||||||
|
public class CompactDisc : OpticalDisc
|
||||||
|
{
|
||||||
|
#region Public Fields
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
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 = _currentTrackNumber;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
_currentTrackNumber = cachedValue;
|
||||||
|
|
||||||
|
// Cache the current track for easy access
|
||||||
|
Track track = _image.Tracks[_currentTrackNumber];
|
||||||
|
|
||||||
|
// 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 || App.Settings.PlayDataTracks)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// If we're not playing the track, skip
|
||||||
|
if(increment)
|
||||||
|
cachedValue++;
|
||||||
|
else
|
||||||
|
cachedValue--;
|
||||||
|
}
|
||||||
|
while(cachedValue != cachedTrackNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override 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 4CH flag
|
||||||
|
/// </summary>
|
||||||
|
public bool QuadChannel { get; private set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the DATA flag
|
||||||
|
/// </summary>
|
||||||
|
public bool IsDataTrack => TrackType != TrackType.Audio;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the DCP flag
|
||||||
|
/// </summary>
|
||||||
|
public bool CopyAllowed { get; private set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the PRE flag
|
||||||
|
/// </summary>
|
||||||
|
public bool TrackHasEmphasis { get; private set; } = false;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Private State Variables
|
||||||
|
|
||||||
|
/// <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
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override void Init(IOpticalMediaImage 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(!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
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Helpers
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override void LoadFirstTrack()
|
||||||
|
{
|
||||||
|
CurrentTrackNumber = 0;
|
||||||
|
LoadTrack(CurrentTrackNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override void SetTotalIndexes()
|
||||||
|
{
|
||||||
|
if(_image == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
TotalIndexes = _image.Tracks[CurrentTrackNumber].Indexes.Keys.Max();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Load TOC for the current disc image
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>True if the TOC could be loaded, false otherwise</returns>
|
||||||
|
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(!App.Settings.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<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
|
||||||
|
{
|
||||||
|
_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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set default track flags for the current track
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="track">Track object to read from</param>
|
||||||
|
private void SetDefaultTrackFlags(Track track)
|
||||||
|
{
|
||||||
|
QuadChannel = false;
|
||||||
|
TrackType = track.TrackType;
|
||||||
|
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;
|
||||||
|
TrackType = (flags & (byte)TocControl.DataTrack) == (byte)TocControl.DataTrack ? TrackType.Data : TrackType.Audio;
|
||||||
|
QuadChannel = (flags & (byte)TocControl.FourChanNoPreEmph) == (byte)TocControl.FourChanNoPreEmph;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch(Exception)
|
||||||
|
{
|
||||||
|
SetDefaultTrackFlags(track);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
166
RedBookPlayer/Discs/OpticalDisc.cs
Normal file
166
RedBookPlayer/Discs/OpticalDisc.cs
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
using Aaru.CommonTypes.Enums;
|
||||||
|
using Aaru.CommonTypes.Interfaces;
|
||||||
|
|
||||||
|
namespace RedBookPlayer.Discs
|
||||||
|
{
|
||||||
|
public abstract class OpticalDisc
|
||||||
|
{
|
||||||
|
#region Public Fields
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicate if the disc is ready to be used
|
||||||
|
/// </summary>
|
||||||
|
public bool Initialized { get; protected set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current track number
|
||||||
|
/// </summary>
|
||||||
|
public abstract int CurrentTrackNumber { get; protected set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current track index
|
||||||
|
/// </summary>
|
||||||
|
public abstract ushort CurrentTrackIndex { get; protected set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current sector number
|
||||||
|
/// </summary>
|
||||||
|
public abstract ulong CurrentSector { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the sector starting the section
|
||||||
|
/// </summary>
|
||||||
|
public ulong SectionStartSector { get; protected set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of bytes per sector for the current track
|
||||||
|
/// </summary>
|
||||||
|
public int BytesPerSector => _image.Tracks[CurrentTrackNumber].TrackBytesPerSector;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the track type
|
||||||
|
/// </summary>
|
||||||
|
public TrackType TrackType { get; protected set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the total tracks on the disc
|
||||||
|
/// </summary>
|
||||||
|
public int TotalTracks { get; protected set; } = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the total indices on the disc
|
||||||
|
/// </summary>
|
||||||
|
public int TotalIndexes { get; protected 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; protected set; } = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the total playing time for the disc
|
||||||
|
/// </summary>
|
||||||
|
public ulong TotalTime { get; protected set; } = 0;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Protected State Variables
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Currently loaded disc image
|
||||||
|
/// </summary>
|
||||||
|
protected IOpticalMediaImage _image;
|
||||||
|
|
||||||
|
#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 abstract void Init(IOpticalMediaImage image, bool autoPlay = false);
|
||||||
|
|
||||||
|
#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--;
|
||||||
|
}
|
||||||
|
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 abstract bool NextIndex(bool changeTrack);
|
||||||
|
|
||||||
|
/// <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 abstract bool PreviousIndex(bool changeTrack);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Helpers
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Load the first valid track in the image
|
||||||
|
/// </summary>
|
||||||
|
public abstract void LoadFirstTrack();
|
||||||
|
|
||||||
|
/// <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 abstract void SetTotalIndexes();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Load the desired track, if possible
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="track">Track number to load</param>
|
||||||
|
protected abstract void LoadTrack(int track);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
80
RedBookPlayer/Discs/OpticalDiscFactory.cs
Normal file
80
RedBookPlayer/Discs/OpticalDiscFactory.cs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
using Aaru.CommonTypes.Interfaces;
|
||||||
|
using Aaru.CommonTypes.Metadata;
|
||||||
|
|
||||||
|
namespace RedBookPlayer.Discs
|
||||||
|
{
|
||||||
|
public static class OpticalDiscFactory
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Generate an OpticalDisc from an input IOpticalMediaImage
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="image">IOpticalMediaImage to create from</param>
|
||||||
|
/// <param name="autoPlay">True if the image should be playable immediately, false otherwise</param>
|
||||||
|
/// <returns>Instantiated OpticalDisc, if possible</returns>
|
||||||
|
public static OpticalDisc GenerateFromImage(IOpticalMediaImage image, bool autoPlay)
|
||||||
|
{
|
||||||
|
// If the image is not usable, we don't do anything
|
||||||
|
if(!IsUsableImage(image))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Create the output object
|
||||||
|
OpticalDisc opticalDisc;
|
||||||
|
|
||||||
|
// Create the proper disc type
|
||||||
|
switch(GetMediaType(image))
|
||||||
|
{
|
||||||
|
case "Compact Disc":
|
||||||
|
case "GD":
|
||||||
|
opticalDisc = new CompactDisc();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
opticalDisc = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Null image means we don't do anything
|
||||||
|
if(opticalDisc == null)
|
||||||
|
return opticalDisc;
|
||||||
|
|
||||||
|
// Instantiate the disc and return
|
||||||
|
opticalDisc.Init(image, autoPlay);
|
||||||
|
return opticalDisc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the human-readable media type from an image
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="image">Media image to check</param>
|
||||||
|
/// <returns>Type from the image, empty string on error</returns>
|
||||||
|
/// <remarks>TODO: Can we be more granular with sub types?</remarks>
|
||||||
|
private static string GetMediaType(IOpticalMediaImage image)
|
||||||
|
{
|
||||||
|
// Null image means we don't do anything
|
||||||
|
if(image == null)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
(string type, string _) = MediaType.MediaTypeToString(image.Info.MediaType);
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates if the image is considered "usable" or not
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="image">Aaruformat image file</param>
|
||||||
|
/// <returns>True if the image is playble, false otherwise</returns>
|
||||||
|
private static bool IsUsableImage(IOpticalMediaImage image)
|
||||||
|
{
|
||||||
|
// Invalid images can't be used
|
||||||
|
if(image == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Determine based on media type
|
||||||
|
return GetMediaType(image) switch
|
||||||
|
{
|
||||||
|
"Compact Disc" => true,
|
||||||
|
"GD" => true, // Requires TOC generation
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
|
||||||
x:Class="RedBookPlayer.MainWindow" Title="RedBookPlayer" SizeToContent="WidthAndHeight">
|
x:Class="RedBookPlayer.GUI.MainWindow" Title="RedBookPlayer" SizeToContent="WidthAndHeight"
|
||||||
|
DragDrop.AllowDrop="True">
|
||||||
<ContentControl Name="Content" />
|
<ContentControl Name="Content" />
|
||||||
</Window>
|
</Window>
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
|
|
||||||
namespace RedBookPlayer
|
namespace RedBookPlayer.GUI
|
||||||
{
|
{
|
||||||
public class MainWindow : Window
|
public class MainWindow : Window
|
||||||
{
|
{
|
||||||
@@ -62,15 +63,9 @@ namespace RedBookPlayer
|
|||||||
Instance.Height = ((PlayerView)Instance.ContentControl.Content).Height;
|
Instance.Height = ((PlayerView)Instance.ContentControl.Content).Height;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnKeyDown(object sender, KeyEventArgs e)
|
/// <summary>
|
||||||
{
|
/// Initialize the main window
|
||||||
if(e.Key == Key.F1)
|
/// </summary>
|
||||||
{
|
|
||||||
settingsWindow = new SettingsWindow(App.Settings);
|
|
||||||
settingsWindow.Show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void InitializeComponent()
|
void InitializeComponent()
|
||||||
{
|
{
|
||||||
AvaloniaXamlLoader.Load(this);
|
AvaloniaXamlLoader.Load(this);
|
||||||
@@ -97,6 +92,36 @@ namespace RedBookPlayer
|
|||||||
{
|
{
|
||||||
PlayerView.Player.Stop();
|
PlayerView.Player.Stop();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
AddHandler(DragDrop.DropEvent, MainWindow_Drop);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region Event Handlers
|
||||||
|
|
||||||
|
public async void MainWindow_Drop(object sender, DragEventArgs e)
|
||||||
|
{
|
||||||
|
PlayerView playerView = ContentControl.Content as PlayerView;
|
||||||
|
if(playerView == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
IEnumerable<string> fileNames = e.Data.GetFileNames();
|
||||||
|
foreach(string filename in fileNames)
|
||||||
|
{
|
||||||
|
bool loaded = await playerView.LoadImage(filename);
|
||||||
|
if(loaded)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnKeyDown(object sender, KeyEventArgs e)
|
||||||
|
{
|
||||||
|
if(e.Key == Key.F1)
|
||||||
|
{
|
||||||
|
settingsWindow = new SettingsWindow(App.Settings);
|
||||||
|
settingsWindow.Show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<UserControl xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
<UserControl xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
|
||||||
x:Class="RedBookPlayer.PlayerView" Width="900" Height="400">
|
x:Class="RedBookPlayer.GUI.PlayerView" Width="900" Height="400">
|
||||||
<StackPanel Margin="16" VerticalAlignment="Center">
|
<StackPanel Margin="16" VerticalAlignment="Center">
|
||||||
<Button Click="LoadButton_Click" Margin="32,0,32,16">Load</Button>
|
<Button Click="LoadButton_Click" Margin="32,0,32,16">Load</Button>
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
||||||
@@ -86,8 +86,8 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||||
<TextBlock Margin="0,0,16,0" Foreground="LightGray" IsVisible="{Binding !IsAudioTrack}">AUDIO</TextBlock>
|
<TextBlock Margin="0,0,16,0" Foreground="LightGray" IsVisible="{Binding IsDataTrack}">AUDIO</TextBlock>
|
||||||
<TextBlock Margin="0,0,16,0" IsVisible="{Binding IsAudioTrack}">AUDIO</TextBlock>
|
<TextBlock Margin="0,0,16,0" IsVisible="{Binding !IsDataTrack}">AUDIO</TextBlock>
|
||||||
<TextBlock Margin="0,0,16,0" Foreground="LightGray" IsVisible="{Binding !IsDataTrack}">DATA</TextBlock>
|
<TextBlock Margin="0,0,16,0" Foreground="LightGray" IsVisible="{Binding !IsDataTrack}">DATA</TextBlock>
|
||||||
<TextBlock Margin="0,0,16,0" IsVisible="{Binding IsDataTrack}">DATA</TextBlock>
|
<TextBlock Margin="0,0,16,0" IsVisible="{Binding IsDataTrack}">DATA</TextBlock>
|
||||||
<TextBlock Margin="0,0,16,0" Foreground="LightGray" IsVisible="{Binding !TrackHasEmphasis}">EMPHASIS</TextBlock>
|
<TextBlock Margin="0,0,16,0" Foreground="LightGray" IsVisible="{Binding !TrackHasEmphasis}">EMPHASIS</TextBlock>
|
||||||
@@ -4,10 +4,6 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Timers;
|
using System.Timers;
|
||||||
using Aaru.CommonTypes.Enums;
|
|
||||||
using Aaru.CommonTypes.Interfaces;
|
|
||||||
using Aaru.DiscImages;
|
|
||||||
using Aaru.Filters;
|
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
@@ -15,8 +11,9 @@ using Avalonia.Markup.Xaml;
|
|||||||
using Avalonia.Media.Imaging;
|
using Avalonia.Media.Imaging;
|
||||||
using Avalonia.Platform;
|
using Avalonia.Platform;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
|
using RedBookPlayer.Hardware;
|
||||||
|
|
||||||
namespace RedBookPlayer
|
namespace RedBookPlayer.GUI
|
||||||
{
|
{
|
||||||
public class PlayerView : UserControl
|
public class PlayerView : UserControl
|
||||||
{
|
{
|
||||||
@@ -25,11 +22,6 @@ namespace RedBookPlayer
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static Player Player = new Player();
|
public static Player Player = new Player();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Disc representing the loaded image
|
|
||||||
/// </summary>
|
|
||||||
public static PlayableDisc PlayableDisc = new PlayableDisc();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Set of images representing the digits for the UI
|
/// Set of images representing the digits for the UI
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -56,7 +48,7 @@ namespace RedBookPlayer
|
|||||||
public async Task<string> GetPath()
|
public async Task<string> GetPath()
|
||||||
{
|
{
|
||||||
var dialog = new OpenFileDialog { AllowMultiple = false };
|
var dialog = new OpenFileDialog { AllowMultiple = false };
|
||||||
List<string> knownExtensions = new AaruFormat().KnownExtensions.ToList();
|
List<string> knownExtensions = new Aaru.DiscImages.AaruFormat().KnownExtensions.ToList();
|
||||||
dialog.Filters.Add(new FileDialogFilter()
|
dialog.Filters.Add(new FileDialogFilter()
|
||||||
{
|
{
|
||||||
Name = "Aaru Image Format (*" + string.Join(", *", knownExtensions) + ")",
|
Name = "Aaru Image Format (*" + string.Join(", *", knownExtensions) + ")",
|
||||||
@@ -67,40 +59,26 @@ namespace RedBookPlayer
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generate the digit string to be interpreted by the UI
|
/// Load an image from the path
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>String representing the digits for the player</returns>
|
/// <param name="path">Path to the image to load</param>
|
||||||
private string GenerateDigitString()
|
public async Task<bool> LoadImage(string path)
|
||||||
{
|
{
|
||||||
// If the disc or player aren't initialized, return all '-' characters
|
bool result = await Task.Run(() =>
|
||||||
if (!PlayableDisc.Initialized)
|
|
||||||
return string.Empty.PadLeft(20, '-');
|
|
||||||
|
|
||||||
// Otherwise, take the current time into account
|
|
||||||
ulong sectorTime = PlayableDisc.CurrentSector;
|
|
||||||
if (PlayableDisc.SectionStartSector != 0)
|
|
||||||
sectorTime -= PlayableDisc.SectionStartSector;
|
|
||||||
else
|
|
||||||
sectorTime += PlayableDisc.TimeOffset;
|
|
||||||
|
|
||||||
int[] numbers = new int[]
|
|
||||||
{
|
{
|
||||||
PlayableDisc.CurrentTrackNumber + 1,
|
Player.Init(path, App.Settings.AutoPlay);
|
||||||
PlayableDisc.CurrentTrackIndex,
|
return Player.Initialized;
|
||||||
|
});
|
||||||
|
|
||||||
(int)(sectorTime / (75 * 60)),
|
if(result)
|
||||||
(int)(sectorTime / 75 % 60),
|
{
|
||||||
(int)(sectorTime % 75),
|
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
MainWindow.Instance.Title = "RedBookPlayer - " + path.Split('/').Last().Split('\\').Last();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
PlayableDisc.TotalTracks,
|
return result;
|
||||||
PlayableDisc.TotalIndexes,
|
|
||||||
|
|
||||||
(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)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -127,10 +105,44 @@ namespace RedBookPlayer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialize the UI based on the currently selected theme
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="xaml">XAML data representing the theme, null for default</param>
|
||||||
|
private void InitializeComponent(string xaml)
|
||||||
|
{
|
||||||
|
DataContext = new PlayerViewModel();
|
||||||
|
|
||||||
|
// Load the theme
|
||||||
|
if (xaml != null)
|
||||||
|
new AvaloniaXamlLoader().Load(xaml, null, this);
|
||||||
|
else
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
|
||||||
|
InitializeDigits();
|
||||||
|
|
||||||
|
_updateTimer = new Timer(1000 / 60);
|
||||||
|
|
||||||
|
_updateTimer.Elapsed += (sender, e) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
UpdateView(sender, e);
|
||||||
|
}
|
||||||
|
catch(Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine(ex);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_updateTimer.AutoReset = true;
|
||||||
|
_updateTimer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initialize the displayed digits array
|
/// Initialize the displayed digits array
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void Initialize()
|
private void InitializeDigits()
|
||||||
{
|
{
|
||||||
_digits = new Image[]
|
_digits = new Image[]
|
||||||
{
|
{
|
||||||
@@ -162,98 +174,6 @@ namespace RedBookPlayer
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initialize the UI based on the currently selected theme
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="xaml">XAML data representing the theme, null for default</param>
|
|
||||||
private void InitializeComponent(string xaml)
|
|
||||||
{
|
|
||||||
DataContext = new PlayerViewModel();
|
|
||||||
|
|
||||||
if (xaml != null)
|
|
||||||
new AvaloniaXamlLoader().Load(xaml, null, this);
|
|
||||||
else
|
|
||||||
AvaloniaXamlLoader.Load(this);
|
|
||||||
|
|
||||||
Initialize();
|
|
||||||
|
|
||||||
_updateTimer = new Timer(1000 / 60);
|
|
||||||
|
|
||||||
_updateTimer.Elapsed += (sender, e) =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
UpdateView(sender, e);
|
|
||||||
}
|
|
||||||
catch(Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine(ex);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
_updateTimer.AutoReset = true;
|
|
||||||
_updateTimer.Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicates if the image is considered "playable" or not
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="image">Aaruformat image file</param>
|
|
||||||
/// <returns>True if the image is playble, false otherwise</returns>
|
|
||||||
private bool IsPlayableImage(IOpticalMediaImage image)
|
|
||||||
{
|
|
||||||
// Invalid images can't be played
|
|
||||||
if (image == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Determine based on media type
|
|
||||||
// TODO: Can we be more granular with sub types?
|
|
||||||
(string type, string _) = Aaru.CommonTypes.Metadata.MediaType.MediaTypeToString(image.Info.MediaType);
|
|
||||||
return type switch
|
|
||||||
{
|
|
||||||
"Compact Disc" => true,
|
|
||||||
"GD" => true, // Requires TOC generation
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Load an image from the path
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">Path to the image to load</param>
|
|
||||||
private async void LoadImage(string path)
|
|
||||||
{
|
|
||||||
bool result = await Task.Run(() =>
|
|
||||||
{
|
|
||||||
var image = new AaruFormat();
|
|
||||||
var filter = new ZZZNoFilter();
|
|
||||||
filter.Open(path);
|
|
||||||
image.Open(filter);
|
|
||||||
|
|
||||||
if(IsPlayableImage(image))
|
|
||||||
{
|
|
||||||
PlayableDisc.Init(image, App.Settings.AutoPlay);
|
|
||||||
if(PlayableDisc.Initialized)
|
|
||||||
{
|
|
||||||
Player.Init(PlayableDisc, App.Settings.AutoPlay);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
if(result)
|
|
||||||
{
|
|
||||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
|
||||||
{
|
|
||||||
MainWindow.Instance.Title = "RedBookPlayer - " + path.Split('/').Last().Split('\\').Last();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update the UI with the most recent information from the Player
|
/// Update the UI with the most recent information from the Player
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -261,24 +181,14 @@ namespace RedBookPlayer
|
|||||||
{
|
{
|
||||||
Dispatcher.UIThread.InvokeAsync(() =>
|
Dispatcher.UIThread.InvokeAsync(() =>
|
||||||
{
|
{
|
||||||
string digitString = GenerateDigitString();
|
string digitString = Player.GenerateDigitString();
|
||||||
for (int i = 0; i < _digits.Length; i++)
|
for (int i = 0; i < _digits.Length; i++)
|
||||||
{
|
{
|
||||||
if (_digits[i] != null)
|
if (_digits[i] != null)
|
||||||
_digits[i].Source = GetBitmap(digitString[i]);
|
_digits[i].Source = GetBitmap(digitString[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Player.Initialized)
|
Player.UpdateDataContext(DataContext as PlayerViewModel);
|
||||||
{
|
|
||||||
PlayerViewModel dataContext = (PlayerViewModel)DataContext;
|
|
||||||
dataContext.HiddenTrack = PlayableDisc.TimeOffset > 150;
|
|
||||||
dataContext.ApplyDeEmphasis = PlayableDisc.ApplyDeEmphasis;
|
|
||||||
dataContext.TrackHasEmphasis = PlayableDisc.TrackHasEmphasis;
|
|
||||||
dataContext.CopyAllowed = PlayableDisc.CopyAllowed;
|
|
||||||
dataContext.QuadChannel = PlayableDisc.QuadChannel;
|
|
||||||
dataContext.IsAudioTrack = PlayableDisc.TrackType == TrackType.Audio;
|
|
||||||
dataContext.IsDataTrack = PlayableDisc.TrackType != TrackType.Audio;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,51 +205,29 @@ namespace RedBookPlayer
|
|||||||
LoadImage(path);
|
LoadImage(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void PlayButton_Click(object sender, RoutedEventArgs e) => Player.Play();
|
public void PlayButton_Click(object sender, RoutedEventArgs e) => Player.TogglePlayPause(true);
|
||||||
|
|
||||||
public void PauseButton_Click(object sender, RoutedEventArgs e) => Player.Pause();
|
public void PauseButton_Click(object sender, RoutedEventArgs e) => Player.TogglePlayPause(false);
|
||||||
|
|
||||||
|
public void PlayPauseButton_Click(object sender, RoutedEventArgs e) => Player.TogglePlayPause(!Player.Playing);
|
||||||
|
|
||||||
public void StopButton_Click(object sender, RoutedEventArgs e) => Player.Stop();
|
public void StopButton_Click(object sender, RoutedEventArgs e) => Player.Stop();
|
||||||
|
|
||||||
public void NextTrackButton_Click(object sender, RoutedEventArgs e)
|
public void NextTrackButton_Click(object sender, RoutedEventArgs e) => Player.NextTrack();
|
||||||
{
|
|
||||||
bool wasPlaying = Player.Playing;
|
|
||||||
if(wasPlaying) Player.Pause();
|
|
||||||
PlayableDisc.NextTrack();
|
|
||||||
if(wasPlaying) Player.Play();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void PreviousTrackButton_Click(object sender, RoutedEventArgs e)
|
public void PreviousTrackButton_Click(object sender, RoutedEventArgs e) => Player.PreviousTrack();
|
||||||
{
|
|
||||||
bool wasPlaying = Player.Playing;
|
|
||||||
if(wasPlaying) Player.Pause();
|
|
||||||
PlayableDisc.PreviousTrack();
|
|
||||||
if(wasPlaying) Player.Play();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void NextIndexButton_Click(object sender, RoutedEventArgs e)
|
public void NextIndexButton_Click(object sender, RoutedEventArgs e) => Player.NextIndex(App.Settings.IndexButtonChangeTrack);
|
||||||
{
|
|
||||||
bool wasPlaying = Player.Playing;
|
|
||||||
if(wasPlaying) Player.Pause();
|
|
||||||
PlayableDisc.NextIndex(App.Settings.IndexButtonChangeTrack);
|
|
||||||
if(wasPlaying) Player.Play();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void PreviousIndexButton_Click(object sender, RoutedEventArgs e)
|
public void PreviousIndexButton_Click(object sender, RoutedEventArgs e) => Player.PreviousIndex(App.Settings.IndexButtonChangeTrack);
|
||||||
{
|
|
||||||
bool wasPlaying = Player.Playing;
|
|
||||||
if(wasPlaying) Player.Pause();
|
|
||||||
PlayableDisc.PreviousIndex(App.Settings.IndexButtonChangeTrack);
|
|
||||||
if(wasPlaying) Player.Play();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void FastForwardButton_Click(object sender, RoutedEventArgs e) => PlayableDisc.FastForward();
|
public void FastForwardButton_Click(object sender, RoutedEventArgs e) => Player.FastForward();
|
||||||
|
|
||||||
public void RewindButton_Click(object sender, RoutedEventArgs e) => PlayableDisc.Rewind();
|
public void RewindButton_Click(object sender, RoutedEventArgs e) => Player.Rewind();
|
||||||
|
|
||||||
public void EnableDeEmphasisButton_Click(object sender, RoutedEventArgs e) => PlayableDisc.ToggleDeEmphasis(true);
|
public void EnableDeEmphasisButton_Click(object sender, RoutedEventArgs e) => Player.ToggleDeEmphasis(true);
|
||||||
|
|
||||||
public void DisableDeEmphasisButton_Click(object sender, RoutedEventArgs e) => PlayableDisc.ToggleDeEmphasis(false);
|
public void DisableDeEmphasisButton_Click(object sender, RoutedEventArgs e) => Player.ToggleDeEmphasis(false);
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
|
||||||
namespace RedBookPlayer
|
namespace RedBookPlayer.GUI
|
||||||
{
|
{
|
||||||
public class PlayerViewModel : ReactiveObject
|
public class PlayerViewModel : ReactiveObject
|
||||||
{
|
{
|
||||||
@@ -11,6 +11,27 @@ namespace RedBookPlayer
|
|||||||
set => this.RaiseAndSetIfChanged(ref _applyDeEmphasis, value);
|
set => this.RaiseAndSetIfChanged(ref _applyDeEmphasis, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool _quadChannel;
|
||||||
|
public bool QuadChannel
|
||||||
|
{
|
||||||
|
get => _quadChannel;
|
||||||
|
set => this.RaiseAndSetIfChanged(ref _quadChannel, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _isDataTrack;
|
||||||
|
public bool IsDataTrack
|
||||||
|
{
|
||||||
|
get => _isDataTrack;
|
||||||
|
set => this.RaiseAndSetIfChanged(ref _isDataTrack, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _copyAllowed;
|
||||||
|
public bool CopyAllowed
|
||||||
|
{
|
||||||
|
get => _copyAllowed;
|
||||||
|
set => this.RaiseAndSetIfChanged(ref _copyAllowed, value);
|
||||||
|
}
|
||||||
|
|
||||||
private bool _trackHasEmphasis;
|
private bool _trackHasEmphasis;
|
||||||
public bool TrackHasEmphasis
|
public bool TrackHasEmphasis
|
||||||
{
|
{
|
||||||
@@ -24,33 +45,5 @@ namespace RedBookPlayer
|
|||||||
get => _hiddenTrack;
|
get => _hiddenTrack;
|
||||||
set => this.RaiseAndSetIfChanged(ref _hiddenTrack, value);
|
set => this.RaiseAndSetIfChanged(ref _hiddenTrack, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool _copyAllowed;
|
|
||||||
public bool CopyAllowed
|
|
||||||
{
|
|
||||||
get => _copyAllowed;
|
|
||||||
set => this.RaiseAndSetIfChanged(ref _copyAllowed, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool _quadChannel;
|
|
||||||
public bool QuadChannel
|
|
||||||
{
|
|
||||||
get => _quadChannel;
|
|
||||||
set => this.RaiseAndSetIfChanged(ref _quadChannel, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool _isAudioTrack;
|
|
||||||
public bool IsAudioTrack
|
|
||||||
{
|
|
||||||
get => _isAudioTrack;
|
|
||||||
set => this.RaiseAndSetIfChanged(ref _isAudioTrack, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool _isDataTrack;
|
|
||||||
public bool IsDataTrack
|
|
||||||
{
|
|
||||||
get => _isDataTrack;
|
|
||||||
set => this.RaiseAndSetIfChanged(ref _isDataTrack, value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800"
|
||||||
d:DesignHeight="450" x:Class="RedBookPlayer.SettingsWindow" Title="Settings" Width="450" Height="600">
|
d:DesignHeight="450" x:Class="RedBookPlayer.GUI.SettingsWindow" Title="Settings" Width="450" Height="600">
|
||||||
<DockPanel Margin="16">
|
<DockPanel Margin="16">
|
||||||
<TextBlock DockPanel.Dock="Top" Margin="0,0,0,4">Themes</TextBlock>
|
<TextBlock DockPanel.Dock="Top" Margin="0,0,0,4">Themes</TextBlock>
|
||||||
<StackPanel DockPanel.Dock="Bottom">
|
<StackPanel DockPanel.Dock="Bottom">
|
||||||
@@ -4,7 +4,7 @@ using Avalonia.Controls;
|
|||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
|
|
||||||
namespace RedBookPlayer
|
namespace RedBookPlayer.GUI
|
||||||
{
|
{
|
||||||
public class SettingsWindow : Window
|
public class SettingsWindow : Window
|
||||||
{
|
{
|
||||||
@@ -45,29 +45,41 @@ namespace RedBookPlayer
|
|||||||
{
|
{
|
||||||
AvaloniaXamlLoader.Load(this);
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
|
||||||
_themeList = this.FindControl<ListBox>("ThemeList");
|
PopulateThemes();
|
||||||
_themeList.SelectionChanged += ThemeList_SelectionChanged;
|
|
||||||
|
|
||||||
List<string> items = new List<string>();
|
|
||||||
items.Add("default");
|
|
||||||
|
|
||||||
if(Directory.Exists("themes/"))
|
|
||||||
{
|
|
||||||
foreach(string dir in Directory.EnumerateDirectories("themes/"))
|
|
||||||
{
|
|
||||||
string themeName = dir.Split('/')[1];
|
|
||||||
|
|
||||||
if (!File.Exists($"themes/{themeName}/view.xaml"))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
items.Add(themeName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_themeList.Items = items;
|
|
||||||
|
|
||||||
this.FindControl<Button>("ApplyButton").Click += ApplySettings;
|
this.FindControl<Button>("ApplyButton").Click += ApplySettings;
|
||||||
this.FindControl<Slider>("VolumeSlider").PropertyChanged += (s, e) => UpdateView();
|
this.FindControl<Slider>("VolumeSlider").PropertyChanged += (s, e) => UpdateView();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Populate the list of themes
|
||||||
|
/// </summary>
|
||||||
|
private void PopulateThemes()
|
||||||
|
{
|
||||||
|
// Get a reference to the theme list
|
||||||
|
_themeList = this.FindControl<ListBox>("ThemeList");
|
||||||
|
_themeList.SelectionChanged += ThemeList_SelectionChanged;
|
||||||
|
|
||||||
|
// Create a list of all found themes
|
||||||
|
List<string> items = new List<string>();
|
||||||
|
items.Add("default");
|
||||||
|
|
||||||
|
// Ensure the theme directory exists
|
||||||
|
if(!Directory.Exists("themes/"))
|
||||||
|
Directory.CreateDirectory("themes/");
|
||||||
|
|
||||||
|
// Add all theme directories if they're valid
|
||||||
|
foreach(string dir in Directory.EnumerateDirectories("themes/"))
|
||||||
|
{
|
||||||
|
string themeName = dir.Split('/')[1];
|
||||||
|
|
||||||
|
if(!File.Exists($"themes/{themeName}/view.xaml"))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
items.Add(themeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
_themeList.Items = items;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using NWaves.Filters.BiQuad;
|
using NWaves.Filters.BiQuad;
|
||||||
|
|
||||||
namespace RedBookPlayer
|
namespace RedBookPlayer.Hardware
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Filter for applying de-emphasis to audio
|
/// Filter for applying de-emphasis to audio
|
||||||
293
RedBookPlayer/Hardware/Player.cs
Normal file
293
RedBookPlayer/Hardware/Player.cs
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using Aaru.CommonTypes.Enums;
|
||||||
|
using Aaru.DiscImages;
|
||||||
|
using Aaru.Filters;
|
||||||
|
using RedBookPlayer.Discs;
|
||||||
|
using RedBookPlayer.GUI;
|
||||||
|
|
||||||
|
namespace RedBookPlayer.Hardware
|
||||||
|
{
|
||||||
|
public class Player
|
||||||
|
{
|
||||||
|
#region Public Fields
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicate if the player is ready to be used
|
||||||
|
/// </summary>
|
||||||
|
public bool Initialized { get; private set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicate if the disc is playing
|
||||||
|
/// </summary>
|
||||||
|
public bool Playing => _soundOutput?.Playing ?? false;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Private State Variables
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// OpticalDisc object
|
||||||
|
/// </summary>
|
||||||
|
private OpticalDisc _opticalDisc;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sound output handling class
|
||||||
|
/// </summary>
|
||||||
|
public SoundOutput _soundOutput;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialize the player with a given image path
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">Path to the disc image</param>
|
||||||
|
/// <param name="autoPlay">True if playback should begin immediately, false otherwise</param>
|
||||||
|
public void Init(string path, bool autoPlay = false)
|
||||||
|
{
|
||||||
|
// Reset the internal state for initialization
|
||||||
|
Initialized = false;
|
||||||
|
_soundOutput = new SoundOutput();
|
||||||
|
_soundOutput.ApplyDeEmphasis = false;
|
||||||
|
_opticalDisc = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Validate the image exists
|
||||||
|
if(string.IsNullOrWhiteSpace(path) || !File.Exists(path))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Load the disc image to memory
|
||||||
|
var image = new AaruFormat();
|
||||||
|
var filter = new ZZZNoFilter();
|
||||||
|
filter.Open(path);
|
||||||
|
image.Open(filter);
|
||||||
|
|
||||||
|
// Generate and instantiate the disc
|
||||||
|
_opticalDisc = OpticalDiscFactory.GenerateFromImage(image, App.Settings.AutoPlay);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// All errors mean an invalid image in some way
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the sound output
|
||||||
|
_soundOutput.Init(_opticalDisc, autoPlay);
|
||||||
|
if(_soundOutput == null || !_soundOutput.Initialized)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Mark the player as ready
|
||||||
|
Initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Playback
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Toggle audio playback
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="start">True to start playback, false to pause</param>
|
||||||
|
public void TogglePlayPause(bool start)
|
||||||
|
{
|
||||||
|
if(_opticalDisc == null || !_opticalDisc.Initialized)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if(start)
|
||||||
|
{
|
||||||
|
_soundOutput.Play();
|
||||||
|
_opticalDisc.SetTotalIndexes();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_soundOutput.Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stop the current audio playback
|
||||||
|
/// </summary>
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
if(_opticalDisc == null || !_opticalDisc.Initialized)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_soundOutput.Stop();
|
||||||
|
_opticalDisc.LoadFirstTrack();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Move to the next playable track
|
||||||
|
/// </summary>
|
||||||
|
public void NextTrack()
|
||||||
|
{
|
||||||
|
if(_opticalDisc == null || !_opticalDisc.Initialized)
|
||||||
|
return;
|
||||||
|
|
||||||
|
bool wasPlaying = Playing;
|
||||||
|
if(wasPlaying) TogglePlayPause(false);
|
||||||
|
|
||||||
|
_opticalDisc.NextTrack();
|
||||||
|
if(_opticalDisc is CompactDisc compactDisc)
|
||||||
|
_soundOutput.ApplyDeEmphasis = compactDisc.TrackHasEmphasis;
|
||||||
|
|
||||||
|
if(wasPlaying) TogglePlayPause(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Move to the previous playable track
|
||||||
|
/// </summary>
|
||||||
|
public void PreviousTrack()
|
||||||
|
{
|
||||||
|
if(_opticalDisc == null || !_opticalDisc.Initialized)
|
||||||
|
return;
|
||||||
|
|
||||||
|
bool wasPlaying = Playing;
|
||||||
|
if(wasPlaying) TogglePlayPause(false);
|
||||||
|
|
||||||
|
_opticalDisc.PreviousTrack();
|
||||||
|
if(_opticalDisc is CompactDisc compactDisc)
|
||||||
|
_soundOutput.ApplyDeEmphasis = compactDisc.TrackHasEmphasis;
|
||||||
|
|
||||||
|
if(wasPlaying) TogglePlayPause(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Move to the next index
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="changeTrack">True if index changes can trigger a track change, false otherwise</param>
|
||||||
|
public void NextIndex(bool changeTrack)
|
||||||
|
{
|
||||||
|
if(_opticalDisc == null || !_opticalDisc.Initialized)
|
||||||
|
return;
|
||||||
|
|
||||||
|
bool wasPlaying = Playing;
|
||||||
|
if(wasPlaying) TogglePlayPause(false);
|
||||||
|
|
||||||
|
_opticalDisc.NextIndex(changeTrack);
|
||||||
|
if(_opticalDisc is CompactDisc compactDisc)
|
||||||
|
_soundOutput.ApplyDeEmphasis = compactDisc.TrackHasEmphasis;
|
||||||
|
|
||||||
|
if(wasPlaying) TogglePlayPause(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Move to the previous index
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="changeTrack">True if index changes can trigger a track change, false otherwise</param>
|
||||||
|
public void PreviousIndex(bool changeTrack)
|
||||||
|
{
|
||||||
|
if(_opticalDisc == null || !_opticalDisc.Initialized)
|
||||||
|
return;
|
||||||
|
|
||||||
|
bool wasPlaying = Playing;
|
||||||
|
if(wasPlaying) TogglePlayPause(false);
|
||||||
|
|
||||||
|
_opticalDisc.PreviousIndex(changeTrack);
|
||||||
|
if(_opticalDisc is CompactDisc compactDisc)
|
||||||
|
_soundOutput.ApplyDeEmphasis = compactDisc.TrackHasEmphasis;
|
||||||
|
|
||||||
|
if(wasPlaying) TogglePlayPause(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fast-forward playback by 75 sectors, if possible
|
||||||
|
/// </summary>
|
||||||
|
public void FastForward()
|
||||||
|
{
|
||||||
|
if(_opticalDisc == null || !_opticalDisc.Initialized)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_opticalDisc.CurrentSector = Math.Min(_opticalDisc.TotalSectors, _opticalDisc.CurrentSector + 75);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rewind playback by 75 sectors, if possible
|
||||||
|
/// </summary>
|
||||||
|
public void Rewind()
|
||||||
|
{
|
||||||
|
if(_opticalDisc == null || !_opticalDisc.Initialized)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if(_opticalDisc.CurrentSector >= 75)
|
||||||
|
_opticalDisc.CurrentSector -= 75;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Helpers
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate the digit string to be interpreted by the frontend
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>String representing the digits for the frontend</returns>
|
||||||
|
public string GenerateDigitString()
|
||||||
|
{
|
||||||
|
// If the disc isn't initialized, return all '-' characters
|
||||||
|
if(_opticalDisc == null || !_opticalDisc.Initialized)
|
||||||
|
return string.Empty.PadLeft(20, '-');
|
||||||
|
|
||||||
|
// Otherwise, take the current time into account
|
||||||
|
ulong sectorTime = _opticalDisc.CurrentSector;
|
||||||
|
if(_opticalDisc.SectionStartSector != 0)
|
||||||
|
sectorTime -= _opticalDisc.SectionStartSector;
|
||||||
|
else
|
||||||
|
sectorTime += _opticalDisc.TimeOffset;
|
||||||
|
|
||||||
|
int[] numbers = new int[]
|
||||||
|
{
|
||||||
|
_opticalDisc.CurrentTrackNumber + 1,
|
||||||
|
_opticalDisc.CurrentTrackIndex,
|
||||||
|
|
||||||
|
(int)(sectorTime / (75 * 60)),
|
||||||
|
(int)(sectorTime / 75 % 60),
|
||||||
|
(int)(sectorTime % 75),
|
||||||
|
|
||||||
|
_opticalDisc.TotalTracks,
|
||||||
|
_opticalDisc.TotalIndexes,
|
||||||
|
|
||||||
|
(int)(_opticalDisc.TotalTime / (75 * 60)),
|
||||||
|
(int)(_opticalDisc.TotalTime / 75 % 60),
|
||||||
|
(int)(_opticalDisc.TotalTime % 75),
|
||||||
|
};
|
||||||
|
|
||||||
|
return string.Join("", numbers.Select(i => i.ToString().PadLeft(2, '0').Substring(0, 2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Toggle de-emphasis processing
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="enable">True to apply de-emphasis, false otherwise</param>
|
||||||
|
public void ToggleDeEmphasis(bool enable) => _soundOutput.ToggleDeEmphasis(enable);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update the data context for the frontend
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dataContext">Data context to be updated</param>
|
||||||
|
public void UpdateDataContext(PlayerViewModel dataContext)
|
||||||
|
{
|
||||||
|
if(!Initialized || dataContext == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
dataContext.HiddenTrack = _opticalDisc.TimeOffset > 150;
|
||||||
|
dataContext.ApplyDeEmphasis = _soundOutput.ApplyDeEmphasis;
|
||||||
|
|
||||||
|
if(_opticalDisc is CompactDisc compactDisc)
|
||||||
|
{
|
||||||
|
dataContext.QuadChannel = compactDisc.QuadChannel;
|
||||||
|
dataContext.IsDataTrack = compactDisc.IsDataTrack;
|
||||||
|
dataContext.CopyAllowed = compactDisc.CopyAllowed;
|
||||||
|
dataContext.TrackHasEmphasis = compactDisc.TrackHasEmphasis;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dataContext.QuadChannel = false;
|
||||||
|
dataContext.IsDataTrack = _opticalDisc.TrackType != TrackType.Audio;
|
||||||
|
dataContext.CopyAllowed = false;
|
||||||
|
dataContext.TrackHasEmphasis = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ using System;
|
|||||||
using CSCore;
|
using CSCore;
|
||||||
using WaveFormat = CSCore.WaveFormat;
|
using WaveFormat = CSCore.WaveFormat;
|
||||||
|
|
||||||
namespace RedBookPlayer
|
namespace RedBookPlayer.Hardware
|
||||||
{
|
{
|
||||||
public class PlayerSource : IWaveSource
|
public class PlayerSource : IWaveSource
|
||||||
{
|
{
|
||||||
@@ -4,20 +4,26 @@ using System.Threading.Tasks;
|
|||||||
using CSCore.SoundOut;
|
using CSCore.SoundOut;
|
||||||
using NWaves.Audio;
|
using NWaves.Audio;
|
||||||
using NWaves.Filters.BiQuad;
|
using NWaves.Filters.BiQuad;
|
||||||
|
using RedBookPlayer.Discs;
|
||||||
|
|
||||||
namespace RedBookPlayer
|
namespace RedBookPlayer.Hardware
|
||||||
{
|
{
|
||||||
public class Player
|
public class SoundOutput
|
||||||
{
|
{
|
||||||
#region Public Fields
|
#region Public Fields
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicate if the player is ready to be used
|
/// Indicate if the output is ready to be used
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Initialized { get; private set; } = false;
|
public bool Initialized { get; private set; } = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicate if the disc is playing
|
/// Indicates if de-emphasis should be applied
|
||||||
|
/// </summary>
|
||||||
|
public bool ApplyDeEmphasis { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicate if the output is playing
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Playing => _soundOut.PlaybackState == PlaybackState.Playing;
|
public bool Playing => _soundOut.PlaybackState == PlaybackState.Playing;
|
||||||
|
|
||||||
@@ -31,9 +37,12 @@ namespace RedBookPlayer
|
|||||||
private int _currentSectorReadPosition = 0;
|
private int _currentSectorReadPosition = 0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// PlaybableDisc object
|
/// OpticalDisc from the parent player for easy access
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private PlayableDisc _playableDisc;
|
/// <remarks>
|
||||||
|
/// TODO: Can we remove the need for a local reference to OpticalDisc?
|
||||||
|
/// </remarks>
|
||||||
|
private OpticalDisc _opticalDisc;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data provider for sound output
|
/// Data provider for sound output
|
||||||
@@ -63,20 +72,24 @@ namespace RedBookPlayer
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initialize the player with a given image
|
/// Initialize the output with a given image
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="disc">Initialized disc image</param>
|
/// <param name="opticalDisc">OpticalDisc to load from</param>
|
||||||
/// <param name="autoPlay">True if playback should begin immediately, false otherwise</param>
|
/// <param name="autoPlay">True if playback should begin immediately, false otherwise</param>
|
||||||
public void Init(PlayableDisc disc, bool autoPlay = false)
|
public void Init(OpticalDisc opticalDisc, bool autoPlay = false)
|
||||||
{
|
{
|
||||||
// If the disc is not initalized, we can't do anything
|
// If we have an unusable disc, just return
|
||||||
if(!disc.Initialized)
|
if(opticalDisc == null || !opticalDisc.Initialized)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Set the internal reference to the disc
|
// Save a reference to the disc
|
||||||
_playableDisc = disc;
|
_opticalDisc = opticalDisc;
|
||||||
|
|
||||||
// Setup the de-emphasis filters
|
// Enable de-emphasis for CDs, if necessary
|
||||||
|
if(opticalDisc is CompactDisc compactDisc)
|
||||||
|
ApplyDeEmphasis = compactDisc.TrackHasEmphasis;
|
||||||
|
|
||||||
|
// Setup de-emphasis filters
|
||||||
SetupFilters();
|
SetupFilters();
|
||||||
|
|
||||||
// Setup the audio output
|
// Setup the audio output
|
||||||
@@ -86,7 +99,7 @@ namespace RedBookPlayer
|
|||||||
if(autoPlay)
|
if(autoPlay)
|
||||||
_soundOut.Play();
|
_soundOut.Play();
|
||||||
|
|
||||||
// Mark the player as ready
|
// Mark the output as ready
|
||||||
Initialized = true;
|
Initialized = true;
|
||||||
|
|
||||||
// Begin loading data
|
// Begin loading data
|
||||||
@@ -111,27 +124,29 @@ namespace RedBookPlayer
|
|||||||
do
|
do
|
||||||
{
|
{
|
||||||
// Attempt to read 2 more sectors than requested
|
// Attempt to read 2 more sectors than requested
|
||||||
sectorsToRead = ((ulong)count / 2352) + 2;
|
sectorsToRead = ((ulong)count / (ulong)_opticalDisc.BytesPerSector) + 2;
|
||||||
zeroSectorsAmount = 0;
|
zeroSectorsAmount = 0;
|
||||||
|
|
||||||
// Avoid overreads by padding with 0-byte data at the end
|
// Avoid overreads by padding with 0-byte data at the end
|
||||||
if(_playableDisc.CurrentSector + sectorsToRead > _playableDisc.TotalSectors)
|
if(_opticalDisc.CurrentSector + sectorsToRead > _opticalDisc.TotalSectors)
|
||||||
{
|
{
|
||||||
ulong oldSectorsToRead = sectorsToRead;
|
ulong oldSectorsToRead = sectorsToRead;
|
||||||
sectorsToRead = _playableDisc.TotalSectors - _playableDisc.CurrentSector;
|
sectorsToRead = _opticalDisc.TotalSectors - _opticalDisc.CurrentSector;
|
||||||
zeroSectorsAmount = oldSectorsToRead - sectorsToRead;
|
|
||||||
|
int tempZeroSectorCount = (int)(oldSectorsToRead - sectorsToRead);
|
||||||
|
zeroSectorsAmount = (ulong)(tempZeroSectorCount < 0 ? 0 : tempZeroSectorCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Figure out when this value could be negative
|
// TODO: Figure out when this value could be negative
|
||||||
if(sectorsToRead <= 0)
|
if(sectorsToRead <= 0)
|
||||||
{
|
{
|
||||||
_playableDisc.LoadFirstTrack();
|
_opticalDisc.LoadFirstTrack();
|
||||||
_currentSectorReadPosition = 0;
|
_currentSectorReadPosition = 0;
|
||||||
}
|
}
|
||||||
} while(sectorsToRead <= 0);
|
} while(sectorsToRead <= 0);
|
||||||
|
|
||||||
// Create padding data for overreads
|
// Create padding data for overreads
|
||||||
byte[] zeroSectors = new byte[zeroSectorsAmount * 2352];
|
byte[] zeroSectors = new byte[(int)zeroSectorsAmount * _opticalDisc.BytesPerSector];
|
||||||
byte[] audioData;
|
byte[] audioData;
|
||||||
|
|
||||||
// Attempt to read the required number of sectors
|
// Attempt to read the required number of sectors
|
||||||
@@ -139,15 +154,19 @@ namespace RedBookPlayer
|
|||||||
{
|
{
|
||||||
lock(_readingImage)
|
lock(_readingImage)
|
||||||
{
|
{
|
||||||
try
|
for(int i = 0; i < 4; i++)
|
||||||
{
|
{
|
||||||
return _playableDisc.ReadSectors((uint)sectorsToRead).Concat(zeroSectors).ToArray();
|
try
|
||||||
}
|
{
|
||||||
catch(ArgumentOutOfRangeException)
|
return _opticalDisc.ReadSectors((uint)sectorsToRead).Concat(zeroSectors).ToArray();
|
||||||
{
|
}
|
||||||
_playableDisc.LoadFirstTrack();
|
catch(ArgumentOutOfRangeException)
|
||||||
return _playableDisc.ReadSectors((uint)sectorsToRead).Concat(zeroSectors).ToArray();
|
{
|
||||||
|
_opticalDisc.LoadFirstTrack();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return zeroSectors;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -164,10 +183,17 @@ namespace RedBookPlayer
|
|||||||
|
|
||||||
// Load only the requested audio segment
|
// Load only the requested audio segment
|
||||||
byte[] audioDataSegment = new byte[count];
|
byte[] audioDataSegment = new byte[count];
|
||||||
Array.Copy(audioData, _currentSectorReadPosition, audioDataSegment, 0, Math.Min(count, audioData.Length - _currentSectorReadPosition));
|
int copyAmount = Math.Min(count, audioData.Length - _currentSectorReadPosition);
|
||||||
|
if(Math.Max(0, copyAmount) == 0)
|
||||||
|
{
|
||||||
|
Array.Clear(buffer, offset, count);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
Array.Copy(audioData, _currentSectorReadPosition, audioDataSegment, 0, copyAmount);
|
||||||
|
|
||||||
// Apply de-emphasis filtering, only if enabled
|
// Apply de-emphasis filtering, only if enabled
|
||||||
if(_playableDisc.ApplyDeEmphasis)
|
if(ApplyDeEmphasis)
|
||||||
{
|
{
|
||||||
float[][] floatAudioData = new float[2][];
|
float[][] floatAudioData = new float[2][];
|
||||||
floatAudioData[0] = new float[audioDataSegment.Length / 4];
|
floatAudioData[0] = new float[audioDataSegment.Length / 4];
|
||||||
@@ -188,10 +214,10 @@ namespace RedBookPlayer
|
|||||||
|
|
||||||
// Set the read position in the sector for easier access
|
// Set the read position in the sector for easier access
|
||||||
_currentSectorReadPosition += count;
|
_currentSectorReadPosition += count;
|
||||||
if(_currentSectorReadPosition >= 2352)
|
if(_currentSectorReadPosition >= _opticalDisc.BytesPerSector)
|
||||||
{
|
{
|
||||||
_playableDisc.CurrentSector += (ulong)_currentSectorReadPosition / 2352;
|
_opticalDisc.CurrentSector += (ulong)(_currentSectorReadPosition / _opticalDisc.BytesPerSector);
|
||||||
_currentSectorReadPosition %= 2352;
|
_currentSectorReadPosition %= _opticalDisc.BytesPerSector;
|
||||||
}
|
}
|
||||||
|
|
||||||
return count;
|
return count;
|
||||||
@@ -202,42 +228,23 @@ namespace RedBookPlayer
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Start audio playback
|
/// Start audio playback
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Play()
|
public void Play() => _soundOut.Play();
|
||||||
{
|
|
||||||
if(!_playableDisc.Initialized)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_soundOut.Play();
|
|
||||||
_playableDisc.SetTotalIndexes();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Pause the current audio playback
|
/// Stop audio playback
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Pause()
|
public void Stop() => _soundOut.Stop();
|
||||||
{
|
|
||||||
if(!_playableDisc.Initialized)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_soundOut.Stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Stop the current audio playback
|
|
||||||
/// </summary>
|
|
||||||
public void Stop()
|
|
||||||
{
|
|
||||||
if(!_playableDisc.Initialized)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_soundOut.Stop();
|
|
||||||
_playableDisc.LoadFirstTrack();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Helpers
|
#region Helpers
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Toggle de-emphasis processing
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="enable">True to apply de-emphasis, false otherwise</param>
|
||||||
|
public void ToggleDeEmphasis(bool enable) => ApplyDeEmphasis = enable;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets or resets the de-emphasis filters
|
/// Sets or resets the de-emphasis filters
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace RedBookPlayer
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Recurring timer wrapper with a high degree of accuracy
|
|
||||||
/// </summary>
|
|
||||||
public class HiResTimer
|
|
||||||
{
|
|
||||||
static readonly float tickFrequency = 1000f / Stopwatch.Frequency;
|
|
||||||
|
|
||||||
volatile float _interval;
|
|
||||||
volatile bool _isRunning;
|
|
||||||
|
|
||||||
public HiResTimer() : this(1f) {}
|
|
||||||
|
|
||||||
public HiResTimer(float interval)
|
|
||||||
{
|
|
||||||
if(interval < 0f ||
|
|
||||||
float.IsNaN(interval))
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(interval));
|
|
||||||
|
|
||||||
_interval = interval;
|
|
||||||
}
|
|
||||||
|
|
||||||
public float Interval
|
|
||||||
{
|
|
||||||
get => _interval;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if(value < 0f ||
|
|
||||||
float.IsNaN(value))
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(value));
|
|
||||||
|
|
||||||
_interval = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Enabled
|
|
||||||
{
|
|
||||||
get => _isRunning;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if(value)
|
|
||||||
Start();
|
|
||||||
else
|
|
||||||
Stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public event EventHandler<HiResTimerElapsedEventArgs> Elapsed;
|
|
||||||
|
|
||||||
public void Start()
|
|
||||||
{
|
|
||||||
if(_isRunning)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_isRunning = true;
|
|
||||||
var thread = new Thread(ExecuteTimer);
|
|
||||||
thread.Priority = ThreadPriority.Highest;
|
|
||||||
thread.Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Stop() => _isRunning = false;
|
|
||||||
|
|
||||||
void ExecuteTimer()
|
|
||||||
{
|
|
||||||
float nextTrigger = 0f;
|
|
||||||
|
|
||||||
var stopwatch = new Stopwatch();
|
|
||||||
stopwatch.Start();
|
|
||||||
|
|
||||||
while(_isRunning)
|
|
||||||
{
|
|
||||||
nextTrigger += _interval;
|
|
||||||
float elapsed;
|
|
||||||
|
|
||||||
while(true)
|
|
||||||
{
|
|
||||||
elapsed = ElapsedHiRes(stopwatch);
|
|
||||||
float diff = nextTrigger - elapsed;
|
|
||||||
|
|
||||||
if(diff <= 0f)
|
|
||||||
break;
|
|
||||||
|
|
||||||
if(diff < 1f)
|
|
||||||
Thread.SpinWait(10);
|
|
||||||
else if(diff < 5f)
|
|
||||||
Thread.SpinWait(100);
|
|
||||||
else if(diff < 15f)
|
|
||||||
Thread.Sleep(1);
|
|
||||||
else
|
|
||||||
Thread.Sleep(10);
|
|
||||||
|
|
||||||
if(!_isRunning)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
float delay = elapsed - nextTrigger;
|
|
||||||
Elapsed?.Invoke(this, new HiResTimerElapsedEventArgs(delay));
|
|
||||||
|
|
||||||
if(!(stopwatch.Elapsed.TotalHours >= 1d))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
stopwatch.Restart();
|
|
||||||
nextTrigger = 0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
stopwatch.Stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
static float ElapsedHiRes(Stopwatch stopwatch) => stopwatch.ElapsedTicks * tickFrequency;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace RedBookPlayer
|
|
||||||
{
|
|
||||||
public class HiResTimerElapsedEventArgs : EventArgs
|
|
||||||
{
|
|
||||||
internal HiResTimerElapsedEventArgs(float delay) => Delay = delay;
|
|
||||||
|
|
||||||
public float Delay { get; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,699 +0,0 @@
|
|||||||
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 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;
|
|
||||||
|
|
||||||
// Cache the value and the current track number
|
|
||||||
int cachedValue = value;
|
|
||||||
int cachedTrackNumber = _currentTrackNumber;
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
|
|
||||||
_currentTrackNumber = cachedValue;
|
|
||||||
|
|
||||||
// Cache the current track for easy access
|
|
||||||
Track track = _image.Tracks[_currentTrackNumber];
|
|
||||||
|
|
||||||
// Set track flags from subchannel data, if possible
|
|
||||||
SetTrackFlags(track);
|
|
||||||
|
|
||||||
ApplyDeEmphasis = TrackHasEmphasis;
|
|
||||||
|
|
||||||
TotalIndexes = track.Indexes.Keys.Max();
|
|
||||||
CurrentTrackIndex = track.Indexes.Keys.Min();
|
|
||||||
|
|
||||||
// If the track is playable, just return
|
|
||||||
if(TrackType == TrackType.Audio || App.Settings.PlayDataTracks)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// If we're not playing the track, skip
|
|
||||||
if(increment)
|
|
||||||
cachedValue++;
|
|
||||||
else
|
|
||||||
cachedValue--;
|
|
||||||
}
|
|
||||||
while(cachedValue != cachedTrackNumber);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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 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 DCP flag
|
|
||||||
/// </summary>
|
|
||||||
public bool CopyAllowed { get; private set; } = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents the track type
|
|
||||||
/// </summary>
|
|
||||||
public TrackType TrackType { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents the 4CH flag
|
|
||||||
/// </summary>
|
|
||||||
public bool QuadChannel { get; private set; } = false;
|
|
||||||
|
|
||||||
/// <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 IOpticalMediaImage _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 void Init(IOpticalMediaImage 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(!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--;
|
|
||||||
}
|
|
||||||
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 bool LoadTOC()
|
|
||||||
{
|
|
||||||
if(_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 = _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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set default track flags for the current track
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="track">Track object to read from</param>
|
|
||||||
private void SetDefaultTrackFlags(Track track)
|
|
||||||
{
|
|
||||||
QuadChannel = false;
|
|
||||||
TrackType = track.TrackType;
|
|
||||||
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;
|
|
||||||
TrackType = (flags & (byte)TocControl.DataTrack) == (byte)TocControl.DataTrack ? TrackType.Data : TrackType.Audio;
|
|
||||||
QuadChannel = (flags & (byte)TocControl.FourChanNoPreEmph) == (byte)TocControl.FourChanNoPreEmph;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
catch(Exception)
|
|
||||||
{
|
|
||||||
SetDefaultTrackFlags(track);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#if Windows
|
using System;
|
||||||
|
#if WindowsDebug
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
#endif
|
#endif
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
@@ -8,19 +9,20 @@ namespace RedBookPlayer
|
|||||||
{
|
{
|
||||||
internal class Program
|
internal class Program
|
||||||
{
|
{
|
||||||
|
[STAThread]
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
#if Windows
|
#if WindowsDebug
|
||||||
AllocConsole();
|
AllocConsole();
|
||||||
#endif
|
#endif
|
||||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
#if Windows
|
#if WindowsDebug
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
static extern bool AllocConsole();
|
static extern bool AllocConsole();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure<App>().UsePlatformDetect().LogToDebug();
|
public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure<App>().UsePlatformDetect().LogToDebug();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
<RuntimeIdentifiers>win-x64;linux-x64</RuntimeIdentifiers>
|
<RuntimeIdentifiers>win-x64;linux-x64</RuntimeIdentifiers>
|
||||||
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
|
||||||
<DebugType>embedded</DebugType>
|
<DebugType>embedded</DebugType>
|
||||||
<PublishSingleFile>true</PublishSingleFile>
|
|
||||||
<SelfContained>true</SelfContained>
|
|
||||||
<PublishTrimmed>true</PublishTrimmed>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'win-x64'">
|
<PropertyGroup Condition="'$(RuntimeIdentifier)|$(Configuration)' == 'win-x64|Debug'">
|
||||||
<DefineConstants>Windows</DefineConstants>
|
<DefineConstants>WindowsDebug</DefineConstants>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Update="**\*.xaml.cs">
|
<Compile Update="**\*.xaml.cs">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using RedBookPlayer.GUI;
|
||||||
|
|
||||||
namespace RedBookPlayer
|
namespace RedBookPlayer
|
||||||
{
|
{
|
||||||
|
|||||||
2
build.bat
Normal file
2
build.bat
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
dotnet publish -f netcoreapp3.1 -r win-x64 -c Debug -p:PublishSingleFile=true -p:PublishTrimmed=true --self-contained true
|
||||||
|
dotnet publish -f netcoreapp3.1 -r win-x64 -c Release -p:PublishSingleFile=true -p:PublishTrimmed=true --self-contained true
|
||||||
3
build.sh
Normal file
3
build.sh
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
dotnet publish -f netcoreapp3.1 -r linux-x64 -c Debug -p:PublishSingleFile=true -p:PublishTrimmed=true --self-contained true
|
||||||
|
dotnet publish -f netcoreapp3.1 -r linux-x64 -c Release -p:PublishSingleFile=true -p:PublishTrimmed=true --self-contained true
|
||||||
Reference in New Issue
Block a user