refactor cue code to be less weirdly structured and partial-classy
This commit is contained in:
parent
667e4273f3
commit
b5d3ff4397
|
@ -71,9 +71,11 @@
|
|||
<Compile Include="DiscFormats\CUE\CueFileResolver.cs" />
|
||||
<Compile Include="DiscFormats\CUE\CUE_Compile.cs" />
|
||||
<Compile Include="DiscFormats\CUE\CUE_Context.cs" />
|
||||
<Compile Include="DiscFormats\CUE\CUE_File.cs" />
|
||||
<Compile Include="DiscFormats\CUE\CUE_Load.cs" />
|
||||
<Compile Include="DiscFormats\CUE\CUE_Parse.cs" />
|
||||
<Compile Include="DiscFormats\CUE\CUE_Synths.cs" />
|
||||
<Compile Include="DiscFormats\CUE\CUE_Types.cs" />
|
||||
<Compile Include="DiscFormats\M3U_file.cs" />
|
||||
<Compile Include="DiscFormats\SBI_format.cs" />
|
||||
<Compile Include="DiscFormats\TOC_format.cs" />
|
||||
|
|
|
@ -7,483 +7,479 @@ using System.Collections.Generic;
|
|||
//this would be a good place for structural validation
|
||||
//after this step, we won't want to have to do stuff like that (it will gunk up already sticky code)
|
||||
|
||||
namespace BizHawk.Emulation.DiscSystem
|
||||
namespace BizHawk.Emulation.DiscSystem.CUE
|
||||
{
|
||||
partial class CUE_Context
|
||||
internal class CompiledCDText
|
||||
{
|
||||
internal class CompiledCDText
|
||||
{
|
||||
public string Songwriter;
|
||||
public string Performer;
|
||||
public string Title;
|
||||
public string ISRC;
|
||||
}
|
||||
public string Songwriter;
|
||||
public string Performer;
|
||||
public string Title;
|
||||
public string ISRC;
|
||||
}
|
||||
|
||||
internal class CompiledCueIndex
|
||||
{
|
||||
public int Number;
|
||||
|
||||
/// <summary>
|
||||
/// this is annoying, it should just be an integer
|
||||
/// </summary>
|
||||
public Timestamp FileMSF;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("I#{0:D2} {1}", Number, FileMSF);
|
||||
}
|
||||
}
|
||||
internal class CompiledCueIndex
|
||||
{
|
||||
public int Number;
|
||||
|
||||
/// <summary>
|
||||
/// What type of file we're looking at.. each one would require a different ingestion handler
|
||||
/// this is annoying, it should just be an integer
|
||||
/// </summary>
|
||||
public enum CompiledCueFileType
|
||||
public Timestamp FileMSF;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
Unknown,
|
||||
|
||||
/// <summary>
|
||||
/// a raw BIN that can be mounted directly
|
||||
/// </summary>
|
||||
BIN,
|
||||
|
||||
/// <summary>
|
||||
/// a raw WAV that can be mounted directly
|
||||
/// </summary>
|
||||
WAVE,
|
||||
|
||||
/// <summary>
|
||||
/// an ECM file that can be mounted directly (once the index is generated)
|
||||
/// </summary>
|
||||
ECM,
|
||||
|
||||
/// <summary>
|
||||
/// An encoded audio file which can be seeked on the fly, therefore roughly mounted on the fly
|
||||
/// THIS ISN'T SUPPORTED YET
|
||||
/// </summary>
|
||||
SeekAudio,
|
||||
|
||||
/// <summary>
|
||||
/// An encoded audio file which can't be seeked on the fly. It must be decoded to a temp buffer, or pre-discohawked
|
||||
/// </summary>
|
||||
DecodeAudio,
|
||||
return string.Format("I#{0:D2} {1}", Number, FileMSF);
|
||||
}
|
||||
}
|
||||
|
||||
internal class CompiledCueFile
|
||||
/// <summary>
|
||||
/// What type of file we're looking at.. each one would require a different ingestion handler
|
||||
/// </summary>
|
||||
public enum CompiledCueFileType
|
||||
{
|
||||
Unknown,
|
||||
|
||||
/// <summary>
|
||||
/// a raw BIN that can be mounted directly
|
||||
/// </summary>
|
||||
BIN,
|
||||
|
||||
/// <summary>
|
||||
/// a raw WAV that can be mounted directly
|
||||
/// </summary>
|
||||
WAVE,
|
||||
|
||||
/// <summary>
|
||||
/// an ECM file that can be mounted directly (once the index is generated)
|
||||
/// </summary>
|
||||
ECM,
|
||||
|
||||
/// <summary>
|
||||
/// An encoded audio file which can be seeked on the fly, therefore roughly mounted on the fly
|
||||
/// THIS ISN'T SUPPORTED YET
|
||||
/// </summary>
|
||||
SeekAudio,
|
||||
|
||||
/// <summary>
|
||||
/// An encoded audio file which can't be seeked on the fly. It must be decoded to a temp buffer, or pre-discohawked
|
||||
/// </summary>
|
||||
DecodeAudio,
|
||||
}
|
||||
|
||||
internal class CompiledCueFile
|
||||
{
|
||||
public string FullPath;
|
||||
public CompiledCueFileType Type;
|
||||
public override string ToString()
|
||||
{
|
||||
public string FullPath;
|
||||
public CompiledCueFileType Type;
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("{0}: {1}", Type, Path.GetFileName(FullPath));
|
||||
}
|
||||
return string.Format("{0}: {1}", Type, Path.GetFileName(FullPath));
|
||||
}
|
||||
}
|
||||
|
||||
internal class CompiledDiscInfo
|
||||
{
|
||||
public int FirstRecordedTrackNumber, LastRecordedTrackNumber;
|
||||
public SessionFormat SessionFormat;
|
||||
}
|
||||
internal class CompiledDiscInfo
|
||||
{
|
||||
public int FirstRecordedTrackNumber, LastRecordedTrackNumber;
|
||||
public SessionFormat SessionFormat;
|
||||
}
|
||||
|
||||
internal class CompiledCueTrack
|
||||
{
|
||||
public int BlobIndex;
|
||||
public int Number;
|
||||
internal class CompiledCueTrack
|
||||
{
|
||||
public int BlobIndex;
|
||||
public int Number;
|
||||
|
||||
/// <summary>
|
||||
/// A track that's final in a file gets its length from the length of the file; other tracks lengths are determined from the succeeding track
|
||||
/// </summary>
|
||||
public bool IsFinalInFile;
|
||||
/// <summary>
|
||||
/// A track that's final in a file gets its length from the length of the file; other tracks lengths are determined from the succeeding track
|
||||
/// </summary>
|
||||
public bool IsFinalInFile;
|
||||
|
||||
/// <summary>
|
||||
/// A track that's first in a file has an implicit index 0 at 00:00:00
|
||||
/// Otherwise it has an implicit index 0 at the placement of the index 1
|
||||
/// </summary>
|
||||
public bool IsFirstInFile;
|
||||
/// <summary>
|
||||
/// A track that's first in a file has an implicit index 0 at 00:00:00
|
||||
/// Otherwise it has an implicit index 0 at the placement of the index 1
|
||||
/// </summary>
|
||||
public bool IsFirstInFile;
|
||||
|
||||
public CompiledCDText CDTextData = new CompiledCDText();
|
||||
public Timestamp PregapLength, PostgapLength;
|
||||
public CueFile.TrackFlags Flags = CueFile.TrackFlags.None;
|
||||
public CueFile.TrackType TrackType = CueFile.TrackType.Unknown;
|
||||
public CompiledCDText CDTextData = new CompiledCDText();
|
||||
public Timestamp PregapLength, PostgapLength;
|
||||
public CueTrackFlags Flags = CueTrackFlags.None;
|
||||
public CueTrackType TrackType = CueTrackType.Unknown;
|
||||
|
||||
public List<CompiledCueIndex> Indexes = new List<CompiledCueIndex>();
|
||||
public List<CompiledCueIndex> Indexes = new List<CompiledCueIndex>();
|
||||
|
||||
public override string ToString()
|
||||
public override string ToString()
|
||||
{
|
||||
var idx = Indexes.Find((i) => i.Number == 1);
|
||||
if (idx == null)
|
||||
return string.Format("T#{0:D2} NO INDEX 1", Number);
|
||||
else
|
||||
{
|
||||
var idx = Indexes.Find((i) => i.Number == 1);
|
||||
if (idx == null)
|
||||
return string.Format("T#{0:D2} NO INDEX 1", Number);
|
||||
else
|
||||
var indexlist = string.Join("|", Indexes);
|
||||
return string.Format("T#{0:D2} {1}:{2} ({3})", Number, BlobIndex, idx.FileMSF, indexlist);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class CompileCueJob : DiscJob
|
||||
{
|
||||
/// <summary>
|
||||
/// input: the CueFile to analyze
|
||||
/// </summary>
|
||||
public CUE_File IN_CueFile;
|
||||
|
||||
/// <summary>
|
||||
/// The context used for this compiling job
|
||||
/// TODO - rename something like context
|
||||
/// </summary>
|
||||
public CUE_Context IN_CueFormat;
|
||||
|
||||
/// <summary>
|
||||
/// output: high level disc info
|
||||
/// </summary>
|
||||
public CompiledDiscInfo OUT_CompiledDiscInfo;
|
||||
|
||||
/// <summary>
|
||||
/// output: CD-Text set at the global level (before any track commands)
|
||||
/// </summary>
|
||||
public CompiledCDText OUT_GlobalCDText;
|
||||
|
||||
/// <summary>
|
||||
/// output: The compiled file info
|
||||
/// </summary>
|
||||
public List<CompiledCueFile> OUT_CompiledCueFiles;
|
||||
|
||||
/// <summary>
|
||||
/// output: The compiled track info
|
||||
/// </summary>
|
||||
public List<CompiledCueTrack> OUT_CompiledCueTracks;
|
||||
|
||||
/// <summary>
|
||||
/// output: An integer between 0 and 10 indicating how costly it will be to load this disc completely.
|
||||
/// Activites like decoding non-seekable media will increase the load time.
|
||||
/// 0 - Requires no noticeable time
|
||||
/// 1 - Requires minimal processing (indexing ECM)
|
||||
/// 10 - Requires ages, decoding audio data, etc.
|
||||
/// </summary>
|
||||
public int OUT_LoadTime;
|
||||
|
||||
//-----------------------------------------------------------------
|
||||
|
||||
CompiledCDText curr_cdtext;
|
||||
int curr_blobIndex = -1;
|
||||
CompiledCueTrack curr_track = null;
|
||||
CompiledCueFile curr_file = null;
|
||||
bool discinfo_session1Format_determined = false;
|
||||
bool curr_fileHasTrack = false;
|
||||
|
||||
void UpdateDiscInfo(CUE_File.Command.TRACK trackCommand)
|
||||
{
|
||||
if (OUT_CompiledDiscInfo.FirstRecordedTrackNumber == 0)
|
||||
OUT_CompiledDiscInfo.FirstRecordedTrackNumber = trackCommand.Number;
|
||||
OUT_CompiledDiscInfo.LastRecordedTrackNumber = trackCommand.Number;
|
||||
if (!discinfo_session1Format_determined)
|
||||
{
|
||||
switch (trackCommand.Type)
|
||||
{
|
||||
var indexlist = string.Join("|", Indexes);
|
||||
return string.Format("T#{0:D2} {1}:{2} ({3})", Number, BlobIndex, idx.FileMSF, indexlist);
|
||||
case CueTrackType.Mode2_2336:
|
||||
case CueTrackType.Mode2_2352:
|
||||
OUT_CompiledDiscInfo.SessionFormat = SessionFormat.Type20_CDXA;
|
||||
discinfo_session1Format_determined = true;
|
||||
break;
|
||||
|
||||
case CueTrackType.CDI_2336:
|
||||
case CueTrackType.CDI_2352:
|
||||
OUT_CompiledDiscInfo.SessionFormat = SessionFormat.Type10_CDI;
|
||||
discinfo_session1Format_determined = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class CompileCueJob : DiscJob
|
||||
void CloseFile()
|
||||
{
|
||||
/// <summary>
|
||||
/// input: the CueFile to analyze
|
||||
/// </summary>
|
||||
public CueFile IN_CueFile;
|
||||
|
||||
/// <summary>
|
||||
/// The context used for this compiling job
|
||||
/// TODO - rename something like context
|
||||
/// </summary>
|
||||
public CUE_Context IN_CueFormat;
|
||||
|
||||
/// <summary>
|
||||
/// output: high level disc info
|
||||
/// </summary>
|
||||
public CompiledDiscInfo OUT_CompiledDiscInfo;
|
||||
|
||||
/// <summary>
|
||||
/// output: CD-Text set at the global level (before any track commands)
|
||||
/// </summary>
|
||||
public CompiledCDText OUT_GlobalCDText;
|
||||
|
||||
/// <summary>
|
||||
/// output: The compiled file info
|
||||
/// </summary>
|
||||
public List<CompiledCueFile> OUT_CompiledCueFiles;
|
||||
|
||||
/// <summary>
|
||||
/// output: The compiled track info
|
||||
/// </summary>
|
||||
public List<CompiledCueTrack> OUT_CompiledCueTracks;
|
||||
|
||||
/// <summary>
|
||||
/// output: An integer between 0 and 10 indicating how costly it will be to load this disc completely.
|
||||
/// Activites like decoding non-seekable media will increase the load time.
|
||||
/// 0 - Requires no noticeable time
|
||||
/// 1 - Requires minimal processing (indexing ECM)
|
||||
/// 10 - Requires ages, decoding audio data, etc.
|
||||
/// </summary>
|
||||
public int OUT_LoadTime;
|
||||
|
||||
//-----------------------------------------------------------------
|
||||
|
||||
CompiledCDText curr_cdtext;
|
||||
int curr_blobIndex = -1;
|
||||
CompiledCueTrack curr_track = null;
|
||||
CompiledCueFile curr_file = null;
|
||||
bool discinfo_session1Format_determined = false;
|
||||
bool curr_fileHasTrack = false;
|
||||
|
||||
void UpdateDiscInfo(CueFile.Command.TRACK trackCommand)
|
||||
if (curr_track != null)
|
||||
{
|
||||
if (OUT_CompiledDiscInfo.FirstRecordedTrackNumber == 0)
|
||||
OUT_CompiledDiscInfo.FirstRecordedTrackNumber = trackCommand.Number;
|
||||
OUT_CompiledDiscInfo.LastRecordedTrackNumber = trackCommand.Number;
|
||||
if (!discinfo_session1Format_determined)
|
||||
//flag this track as the final one in the file
|
||||
curr_track.IsFinalInFile = true;
|
||||
}
|
||||
|
||||
curr_file = null;
|
||||
}
|
||||
|
||||
void OpenFile(CUE_File.Command.FILE f)
|
||||
{
|
||||
if (curr_file != null)
|
||||
CloseFile();
|
||||
|
||||
curr_blobIndex++;
|
||||
curr_fileHasTrack = false;
|
||||
|
||||
var Resolver = IN_CueFormat.Resolver;
|
||||
|
||||
//TODO - smart audio file resolving only for AUDIO types. not BINARY or MOTOROLA or AIFF or ECM or what have you
|
||||
|
||||
var options = Resolver.Resolve(f.Path);
|
||||
string choice = null;
|
||||
if (options.Count == 0)
|
||||
{
|
||||
Error("Couldn't resolve referenced cue file: " + f.Path);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
choice = options[0];
|
||||
if (options.Count > 1)
|
||||
Warn("Multiple options resolving referenced cue file; choosing: " + Path.GetFileName(choice));
|
||||
}
|
||||
|
||||
var cfi = new CompiledCueFile();
|
||||
OUT_CompiledCueFiles.Add(cfi);
|
||||
|
||||
cfi.FullPath = choice;
|
||||
|
||||
//determine the CueFileInfo's type, based on extension and extra checking
|
||||
//TODO - once we reorganize the file ID stuff, do legit checks here (this is completely redundant with the fileID system
|
||||
//TODO - decode vs stream vs unpossible policies in input policies object (including ffmpeg availability-checking callback (results can be cached))
|
||||
string blobPathExt = Path.GetExtension(choice).ToUpperInvariant();
|
||||
if (blobPathExt == ".BIN" || blobPathExt == ".IMG") cfi.Type = CompiledCueFileType.BIN;
|
||||
else if (blobPathExt == ".ISO") cfi.Type = CompiledCueFileType.BIN;
|
||||
else if (blobPathExt == ".WAV")
|
||||
{
|
||||
//quickly, check the format. turn it to DecodeAudio if it can't be supported
|
||||
//TODO - fix exception-throwing inside
|
||||
//TODO - verify stream-disposing semantics
|
||||
var fs = File.OpenRead(choice);
|
||||
using (var blob = new Disc.Blob_WaveFile())
|
||||
{
|
||||
switch (trackCommand.Type)
|
||||
try
|
||||
{
|
||||
case CueFile.TrackType.Mode2_2336:
|
||||
case CueFile.TrackType.Mode2_2352:
|
||||
OUT_CompiledDiscInfo.SessionFormat = SessionFormat.Type20_CDXA;
|
||||
discinfo_session1Format_determined = true;
|
||||
break;
|
||||
|
||||
case CueFile.TrackType.CDI_2336:
|
||||
case CueFile.TrackType.CDI_2352:
|
||||
OUT_CompiledDiscInfo.SessionFormat = SessionFormat.Type10_CDI;
|
||||
discinfo_session1Format_determined = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
blob.Load(fs);
|
||||
cfi.Type = CompiledCueFileType.WAVE;
|
||||
}
|
||||
catch
|
||||
{
|
||||
cfi.Type = CompiledCueFileType.DecodeAudio;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CloseFile()
|
||||
else if (blobPathExt == ".APE") cfi.Type = CompiledCueFileType.DecodeAudio;
|
||||
else if (blobPathExt == ".MP3") cfi.Type = CompiledCueFileType.DecodeAudio;
|
||||
else if (blobPathExt == ".MPC") cfi.Type = CompiledCueFileType.DecodeAudio;
|
||||
else if (blobPathExt == ".FLAC") cfi.Type = CompiledCueFileType.DecodeAudio;
|
||||
else if (blobPathExt == ".ECM")
|
||||
{
|
||||
if (curr_track != null)
|
||||
cfi.Type = CompiledCueFileType.ECM;
|
||||
if (!Disc.Blob_ECM.IsECM(choice))
|
||||
{
|
||||
//flag this track as the final one in the file
|
||||
curr_track.IsFinalInFile = true;
|
||||
}
|
||||
|
||||
curr_file = null;
|
||||
}
|
||||
|
||||
void OpenFile(CueFile.Command.FILE f)
|
||||
{
|
||||
if (curr_file != null)
|
||||
CloseFile();
|
||||
|
||||
curr_blobIndex++;
|
||||
curr_fileHasTrack = false;
|
||||
|
||||
var Resolver = IN_CueFormat.Resolver;
|
||||
|
||||
//TODO - smart audio file resolving only for AUDIO types. not BINARY or MOTOROLA or AIFF or ECM or what have you
|
||||
|
||||
var options = Resolver.Resolve(f.Path);
|
||||
string choice = null;
|
||||
if (options.Count == 0)
|
||||
{
|
||||
Error("Couldn't resolve referenced cue file: " + f.Path);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
choice = options[0];
|
||||
if (options.Count > 1)
|
||||
Warn("Multiple options resolving referenced cue file; choosing: " + Path.GetFileName(choice));
|
||||
}
|
||||
|
||||
var cfi = new CompiledCueFile();
|
||||
OUT_CompiledCueFiles.Add(cfi);
|
||||
|
||||
cfi.FullPath = choice;
|
||||
|
||||
//determine the CueFileInfo's type, based on extension and extra checking
|
||||
//TODO - once we reorganize the file ID stuff, do legit checks here (this is completely redundant with the fileID system
|
||||
//TODO - decode vs stream vs unpossible policies in input policies object (including ffmpeg availability-checking callback (results can be cached))
|
||||
string blobPathExt = Path.GetExtension(choice).ToUpperInvariant();
|
||||
if (blobPathExt == ".BIN" || blobPathExt == ".IMG") cfi.Type = CompiledCueFileType.BIN;
|
||||
else if (blobPathExt == ".ISO") cfi.Type = CompiledCueFileType.BIN;
|
||||
else if (blobPathExt == ".WAV")
|
||||
{
|
||||
//quickly, check the format. turn it to DecodeAudio if it can't be supported
|
||||
//TODO - fix exception-throwing inside
|
||||
//TODO - verify stream-disposing semantics
|
||||
var fs = File.OpenRead(choice);
|
||||
using (var blob = new Disc.Blob_WaveFile())
|
||||
{
|
||||
try
|
||||
{
|
||||
blob.Load(fs);
|
||||
cfi.Type = CompiledCueFileType.WAVE;
|
||||
}
|
||||
catch
|
||||
{
|
||||
cfi.Type = CompiledCueFileType.DecodeAudio;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (blobPathExt == ".APE") cfi.Type = CompiledCueFileType.DecodeAudio;
|
||||
else if (blobPathExt == ".MP3") cfi.Type = CompiledCueFileType.DecodeAudio;
|
||||
else if (blobPathExt == ".MPC") cfi.Type = CompiledCueFileType.DecodeAudio;
|
||||
else if (blobPathExt == ".FLAC") cfi.Type = CompiledCueFileType.DecodeAudio;
|
||||
else if (blobPathExt == ".ECM")
|
||||
{
|
||||
cfi.Type = CompiledCueFileType.ECM;
|
||||
if (!Disc.Blob_ECM.IsECM(choice))
|
||||
{
|
||||
Error("an ECM file was specified or detected, but it isn't a valid ECM file: " + Path.GetFileName(choice));
|
||||
cfi.Type = CompiledCueFileType.Unknown;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Error("Unknown cue file type. Since it's likely an unsupported compression, this is an error: ", Path.GetFileName(choice));
|
||||
Error("an ECM file was specified or detected, but it isn't a valid ECM file: " + Path.GetFileName(choice));
|
||||
cfi.Type = CompiledCueFileType.Unknown;
|
||||
}
|
||||
|
||||
//TODO - check for mismatches between track types and file types, or is that best done when interpreting the commands?
|
||||
}
|
||||
else
|
||||
{
|
||||
Error("Unknown cue file type. Since it's likely an unsupported compression, this is an error: ", Path.GetFileName(choice));
|
||||
cfi.Type = CompiledCueFileType.Unknown;
|
||||
}
|
||||
|
||||
void CreateTrack1Pregap()
|
||||
//TODO - check for mismatches between track types and file types, or is that best done when interpreting the commands?
|
||||
}
|
||||
|
||||
void CreateTrack1Pregap()
|
||||
{
|
||||
if (OUT_CompiledCueTracks[1].PregapLength.Sector == 0) { }
|
||||
else if (OUT_CompiledCueTracks[1].PregapLength.Sector == 150) { }
|
||||
else
|
||||
{
|
||||
if (OUT_CompiledCueTracks[1].PregapLength.Sector == 0) { }
|
||||
else if (OUT_CompiledCueTracks[1].PregapLength.Sector == 150) { }
|
||||
else
|
||||
Error("Track 1 specified an illegal pregap. It's being ignored and replaced with a 00:02:00 pregap");
|
||||
}
|
||||
OUT_CompiledCueTracks[1].PregapLength = new Timestamp(150);
|
||||
}
|
||||
|
||||
void FinalAnalysis()
|
||||
{
|
||||
//some quick checks:
|
||||
if (OUT_CompiledCueFiles.Count == 0)
|
||||
Error("Cue file doesn't specify any input files!");
|
||||
|
||||
//we can't reliably analyze the length of files here, because we might have to be decoding to get lengths (VBR mp3s)
|
||||
//So, it's not really worth the trouble. We'll cope with lengths later
|
||||
//we could check the format of the wav file here, though
|
||||
|
||||
//score the cost of loading the file
|
||||
bool needsCodec = false;
|
||||
OUT_LoadTime = 0;
|
||||
foreach (var cfi in OUT_CompiledCueFiles)
|
||||
{
|
||||
if (cfi.Type == CompiledCueFileType.DecodeAudio)
|
||||
{
|
||||
Error("Track 1 specified an illegal pregap. It's being ignored and replaced with a 00:02:00 pregap");
|
||||
needsCodec = true;
|
||||
OUT_LoadTime = Math.Max(OUT_LoadTime, 10);
|
||||
}
|
||||
OUT_CompiledCueTracks[1].PregapLength = new Timestamp(150);
|
||||
if (cfi.Type == CompiledCueFileType.SeekAudio)
|
||||
needsCodec = true;
|
||||
if (cfi.Type == CompiledCueFileType.ECM)
|
||||
OUT_LoadTime = Math.Max(OUT_LoadTime, 1);
|
||||
}
|
||||
|
||||
void FinalAnalysis()
|
||||
//check whether processing was available
|
||||
if (needsCodec)
|
||||
{
|
||||
//some quick checks:
|
||||
if (OUT_CompiledCueFiles.Count == 0)
|
||||
Error("Cue file doesn't specify any input files!");
|
||||
|
||||
//we can't reliably analyze the length of files here, because we might have to be decoding to get lengths (VBR mp3s)
|
||||
//So, it's not really worth the trouble. We'll cope with lengths later
|
||||
//we could check the format of the wav file here, though
|
||||
|
||||
//score the cost of loading the file
|
||||
bool needsCodec = false;
|
||||
OUT_LoadTime = 0;
|
||||
foreach (var cfi in OUT_CompiledCueFiles)
|
||||
{
|
||||
if (cfi.Type == CompiledCueFileType.DecodeAudio)
|
||||
{
|
||||
needsCodec = true;
|
||||
OUT_LoadTime = Math.Max(OUT_LoadTime, 10);
|
||||
}
|
||||
if (cfi.Type == CompiledCueFileType.SeekAudio)
|
||||
needsCodec = true;
|
||||
if (cfi.Type == CompiledCueFileType.ECM)
|
||||
OUT_LoadTime = Math.Max(OUT_LoadTime, 1);
|
||||
}
|
||||
|
||||
//check whether processing was available
|
||||
if (needsCodec)
|
||||
{
|
||||
FFMpeg ffmpeg = new FFMpeg();
|
||||
if (!ffmpeg.QueryServiceAvailable())
|
||||
Warn("Decoding service will be required for further processing, but is not available");
|
||||
}
|
||||
FFMpeg ffmpeg = new FFMpeg();
|
||||
if (!ffmpeg.QueryServiceAvailable())
|
||||
Warn("Decoding service will be required for further processing, but is not available");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void CloseTrack()
|
||||
{
|
||||
if (curr_track == null)
|
||||
return;
|
||||
void CloseTrack()
|
||||
{
|
||||
if (curr_track == null)
|
||||
return;
|
||||
|
||||
//normalize: if an index 0 is missing, add it here
|
||||
if (curr_track.Indexes[0].Number != 0)
|
||||
{
|
||||
var index0 = new CompiledCueIndex();
|
||||
var index1 = curr_track.Indexes[0];
|
||||
index0.Number = 0;
|
||||
index0.FileMSF = index1.FileMSF; //same MSF as index 1 will make it effectively nonexistent
|
||||
//normalize: if an index 0 is missing, add it here
|
||||
if (curr_track.Indexes[0].Number != 0)
|
||||
{
|
||||
var index0 = new CompiledCueIndex();
|
||||
var index1 = curr_track.Indexes[0];
|
||||
index0.Number = 0;
|
||||
index0.FileMSF = index1.FileMSF; //same MSF as index 1 will make it effectively nonexistent
|
||||
|
||||
//well now, if it's the first in the file, an implicit index will take its value from 00:00:00 in the file
|
||||
//this is the kind of thing I sought to solve originally by 'interpreting' the file, but it seems easy enough to handle this way
|
||||
//my carlin.cue tests this but test cases shouldnt be hard to find
|
||||
if (curr_track.IsFirstInFile)
|
||||
index0.FileMSF = new Timestamp(0);
|
||||
//well now, if it's the first in the file, an implicit index will take its value from 00:00:00 in the file
|
||||
//this is the kind of thing I sought to solve originally by 'interpreting' the file, but it seems easy enough to handle this way
|
||||
//my carlin.cue tests this but test cases shouldnt be hard to find
|
||||
if (curr_track.IsFirstInFile)
|
||||
index0.FileMSF = new Timestamp(0);
|
||||
|
||||
curr_track.Indexes.Insert(0, index0);
|
||||
}
|
||||
|
||||
OUT_CompiledCueTracks.Add(curr_track);
|
||||
curr_track = null;
|
||||
curr_track.Indexes.Insert(0, index0);
|
||||
}
|
||||
|
||||
void OpenTrack(CueFile.Command.TRACK trackCommand)
|
||||
{
|
||||
curr_track = new CompiledCueTrack();
|
||||
OUT_CompiledCueTracks.Add(curr_track);
|
||||
curr_track = null;
|
||||
}
|
||||
|
||||
//spill cdtext data into this track
|
||||
curr_cdtext = curr_track.CDTextData;
|
||||
void OpenTrack(CUE_File.Command.TRACK trackCommand)
|
||||
{
|
||||
curr_track = new CompiledCueTrack();
|
||||
|
||||
//spill cdtext data into this track
|
||||
curr_cdtext = curr_track.CDTextData;
|
||||
|
||||
curr_track.BlobIndex = curr_blobIndex;
|
||||
curr_track.Number = trackCommand.Number;
|
||||
curr_track.TrackType = trackCommand.Type;
|
||||
curr_track.BlobIndex = curr_blobIndex;
|
||||
curr_track.Number = trackCommand.Number;
|
||||
curr_track.TrackType = trackCommand.Type;
|
||||
|
||||
//default flags
|
||||
if (curr_track.TrackType != CueFile.TrackType.Audio)
|
||||
curr_track.Flags = CueFile.TrackFlags.DATA;
|
||||
//default flags
|
||||
if (curr_track.TrackType != CueTrackType.Audio)
|
||||
curr_track.Flags = CueTrackFlags.DATA;
|
||||
|
||||
if (!curr_fileHasTrack)
|
||||
{
|
||||
curr_fileHasTrack = curr_track.IsFirstInFile = true;
|
||||
}
|
||||
|
||||
UpdateDiscInfo(trackCommand);
|
||||
if (!curr_fileHasTrack)
|
||||
{
|
||||
curr_fileHasTrack = curr_track.IsFirstInFile = true;
|
||||
}
|
||||
|
||||
void AddIndex(CueFile.Command.INDEX indexCommand)
|
||||
UpdateDiscInfo(trackCommand);
|
||||
}
|
||||
|
||||
void AddIndex(CUE_File.Command.INDEX indexCommand)
|
||||
{
|
||||
var newindex = new CompiledCueIndex();
|
||||
newindex.FileMSF = indexCommand.Timestamp;
|
||||
newindex.Number = indexCommand.Number;
|
||||
curr_track.Indexes.Add(newindex);
|
||||
}
|
||||
|
||||
public void Run()
|
||||
{
|
||||
//in params
|
||||
var cue = IN_CueFile;
|
||||
|
||||
//output state
|
||||
OUT_GlobalCDText = new CompiledCDText();
|
||||
OUT_CompiledDiscInfo = new CompiledDiscInfo();
|
||||
OUT_CompiledCueFiles = new List<CompiledCueFile>();
|
||||
OUT_CompiledCueTracks = new List<CompiledCueTrack>();
|
||||
|
||||
//add a track 0, for addressing convenience.
|
||||
//note: for future work, track 0 may need emulation (accessible by very negative LBA--the TOC is stored there)
|
||||
var track0 = new CompiledCueTrack() {
|
||||
Number = 0,
|
||||
};
|
||||
OUT_CompiledCueTracks.Add(track0);
|
||||
|
||||
//global cd text will acquire the cdtext commands set before track commands
|
||||
curr_cdtext = OUT_GlobalCDText;
|
||||
|
||||
for (int i = 0; i < cue.Commands.Count; i++)
|
||||
{
|
||||
var newindex = new CompiledCueIndex();
|
||||
newindex.FileMSF = indexCommand.Timestamp;
|
||||
newindex.Number = indexCommand.Number;
|
||||
curr_track.Indexes.Add(newindex);
|
||||
}
|
||||
var cmd = cue.Commands[i];
|
||||
|
||||
public void Run()
|
||||
{
|
||||
//in params
|
||||
var cue = IN_CueFile;
|
||||
//these commands get dealt with globally. nothing to be done here
|
||||
//(but in the future we need to accumulate them into the compile pass output)
|
||||
if (cmd is CUE_File.Command.CATALOG || cmd is CUE_File.Command.CDTEXTFILE) continue;
|
||||
|
||||
//output state
|
||||
OUT_GlobalCDText = new CompiledCDText();
|
||||
OUT_CompiledDiscInfo = new CompiledDiscInfo();
|
||||
OUT_CompiledCueFiles = new List<CompiledCueFile>();
|
||||
OUT_CompiledCueTracks = new List<CompiledCueTrack>();
|
||||
//nothing to be done for comments
|
||||
if (cmd is CUE_File.Command.REM) continue;
|
||||
if (cmd is CUE_File.Command.COMMENT) continue;
|
||||
|
||||
//add a track 0, for addressing convenience.
|
||||
//note: for future work, track 0 may need emulation (accessible by very negative LBA--the TOC is stored there)
|
||||
var track0 = new CompiledCueTrack() {
|
||||
Number = 0,
|
||||
};
|
||||
OUT_CompiledCueTracks.Add(track0);
|
||||
//CD-text and related
|
||||
if (cmd is CUE_File.Command.PERFORMER) curr_cdtext.Performer = (cmd as CUE_File.Command.PERFORMER).Value;
|
||||
if (cmd is CUE_File.Command.SONGWRITER) curr_cdtext.Songwriter = (cmd as CUE_File.Command.SONGWRITER).Value;
|
||||
if (cmd is CUE_File.Command.TITLE) curr_cdtext.Title = (cmd as CUE_File.Command.TITLE).Value;
|
||||
if (cmd is CUE_File.Command.ISRC) curr_cdtext.ISRC = (cmd as CUE_File.Command.ISRC).Value;
|
||||
|
||||
//global cd text will acquire the cdtext commands set before track commands
|
||||
curr_cdtext = OUT_GlobalCDText;
|
||||
|
||||
for (int i = 0; i < cue.Commands.Count; i++)
|
||||
//flags can only be set when a track command is running
|
||||
if (cmd is CUE_File.Command.FLAGS)
|
||||
{
|
||||
var cmd = cue.Commands[i];
|
||||
|
||||
//these commands get dealt with globally. nothing to be done here
|
||||
//(but in the future we need to accumulate them into the compile pass output)
|
||||
if (cmd is CueFile.Command.CATALOG || cmd is CueFile.Command.CDTEXTFILE) continue;
|
||||
|
||||
//nothing to be done for comments
|
||||
if (cmd is CueFile.Command.REM) continue;
|
||||
if (cmd is CueFile.Command.COMMENT) continue;
|
||||
|
||||
//CD-text and related
|
||||
if (cmd is CueFile.Command.PERFORMER) curr_cdtext.Performer = (cmd as CueFile.Command.PERFORMER).Value;
|
||||
if (cmd is CueFile.Command.SONGWRITER) curr_cdtext.Songwriter = (cmd as CueFile.Command.SONGWRITER).Value;
|
||||
if (cmd is CueFile.Command.TITLE) curr_cdtext.Title = (cmd as CueFile.Command.TITLE).Value;
|
||||
if (cmd is CueFile.Command.ISRC) curr_cdtext.ISRC = (cmd as CueFile.Command.ISRC).Value;
|
||||
|
||||
//flags can only be set when a track command is running
|
||||
if (cmd is CueFile.Command.FLAGS)
|
||||
{
|
||||
if (curr_track == null)
|
||||
Warn("Ignoring invalid flag commands outside of a track command");
|
||||
else
|
||||
//take care to |= it here, so the data flag doesn't get cleared
|
||||
curr_track.Flags |= (cmd as CueFile.Command.FLAGS).Flags;
|
||||
}
|
||||
|
||||
if (cmd is CueFile.Command.TRACK)
|
||||
{
|
||||
CloseTrack();
|
||||
OpenTrack(cmd as CueFile.Command.TRACK);
|
||||
}
|
||||
|
||||
if (cmd is CueFile.Command.FILE)
|
||||
{
|
||||
CloseFile();
|
||||
OpenFile(cmd as CueFile.Command.FILE);
|
||||
}
|
||||
|
||||
if (cmd is CueFile.Command.INDEX)
|
||||
{
|
||||
//todo - validate no postgap specified
|
||||
AddIndex(cmd as CueFile.Command.INDEX);
|
||||
}
|
||||
|
||||
if (cmd is CueFile.Command.PREGAP)
|
||||
{
|
||||
//validate track open
|
||||
//validate no indexes
|
||||
curr_track.PregapLength = (cmd as CueFile.Command.PREGAP).Length;
|
||||
}
|
||||
|
||||
if (cmd is CueFile.Command.POSTGAP)
|
||||
{
|
||||
curr_track.PostgapLength = (cmd as CueFile.Command.POSTGAP).Length;
|
||||
}
|
||||
|
||||
if (curr_track == null)
|
||||
Warn("Ignoring invalid flag commands outside of a track command");
|
||||
else
|
||||
//take care to |= it here, so the data flag doesn't get cleared
|
||||
curr_track.Flags |= (cmd as CUE_File.Command.FLAGS).Flags;
|
||||
}
|
||||
|
||||
//it's a bit odd to close the file before closing the track, but...
|
||||
//we need to be sure to CloseFile first to make sure the track is marked as the final one in the file
|
||||
CloseFile();
|
||||
CloseTrack();
|
||||
if (cmd is CUE_File.Command.TRACK)
|
||||
{
|
||||
CloseTrack();
|
||||
OpenTrack(cmd as CUE_File.Command.TRACK);
|
||||
}
|
||||
|
||||
CreateTrack1Pregap();
|
||||
FinalAnalysis();
|
||||
if (cmd is CUE_File.Command.FILE)
|
||||
{
|
||||
CloseFile();
|
||||
OpenFile(cmd as CUE_File.Command.FILE);
|
||||
}
|
||||
|
||||
if (cmd is CUE_File.Command.INDEX)
|
||||
{
|
||||
//todo - validate no postgap specified
|
||||
AddIndex(cmd as CUE_File.Command.INDEX);
|
||||
}
|
||||
|
||||
if (cmd is CUE_File.Command.PREGAP)
|
||||
{
|
||||
//validate track open
|
||||
//validate no indexes
|
||||
curr_track.PregapLength = (cmd as CUE_File.Command.PREGAP).Length;
|
||||
}
|
||||
|
||||
if (cmd is CUE_File.Command.POSTGAP)
|
||||
{
|
||||
curr_track.PostgapLength = (cmd as CUE_File.Command.POSTGAP).Length;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//it's a bit odd to close the file before closing the track, but...
|
||||
//we need to be sure to CloseFile first to make sure the track is marked as the final one in the file
|
||||
CloseFile();
|
||||
CloseTrack();
|
||||
|
||||
CreateTrack1Pregap();
|
||||
FinalAnalysis();
|
||||
|
||||
} //Run()
|
||||
} //Run()
|
||||
|
||||
} //class CompileCueJob
|
||||
|
||||
} //partial class CUE_Format2
|
||||
} //class CompileCueJob
|
||||
|
||||
} //namespace BizHawk.Emulation.DiscSystem
|
|
@ -6,9 +6,9 @@ using System.Collections.Generic;
|
|||
|
||||
//http://digitalx.org/cue-sheet/index.html "all cue sheet information is a straight 1:1 copy from the cdrwin helpfile"
|
||||
|
||||
namespace BizHawk.Emulation.DiscSystem
|
||||
namespace BizHawk.Emulation.DiscSystem.CUE
|
||||
{
|
||||
public partial class CUE_Context
|
||||
public class CUE_Context
|
||||
{
|
||||
/// <summary>
|
||||
/// The CueFileResolver to be used by this instance
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BizHawk.Emulation.DiscSystem.CUE
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the contents of a cue file
|
||||
/// </summary>
|
||||
class CUE_File
|
||||
{
|
||||
// (here are all the commands we can encounter)
|
||||
public static class Command
|
||||
{
|
||||
//TODO - record line number origin of command? Kind of nice but inessential
|
||||
public class CATALOG { public string Value; public override string ToString() { return string.Format("CATALOG: {0}", Value); } }
|
||||
public class CDTEXTFILE { public string Path; public override string ToString() { return string.Format("CDTEXTFILE: {0}", Path); } }
|
||||
public class FILE { public string Path; public CueFileType Type; public override string ToString() { return string.Format("FILE ({0}): {1}", Type, Path); } }
|
||||
public class FLAGS { public CueTrackFlags Flags; public override string ToString() { return string.Format("FLAGS {0}", Flags); } }
|
||||
public class INDEX { public int Number; public Timestamp Timestamp; public override string ToString() { return string.Format("INDEX {0,2} {1}", Number, Timestamp); } }
|
||||
public class ISRC { public string Value; public override string ToString() { return string.Format("ISRC: {0}", Value); } }
|
||||
public class PERFORMER { public string Value; public override string ToString() { return string.Format("PERFORMER: {0}", Value); } }
|
||||
public class POSTGAP { public Timestamp Length; public override string ToString() { return string.Format("POSTGAP: {0}", Length); } }
|
||||
public class PREGAP { public Timestamp Length; public override string ToString() { return string.Format("PREGAP: {0}", Length); } }
|
||||
public class REM { public string Value; public override string ToString() { return string.Format("REM: {0}", Value); } }
|
||||
public class COMMENT { public string Value; public override string ToString() { return string.Format("COMMENT: {0}", Value); } }
|
||||
public class SONGWRITER { public string Value; public override string ToString() { return string.Format("SONGWRITER: {0}", Value); } }
|
||||
public class TITLE { public string Value; public override string ToString() { return string.Format("TITLE: {0}", Value); } }
|
||||
public class TRACK { public int Number; public CueTrackType Type; public override string ToString() { return string.Format("TRACK {0,2} ({1})", Number, Type); } }
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Stuff other than the commands, global for the whole disc
|
||||
/// </summary>
|
||||
public class DiscInfo
|
||||
{
|
||||
public Command.CATALOG Catalog;
|
||||
public Command.ISRC ISRC;
|
||||
public Command.CDTEXTFILE CDTextFile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The sequential list of commands parsed out of the cue file
|
||||
/// </summary>
|
||||
public List<object> Commands = new List<object>();
|
||||
|
||||
/// <summary>
|
||||
/// Stuff other than the commands, global for the whole disc
|
||||
/// </summary>
|
||||
public DiscInfo GlobalDiscInfo = new DiscInfo();
|
||||
}
|
||||
}
|
|
@ -23,381 +23,378 @@ using System.Text;
|
|||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BizHawk.Emulation.DiscSystem
|
||||
namespace BizHawk.Emulation.DiscSystem.CUE
|
||||
{
|
||||
partial class CUE_Context
|
||||
/// <summary>
|
||||
/// Loads a cue file into a Disc.
|
||||
/// For this job, virtually all nonsense input is treated as errors, but the process will try to recover as best it can.
|
||||
/// The user should still reject any jobs which generated errors
|
||||
/// </summary>
|
||||
internal class LoadCueJob : DiscJob
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads a cue file into a Disc.
|
||||
/// For this job, virtually all nonsense input is treated as errors, but the process will try to recover as best it can.
|
||||
/// The user should still reject any jobs which generated errors
|
||||
/// The results of the compile job, a prerequisite for this
|
||||
/// </summary>
|
||||
internal class LoadCueJob : DiscJob
|
||||
public CompileCueJob IN_CompileJob;
|
||||
|
||||
/// <summary>
|
||||
/// The resulting disc
|
||||
/// </summary>
|
||||
public Disc OUT_Disc;
|
||||
|
||||
private enum BurnType
|
||||
{
|
||||
/// <summary>
|
||||
/// The results of the compile job, a prerequisite for this
|
||||
/// </summary>
|
||||
public CompileCueJob IN_CompileJob;
|
||||
Normal, Pregap, Postgap
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The resulting disc
|
||||
/// </summary>
|
||||
public Disc OUT_Disc;
|
||||
class BlobInfo
|
||||
{
|
||||
public IBlob Blob;
|
||||
public long Length;
|
||||
}
|
||||
|
||||
private enum BurnType
|
||||
//not sure if we need this...
|
||||
class TrackInfo
|
||||
{
|
||||
public int Length;
|
||||
|
||||
public CompiledCueTrack CompiledCueTrack;
|
||||
}
|
||||
|
||||
List<BlobInfo> BlobInfos;
|
||||
List<TrackInfo> TrackInfos = new List<TrackInfo>();
|
||||
|
||||
|
||||
void MountBlobs()
|
||||
{
|
||||
IBlob file_blob = null;
|
||||
|
||||
BlobInfos = new List<BlobInfo>();
|
||||
foreach (var ccf in IN_CompileJob.OUT_CompiledCueFiles)
|
||||
{
|
||||
Normal, Pregap, Postgap
|
||||
}
|
||||
var bi = new BlobInfo();
|
||||
BlobInfos.Add(bi);
|
||||
|
||||
class BlobInfo
|
||||
{
|
||||
public IBlob Blob;
|
||||
public long Length;
|
||||
}
|
||||
|
||||
//not sure if we need this...
|
||||
class TrackInfo
|
||||
{
|
||||
public int Length;
|
||||
|
||||
public CompiledCueTrack CompiledCueTrack;
|
||||
}
|
||||
|
||||
List<BlobInfo> BlobInfos;
|
||||
List<TrackInfo> TrackInfos = new List<TrackInfo>();
|
||||
|
||||
|
||||
void MountBlobs()
|
||||
{
|
||||
IBlob file_blob = null;
|
||||
|
||||
BlobInfos = new List<BlobInfo>();
|
||||
foreach (var ccf in IN_CompileJob.OUT_CompiledCueFiles)
|
||||
switch (ccf.Type)
|
||||
{
|
||||
var bi = new BlobInfo();
|
||||
BlobInfos.Add(bi);
|
||||
|
||||
switch (ccf.Type)
|
||||
{
|
||||
case CompiledCueFileType.BIN:
|
||||
case CompiledCueFileType.Unknown:
|
||||
{
|
||||
//raw files:
|
||||
var blob = new Disc.Blob_RawFile { PhysicalPath = ccf.FullPath };
|
||||
OUT_Disc.DisposableResources.Add(file_blob = blob);
|
||||
bi.Length = blob.Length;
|
||||
break;
|
||||
}
|
||||
case CompiledCueFileType.ECM:
|
||||
{
|
||||
var blob = new Disc.Blob_ECM();
|
||||
OUT_Disc.DisposableResources.Add(file_blob = blob);
|
||||
blob.Load(ccf.FullPath);
|
||||
bi.Length = blob.Length;
|
||||
break;
|
||||
}
|
||||
case CompiledCueFileType.WAVE:
|
||||
{
|
||||
var blob = new Disc.Blob_WaveFile();
|
||||
OUT_Disc.DisposableResources.Add(file_blob = blob);
|
||||
blob.Load(ccf.FullPath);
|
||||
bi.Length = blob.Length;
|
||||
break;
|
||||
}
|
||||
case CompiledCueFileType.DecodeAudio:
|
||||
{
|
||||
FFMpeg ffmpeg = new FFMpeg();
|
||||
if (!ffmpeg.QueryServiceAvailable())
|
||||
{
|
||||
throw new DiscReferenceException(ccf.FullPath, "No decoding service was available (make sure ffmpeg.exe is available. even though this may be a wav, ffmpeg is used to load oddly formatted wave files. If you object to this, please send us a note and we'll see what we can do. It shouldn't be too hard.)");
|
||||
}
|
||||
AudioDecoder dec = new AudioDecoder();
|
||||
byte[] buf = dec.AcquireWaveData(ccf.FullPath);
|
||||
var blob = new Disc.Blob_WaveFile();
|
||||
OUT_Disc.DisposableResources.Add(file_blob = blob);
|
||||
blob.Load(new MemoryStream(buf));
|
||||
bi.Length = buf.Length;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new InvalidOperationException();
|
||||
} //switch(file type)
|
||||
|
||||
//wrap all the blobs with zero padding
|
||||
bi.Blob = new Disc.Blob_ZeroPadAdapter(file_blob, bi.Length);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void AnalyzeTracks()
|
||||
{
|
||||
var compiledTracks = IN_CompileJob.OUT_CompiledCueTracks;
|
||||
|
||||
for(int t=0;t<compiledTracks.Count;t++)
|
||||
{
|
||||
var cct = compiledTracks[t];
|
||||
|
||||
var ti = new TrackInfo() { CompiledCueTrack = cct };
|
||||
TrackInfos.Add(ti);
|
||||
|
||||
//OH NO! CANT DO THIS!
|
||||
//need to read sectors from file to reliably know its ending size.
|
||||
//could determine it from file mode.
|
||||
//do we really need this?
|
||||
//if (cct.IsFinalInFile)
|
||||
//{
|
||||
// //length is determined from length of file
|
||||
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
void EmitRawTOCEntry(CompiledCueTrack cct)
|
||||
{
|
||||
SubchannelQ toc_sq = new SubchannelQ();
|
||||
//absent some kind of policy for how to set it, this is a safe assumption:
|
||||
byte toc_ADR = 1;
|
||||
toc_sq.SetStatus(toc_ADR, (EControlQ)(int)cct.Flags);
|
||||
toc_sq.q_tno.BCDValue = 0; //kind of a little weird here.. the track number becomes the 'point' and put in the index instead. 0 is the track number here.
|
||||
toc_sq.q_index = BCD2.FromDecimal(cct.Number);
|
||||
//not too sure about these yet
|
||||
toc_sq.min = BCD2.FromDecimal(0);
|
||||
toc_sq.sec = BCD2.FromDecimal(0);
|
||||
toc_sq.frame = BCD2.FromDecimal(0);
|
||||
toc_sq.AP_Timestamp = new Timestamp(OUT_Disc.Sectors.Count);
|
||||
OUT_Disc.RawTOCEntries.Add(new RawTOCEntry { QData = toc_sq });
|
||||
}
|
||||
|
||||
public void Run()
|
||||
{
|
||||
//params
|
||||
var compiled = IN_CompileJob;
|
||||
var context = compiled.IN_CueFormat;
|
||||
OUT_Disc = new Disc();
|
||||
|
||||
//generation state
|
||||
int curr_index;
|
||||
int curr_blobIndex = -1;
|
||||
int curr_blobMSF = -1;
|
||||
BlobInfo curr_blobInfo = null;
|
||||
long curr_blobOffset = -1;
|
||||
|
||||
//mount all input files
|
||||
MountBlobs();
|
||||
|
||||
//unhappily, we cannot determine the length of all the tracks without knowing the length of the files
|
||||
//now that the files are mounted, we can figure the track lengths
|
||||
AnalyzeTracks();
|
||||
|
||||
//loop from track 1 to 99
|
||||
//(track 0 isnt handled yet, that's way distant work)
|
||||
for (int t = 1; t < TrackInfos.Count; t++)
|
||||
{
|
||||
TrackInfo ti = TrackInfos[t];
|
||||
CompiledCueTrack cct = ti.CompiledCueTrack;
|
||||
|
||||
//---------------------------------
|
||||
//setup track pregap processing
|
||||
//per "Example 05" on digitalx.org, pregap can come from index specification and pregap command
|
||||
int specifiedPregapLength = cct.PregapLength.Sector;
|
||||
int impliedPregapLength = cct.Indexes[1].FileMSF.Sector - cct.Indexes[0].FileMSF.Sector;
|
||||
int totalPregapLength = specifiedPregapLength + impliedPregapLength;
|
||||
|
||||
//from now on we'll track relative timestamp and increment it continually
|
||||
int relMSF = -totalPregapLength;
|
||||
|
||||
//read more at policies declaration
|
||||
//if (!context.DiscMountPolicy.CUE_PauseContradictionModeA)
|
||||
// relMSF += 1;
|
||||
//---------------------------------
|
||||
|
||||
|
||||
//---------------------------------
|
||||
//generate sectors for this track.
|
||||
|
||||
//advance to the next file if needed
|
||||
if (curr_blobIndex != cct.BlobIndex)
|
||||
{
|
||||
curr_blobIndex = cct.BlobIndex;
|
||||
curr_blobOffset = 0;
|
||||
curr_blobMSF = 0;
|
||||
curr_blobInfo = BlobInfos[curr_blobIndex];
|
||||
}
|
||||
|
||||
//work until the next track is reached, or the end of the current file is reached, depending on the track type
|
||||
curr_index = 0;
|
||||
for (; ; )
|
||||
{
|
||||
bool trackDone = false;
|
||||
bool generateGap = false;
|
||||
|
||||
if (specifiedPregapLength > 0)
|
||||
case CompiledCueFileType.BIN:
|
||||
case CompiledCueFileType.Unknown:
|
||||
{
|
||||
//if burning through a specified pregap, count it down
|
||||
generateGap = true;
|
||||
specifiedPregapLength--;
|
||||
}
|
||||
else
|
||||
{
|
||||
//if burning through the file, select the appropriate index by inspecting the next index and seeing if we've reached it
|
||||
for (; ; )
|
||||
{
|
||||
if (curr_index == cct.Indexes.Count - 1)
|
||||
break;
|
||||
if (curr_blobMSF >= cct.Indexes[curr_index + 1].FileMSF.Sector)
|
||||
{
|
||||
curr_index++;
|
||||
if (curr_index == 1)
|
||||
{
|
||||
//WE ARE NOW AT INDEX 1: generate the RawTOCEntry for this track
|
||||
EmitRawTOCEntry(cct);
|
||||
}
|
||||
}
|
||||
else break;
|
||||
}
|
||||
}
|
||||
|
||||
//select the track type for the subQ
|
||||
//it's obviously the same as the main track type usually, but during a pregap it can be different
|
||||
TrackInfo qTrack = ti;
|
||||
int qRelMSF = relMSF;
|
||||
if (curr_index == 0)
|
||||
{
|
||||
//tweak relMSF due to ambiguity/contradiction in yellowbook docs
|
||||
if (!context.DiscMountPolicy.CUE_PregapContradictionModeA)
|
||||
qRelMSF++;
|
||||
|
||||
//[IEC10149] says there's two "intervals" of a pregap.
|
||||
//mednafen's pseudocode interpretation of this:
|
||||
//if this is a data track and the previous track was not data, the last 150 sectors of the pregap match this track and the earlier sectors (at least 75) math the previous track
|
||||
//I agree, so let's do it that way
|
||||
if (t != 1 && cct.TrackType != CueFile.TrackType.Audio && TrackInfos[t - 1].CompiledCueTrack.TrackType == CueFile.TrackType.Audio)
|
||||
{
|
||||
if (relMSF < -150)
|
||||
{
|
||||
qTrack = TrackInfos[t - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//generate the right kind of sector synth for this track
|
||||
SS_Base ss = null;
|
||||
if (generateGap)
|
||||
{
|
||||
var ss_gap = new SS_Gap();
|
||||
ss_gap.TrackType = qTrack.CompiledCueTrack.TrackType;
|
||||
ss = ss_gap;
|
||||
}
|
||||
else
|
||||
{
|
||||
int sectorSize = int.MaxValue;
|
||||
switch (qTrack.CompiledCueTrack.TrackType)
|
||||
{
|
||||
case CueFile.TrackType.Audio:
|
||||
case CueFile.TrackType.CDI_2352:
|
||||
case CueFile.TrackType.Mode1_2352:
|
||||
case CueFile.TrackType.Mode2_2352:
|
||||
ss = new SS_2352();
|
||||
sectorSize = 2352;
|
||||
break;
|
||||
|
||||
case CueFile.TrackType.Mode1_2048:
|
||||
ss = new SS_Mode1_2048();
|
||||
sectorSize = 2048;
|
||||
break;
|
||||
|
||||
default:
|
||||
case CueFile.TrackType.Mode2_2336:
|
||||
throw new InvalidOperationException("Not supported: " + cct.TrackType);
|
||||
}
|
||||
|
||||
ss.Blob = curr_blobInfo.Blob;
|
||||
ss.BlobOffset = curr_blobOffset;
|
||||
curr_blobOffset += sectorSize;
|
||||
curr_blobMSF++;
|
||||
}
|
||||
|
||||
ss.Policy = context.DiscMountPolicy;
|
||||
|
||||
//setup subQ
|
||||
byte ADR = 1; //absent some kind of policy for how to set it, this is a safe assumption:
|
||||
ss.sq.SetStatus(ADR, (EControlQ)(int)qTrack.CompiledCueTrack.Flags);
|
||||
ss.sq.q_tno = BCD2.FromDecimal(cct.Number);
|
||||
ss.sq.q_index = BCD2.FromDecimal(curr_index);
|
||||
ss.sq.AP_Timestamp = new Timestamp(OUT_Disc.Sectors.Count);
|
||||
ss.sq.Timestamp = new Timestamp(qRelMSF);
|
||||
|
||||
//setup subP
|
||||
if (curr_index == 0)
|
||||
ss.Pause = true;
|
||||
|
||||
OUT_Disc.Sectors.Add(ss);
|
||||
relMSF++;
|
||||
|
||||
if (cct.IsFinalInFile)
|
||||
{
|
||||
//sometimes, break when the file is exhausted
|
||||
if (curr_blobOffset >= curr_blobInfo.Length)
|
||||
trackDone = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
//other times, break when the track is done
|
||||
//(this check is safe because it's not the final track overall if it's not the final track in a file)
|
||||
if (curr_blobMSF >= TrackInfos[t + 1].CompiledCueTrack.Indexes[0].FileMSF.Sector)
|
||||
trackDone = true;
|
||||
}
|
||||
|
||||
if (trackDone)
|
||||
//raw files:
|
||||
var blob = new Disc.Blob_RawFile { PhysicalPath = ccf.FullPath };
|
||||
OUT_Disc.DisposableResources.Add(file_blob = blob);
|
||||
bi.Length = blob.Length;
|
||||
break;
|
||||
}
|
||||
case CompiledCueFileType.ECM:
|
||||
{
|
||||
var blob = new Disc.Blob_ECM();
|
||||
OUT_Disc.DisposableResources.Add(file_blob = blob);
|
||||
blob.Load(ccf.FullPath);
|
||||
bi.Length = blob.Length;
|
||||
break;
|
||||
}
|
||||
case CompiledCueFileType.WAVE:
|
||||
{
|
||||
var blob = new Disc.Blob_WaveFile();
|
||||
OUT_Disc.DisposableResources.Add(file_blob = blob);
|
||||
blob.Load(ccf.FullPath);
|
||||
bi.Length = blob.Length;
|
||||
break;
|
||||
}
|
||||
case CompiledCueFileType.DecodeAudio:
|
||||
{
|
||||
FFMpeg ffmpeg = new FFMpeg();
|
||||
if (!ffmpeg.QueryServiceAvailable())
|
||||
{
|
||||
throw new DiscReferenceException(ccf.FullPath, "No decoding service was available (make sure ffmpeg.exe is available. even though this may be a wav, ffmpeg is used to load oddly formatted wave files. If you object to this, please send us a note and we'll see what we can do. It shouldn't be too hard.)");
|
||||
}
|
||||
AudioDecoder dec = new AudioDecoder();
|
||||
byte[] buf = dec.AcquireWaveData(ccf.FullPath);
|
||||
var blob = new Disc.Blob_WaveFile();
|
||||
OUT_Disc.DisposableResources.Add(file_blob = blob);
|
||||
blob.Load(new MemoryStream(buf));
|
||||
bi.Length = buf.Length;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new InvalidOperationException();
|
||||
} //switch(file type)
|
||||
|
||||
//wrap all the blobs with zero padding
|
||||
bi.Blob = new Disc.Blob_ZeroPadAdapter(file_blob, bi.Length);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void AnalyzeTracks()
|
||||
{
|
||||
var compiledTracks = IN_CompileJob.OUT_CompiledCueTracks;
|
||||
|
||||
for(int t=0;t<compiledTracks.Count;t++)
|
||||
{
|
||||
var cct = compiledTracks[t];
|
||||
|
||||
var ti = new TrackInfo() { CompiledCueTrack = cct };
|
||||
TrackInfos.Add(ti);
|
||||
|
||||
//OH NO! CANT DO THIS!
|
||||
//need to read sectors from file to reliably know its ending size.
|
||||
//could determine it from file mode.
|
||||
//do we really need this?
|
||||
//if (cct.IsFinalInFile)
|
||||
//{
|
||||
// //length is determined from length of file
|
||||
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
void EmitRawTOCEntry(CompiledCueTrack cct)
|
||||
{
|
||||
SubchannelQ toc_sq = new SubchannelQ();
|
||||
//absent some kind of policy for how to set it, this is a safe assumption:
|
||||
byte toc_ADR = 1;
|
||||
toc_sq.SetStatus(toc_ADR, (EControlQ)(int)cct.Flags);
|
||||
toc_sq.q_tno.BCDValue = 0; //kind of a little weird here.. the track number becomes the 'point' and put in the index instead. 0 is the track number here.
|
||||
toc_sq.q_index = BCD2.FromDecimal(cct.Number);
|
||||
//not too sure about these yet
|
||||
toc_sq.min = BCD2.FromDecimal(0);
|
||||
toc_sq.sec = BCD2.FromDecimal(0);
|
||||
toc_sq.frame = BCD2.FromDecimal(0);
|
||||
toc_sq.AP_Timestamp = new Timestamp(OUT_Disc.Sectors.Count);
|
||||
OUT_Disc.RawTOCEntries.Add(new RawTOCEntry { QData = toc_sq });
|
||||
}
|
||||
|
||||
public void Run()
|
||||
{
|
||||
//params
|
||||
var compiled = IN_CompileJob;
|
||||
var context = compiled.IN_CueFormat;
|
||||
OUT_Disc = new Disc();
|
||||
|
||||
//generation state
|
||||
int curr_index;
|
||||
int curr_blobIndex = -1;
|
||||
int curr_blobMSF = -1;
|
||||
BlobInfo curr_blobInfo = null;
|
||||
long curr_blobOffset = -1;
|
||||
|
||||
//mount all input files
|
||||
MountBlobs();
|
||||
|
||||
//unhappily, we cannot determine the length of all the tracks without knowing the length of the files
|
||||
//now that the files are mounted, we can figure the track lengths
|
||||
AnalyzeTracks();
|
||||
|
||||
//loop from track 1 to 99
|
||||
//(track 0 isnt handled yet, that's way distant work)
|
||||
for (int t = 1; t < TrackInfos.Count; t++)
|
||||
{
|
||||
TrackInfo ti = TrackInfos[t];
|
||||
CompiledCueTrack cct = ti.CompiledCueTrack;
|
||||
|
||||
//---------------------------------
|
||||
//setup track pregap processing
|
||||
//per "Example 05" on digitalx.org, pregap can come from index specification and pregap command
|
||||
int specifiedPregapLength = cct.PregapLength.Sector;
|
||||
int impliedPregapLength = cct.Indexes[1].FileMSF.Sector - cct.Indexes[0].FileMSF.Sector;
|
||||
int totalPregapLength = specifiedPregapLength + impliedPregapLength;
|
||||
|
||||
//from now on we'll track relative timestamp and increment it continually
|
||||
int relMSF = -totalPregapLength;
|
||||
|
||||
//read more at policies declaration
|
||||
//if (!context.DiscMountPolicy.CUE_PauseContradictionModeA)
|
||||
// relMSF += 1;
|
||||
//---------------------------------
|
||||
|
||||
|
||||
//---------------------------------
|
||||
//generate sectors for this track.
|
||||
|
||||
//advance to the next file if needed
|
||||
if (curr_blobIndex != cct.BlobIndex)
|
||||
{
|
||||
curr_blobIndex = cct.BlobIndex;
|
||||
curr_blobOffset = 0;
|
||||
curr_blobMSF = 0;
|
||||
curr_blobInfo = BlobInfos[curr_blobIndex];
|
||||
}
|
||||
|
||||
//work until the next track is reached, or the end of the current file is reached, depending on the track type
|
||||
curr_index = 0;
|
||||
for (; ; )
|
||||
{
|
||||
bool trackDone = false;
|
||||
bool generateGap = false;
|
||||
|
||||
if (specifiedPregapLength > 0)
|
||||
{
|
||||
//if burning through a specified pregap, count it down
|
||||
generateGap = true;
|
||||
specifiedPregapLength--;
|
||||
}
|
||||
else
|
||||
{
|
||||
//if burning through the file, select the appropriate index by inspecting the next index and seeing if we've reached it
|
||||
for (; ; )
|
||||
{
|
||||
if (curr_index == cct.Indexes.Count - 1)
|
||||
break;
|
||||
if (curr_blobMSF >= cct.Indexes[curr_index + 1].FileMSF.Sector)
|
||||
{
|
||||
curr_index++;
|
||||
if (curr_index == 1)
|
||||
{
|
||||
//WE ARE NOW AT INDEX 1: generate the RawTOCEntry for this track
|
||||
EmitRawTOCEntry(cct);
|
||||
}
|
||||
}
|
||||
else break;
|
||||
}
|
||||
}
|
||||
|
||||
//---------------------------------
|
||||
//gen postgap sectors
|
||||
int specifiedPostgapLength = cct.PostgapLength.Sector;
|
||||
for (int s = 0; s < specifiedPostgapLength; s++)
|
||||
//select the track type for the subQ
|
||||
//it's obviously the same as the main track type usually, but during a pregap it can be different
|
||||
TrackInfo qTrack = ti;
|
||||
int qRelMSF = relMSF;
|
||||
if (curr_index == 0)
|
||||
{
|
||||
var ss = new SS_Gap();
|
||||
ss.TrackType = cct.TrackType; //TODO - old track type in some < -150 cases?
|
||||
//tweak relMSF due to ambiguity/contradiction in yellowbook docs
|
||||
if (!context.DiscMountPolicy.CUE_PregapContradictionModeA)
|
||||
qRelMSF++;
|
||||
|
||||
//-subq-
|
||||
byte ADR = 1;
|
||||
ss.sq.SetStatus(ADR, (EControlQ)(int)cct.Flags);
|
||||
ss.sq.q_tno = BCD2.FromDecimal(cct.Number);
|
||||
ss.sq.q_index = BCD2.FromDecimal(curr_index);
|
||||
ss.sq.AP_Timestamp = new Timestamp(OUT_Disc.Sectors.Count);
|
||||
ss.sq.Timestamp = new Timestamp(relMSF);
|
||||
//[IEC10149] says there's two "intervals" of a pregap.
|
||||
//mednafen's pseudocode interpretation of this:
|
||||
//if this is a data track and the previous track was not data, the last 150 sectors of the pregap match this track and the earlier sectors (at least 75) math the previous track
|
||||
//I agree, so let's do it that way
|
||||
if (t != 1 && cct.TrackType != CueTrackType.Audio && TrackInfos[t - 1].CompiledCueTrack.TrackType == CueTrackType.Audio)
|
||||
{
|
||||
if (relMSF < -150)
|
||||
{
|
||||
qTrack = TrackInfos[t - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//-subP-
|
||||
//always paused--is this good enough?
|
||||
//generate the right kind of sector synth for this track
|
||||
SS_Base ss = null;
|
||||
if (generateGap)
|
||||
{
|
||||
var ss_gap = new SS_Gap();
|
||||
ss_gap.TrackType = qTrack.CompiledCueTrack.TrackType;
|
||||
ss = ss_gap;
|
||||
}
|
||||
else
|
||||
{
|
||||
int sectorSize = int.MaxValue;
|
||||
switch (qTrack.CompiledCueTrack.TrackType)
|
||||
{
|
||||
case CueTrackType.Audio:
|
||||
case CueTrackType.CDI_2352:
|
||||
case CueTrackType.Mode1_2352:
|
||||
case CueTrackType.Mode2_2352:
|
||||
ss = new SS_2352();
|
||||
sectorSize = 2352;
|
||||
break;
|
||||
|
||||
case CueTrackType.Mode1_2048:
|
||||
ss = new SS_Mode1_2048();
|
||||
sectorSize = 2048;
|
||||
break;
|
||||
|
||||
default:
|
||||
case CueTrackType.Mode2_2336:
|
||||
throw new InvalidOperationException("Not supported: " + cct.TrackType);
|
||||
}
|
||||
|
||||
ss.Blob = curr_blobInfo.Blob;
|
||||
ss.BlobOffset = curr_blobOffset;
|
||||
curr_blobOffset += sectorSize;
|
||||
curr_blobMSF++;
|
||||
}
|
||||
|
||||
ss.Policy = context.DiscMountPolicy;
|
||||
|
||||
//setup subQ
|
||||
byte ADR = 1; //absent some kind of policy for how to set it, this is a safe assumption:
|
||||
ss.sq.SetStatus(ADR, (EControlQ)(int)qTrack.CompiledCueTrack.Flags);
|
||||
ss.sq.q_tno = BCD2.FromDecimal(cct.Number);
|
||||
ss.sq.q_index = BCD2.FromDecimal(curr_index);
|
||||
ss.sq.AP_Timestamp = new Timestamp(OUT_Disc.Sectors.Count);
|
||||
ss.sq.Timestamp = new Timestamp(qRelMSF);
|
||||
|
||||
//setup subP
|
||||
if (curr_index == 0)
|
||||
ss.Pause = true;
|
||||
|
||||
OUT_Disc.Sectors.Add(ss);
|
||||
relMSF++;
|
||||
OUT_Disc.Sectors.Add(ss);
|
||||
relMSF++;
|
||||
|
||||
if (cct.IsFinalInFile)
|
||||
{
|
||||
//sometimes, break when the file is exhausted
|
||||
if (curr_blobOffset >= curr_blobInfo.Length)
|
||||
trackDone = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
//other times, break when the track is done
|
||||
//(this check is safe because it's not the final track overall if it's not the final track in a file)
|
||||
if (curr_blobMSF >= TrackInfos[t + 1].CompiledCueTrack.Indexes[0].FileMSF.Sector)
|
||||
trackDone = true;
|
||||
}
|
||||
|
||||
if (trackDone)
|
||||
break;
|
||||
}
|
||||
|
||||
} //end track loop
|
||||
//---------------------------------
|
||||
//gen postgap sectors
|
||||
int specifiedPostgapLength = cct.PostgapLength.Sector;
|
||||
for (int s = 0; s < specifiedPostgapLength; s++)
|
||||
{
|
||||
var ss = new SS_Gap();
|
||||
ss.TrackType = cct.TrackType; //TODO - old track type in some < -150 cases?
|
||||
|
||||
//-subq-
|
||||
byte ADR = 1;
|
||||
ss.sq.SetStatus(ADR, (EControlQ)(int)cct.Flags);
|
||||
ss.sq.q_tno = BCD2.FromDecimal(cct.Number);
|
||||
ss.sq.q_index = BCD2.FromDecimal(curr_index);
|
||||
ss.sq.AP_Timestamp = new Timestamp(OUT_Disc.Sectors.Count);
|
||||
ss.sq.Timestamp = new Timestamp(relMSF);
|
||||
|
||||
//-subP-
|
||||
//always paused--is this good enough?
|
||||
ss.Pause = true;
|
||||
|
||||
OUT_Disc.Sectors.Add(ss);
|
||||
relMSF++;
|
||||
}
|
||||
|
||||
|
||||
//add RawTOCEntries A0 A1 A2 to round out the TOC
|
||||
var TOCMiscInfo = new Synthesize_A0A1A2_Job {
|
||||
IN_FirstRecordedTrackNumber = IN_CompileJob.OUT_CompiledDiscInfo.FirstRecordedTrackNumber,
|
||||
IN_LastRecordedTrackNumber = IN_CompileJob.OUT_CompiledDiscInfo.LastRecordedTrackNumber,
|
||||
IN_Session1Format = IN_CompileJob.OUT_CompiledDiscInfo.SessionFormat,
|
||||
IN_LeadoutTimestamp = new Timestamp(OUT_Disc.Sectors.Count) //do we need a +150?
|
||||
};
|
||||
TOCMiscInfo.Run(OUT_Disc.RawTOCEntries);
|
||||
} //end track loop
|
||||
|
||||
//TODO - generate leadout, or delegates at least
|
||||
|
||||
//blech, old crap, maybe
|
||||
//OUT_Disc.Structure.Synthesize_TOCPointsFromSessions();
|
||||
//add RawTOCEntries A0 A1 A2 to round out the TOC
|
||||
var TOCMiscInfo = new Synthesize_A0A1A2_Job {
|
||||
IN_FirstRecordedTrackNumber = IN_CompileJob.OUT_CompiledDiscInfo.FirstRecordedTrackNumber,
|
||||
IN_LastRecordedTrackNumber = IN_CompileJob.OUT_CompiledDiscInfo.LastRecordedTrackNumber,
|
||||
IN_Session1Format = IN_CompileJob.OUT_CompiledDiscInfo.SessionFormat,
|
||||
IN_LeadoutTimestamp = new Timestamp(OUT_Disc.Sectors.Count) //do we need a +150?
|
||||
};
|
||||
TOCMiscInfo.Run(OUT_Disc.RawTOCEntries);
|
||||
|
||||
//FinishLog();
|
||||
//TODO - generate leadout, or delegates at least
|
||||
|
||||
} //Run()
|
||||
} //class LoadCueJob
|
||||
} //partial class CUE_Format2
|
||||
//blech, old crap, maybe
|
||||
//OUT_Disc.Structure.Synthesize_TOCPointsFromSessions();
|
||||
|
||||
//FinishLog();
|
||||
|
||||
} //Run()
|
||||
} //class LoadCueJob
|
||||
} //namespace BizHawk.Emulation.DiscSystem
|
||||
|
||||
|
|
|
@ -10,444 +10,355 @@ using System.Collections.Generic;
|
|||
//http://www.gnu.org/software/libcdio/libcdio.html#Sectors
|
||||
//this is actually a great reference. they use LSN instead of LBA.. maybe a good idea for us
|
||||
|
||||
namespace BizHawk.Emulation.DiscSystem
|
||||
namespace BizHawk.Emulation.DiscSystem.CUE
|
||||
{
|
||||
partial class CUE_Context
|
||||
/// <summary>
|
||||
/// Performs minimum parse processing on a cue file
|
||||
/// </summary>
|
||||
class ParseCueJob : DiscJob
|
||||
{
|
||||
public class ParseCueJob : DiscJob
|
||||
{
|
||||
/// <summary>
|
||||
/// input: the cue string to parse
|
||||
/// </summary>
|
||||
public string IN_CueString;
|
||||
|
||||
/// <summary>
|
||||
/// output: the resulting minimally-processed cue file
|
||||
/// </summary>
|
||||
public CueFile OUT_CueFile;
|
||||
}
|
||||
/// <summary>
|
||||
/// input: the cue string to parse
|
||||
/// </summary>
|
||||
public string IN_CueString;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the contents of a cue file
|
||||
/// output: the resulting minimally-processed cue file
|
||||
/// </summary>
|
||||
public class CueFile
|
||||
public CUE_File OUT_CueFile;
|
||||
|
||||
|
||||
class CueLineParser
|
||||
{
|
||||
|
||||
// (here are all the commands we can encounter)
|
||||
public static class Command
|
||||
int index;
|
||||
string str;
|
||||
public bool EOF;
|
||||
public CueLineParser(string line)
|
||||
{
|
||||
//TODO - record line number origin of command? Kind of nice but inessential
|
||||
public class CATALOG { public string Value; public override string ToString() { return string.Format("CATALOG: {0}", Value); } }
|
||||
public class CDTEXTFILE { public string Path; public override string ToString() { return string.Format("CDTEXTFILE: {0}", Path); } }
|
||||
public class FILE { public string Path; public FileType Type; public override string ToString() { return string.Format("FILE ({0}): {1}", Type, Path); } }
|
||||
public class FLAGS { public TrackFlags Flags; public override string ToString() { return string.Format("FLAGS {0}", Flags); } }
|
||||
public class INDEX { public int Number; public Timestamp Timestamp; public override string ToString() { return string.Format("INDEX {0,2} {1}", Number, Timestamp); } }
|
||||
public class ISRC { public string Value; public override string ToString() { return string.Format("ISRC: {0}", Value); } }
|
||||
public class PERFORMER { public string Value; public override string ToString() { return string.Format("PERFORMER: {0}", Value); } }
|
||||
public class POSTGAP { public Timestamp Length; public override string ToString() { return string.Format("POSTGAP: {0}", Length); } }
|
||||
public class PREGAP { public Timestamp Length; public override string ToString() { return string.Format("PREGAP: {0}", Length); } }
|
||||
public class REM { public string Value; public override string ToString() { return string.Format("REM: {0}", Value); } }
|
||||
public class COMMENT { public string Value; public override string ToString() { return string.Format("COMMENT: {0}", Value); } }
|
||||
public class SONGWRITER { public string Value; public override string ToString() { return string.Format("SONGWRITER: {0}", Value); } }
|
||||
public class TITLE { public string Value; public override string ToString() { return string.Format("TITLE: {0}", Value); } }
|
||||
public class TRACK { public int Number; public TrackType Type; public override string ToString() { return string.Format("TRACK {0,2} ({1})", Number, Type); } }
|
||||
str = line;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stuff other than the commands, global for the whole disc
|
||||
/// </summary>
|
||||
public class DiscInfo
|
||||
public string ReadPath() { return ReadToken(Mode.Quotable); }
|
||||
public string ReadToken() { return ReadToken(Mode.Normal); }
|
||||
public string ReadLine()
|
||||
{
|
||||
public Command.CATALOG Catalog;
|
||||
public Command.ISRC ISRC;
|
||||
public Command.CDTEXTFILE CDTextFile;
|
||||
int len = str.Length;
|
||||
string ret = str.Substring(index, len - index);
|
||||
index = len;
|
||||
EOF = true;
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The sequential list of commands parsed out of the cue file
|
||||
/// </summary>
|
||||
public List<object> Commands = new List<object>();
|
||||
|
||||
/// <summary>
|
||||
/// Stuff other than the commands, global for the whole disc
|
||||
/// </summary>
|
||||
public DiscInfo GlobalDiscInfo = new DiscInfo();
|
||||
|
||||
[Flags]
|
||||
public enum TrackFlags
|
||||
enum Mode
|
||||
{
|
||||
None = 0,
|
||||
PRE = 1, //Pre-emphasis enabled (audio tracks only)
|
||||
DCP = 2, //Digital copy permitted
|
||||
DATA = 4, //Set automatically by cue-processing equipment, here for completeness
|
||||
_4CH = 8, //Four channel audio
|
||||
SCMS = 64, //Serial copy management system (not supported by all recorders) (??)
|
||||
Normal, Quotable
|
||||
}
|
||||
|
||||
//All audio files (WAVE, AIFF, and MP3) must be in 44.1KHz 16-bit stereo format.
|
||||
//BUT NOTE: MP3 can be VBR and the length can't be known without decoding the whole thing.
|
||||
//But, some ideas:
|
||||
//1. we could operate ffmpeg differently to retrieve the length, which maybe it can do without having to decode the entire thing
|
||||
//2. we could retrieve it from an ID3 if present.
|
||||
//3. as a last resort, since MP3 is the annoying case usually, we could include my c# mp3 parser and sum the length (test the performance, this might be reasonably fast on par with ECM parsing)
|
||||
//NOTE: once deciding the length, we would have to stick with it! samples would have to be discarded or inserted to make the track work out
|
||||
//but we COULD effectively achieve stream-loading mp3 discs, with enough work.
|
||||
public enum FileType
|
||||
string ReadToken(Mode mode)
|
||||
{
|
||||
Unspecified,
|
||||
BINARY, //Intel binary file (least significant byte first)
|
||||
MOTOROLA, //Motorola binary file (most significant byte first)
|
||||
AIFF, //Audio AIFF file
|
||||
WAVE, //Audio WAVE file
|
||||
MP3, //Audio MP3 file
|
||||
}
|
||||
if (EOF) return null;
|
||||
|
||||
public enum TrackType
|
||||
{
|
||||
Unknown,
|
||||
Audio, //Audio/Music (2352)
|
||||
CDG, //Karaoke CD+G (2448)
|
||||
Mode1_2048, //CDROM Mode1 Data (cooked)
|
||||
Mode1_2352, //CDROM Mode1 Data (raw)
|
||||
Mode2_2336, //CDROM-XA Mode2 Data (could contain form 1 or form 2)
|
||||
Mode2_2352, //CDROM-XA Mode2 Data (but there's no reason to distinguish this from Mode1_2352 other than to alert us that the entire session should be XA
|
||||
CDI_2336, //CDI Mode2 Data
|
||||
CDI_2352 //CDI Mode2 Data
|
||||
}
|
||||
|
||||
class CueLineParser
|
||||
{
|
||||
int index;
|
||||
string str;
|
||||
public bool EOF;
|
||||
public CueLineParser(string line)
|
||||
{
|
||||
str = line;
|
||||
}
|
||||
|
||||
public string ReadPath() { return ReadToken(Mode.Quotable); }
|
||||
public string ReadToken() { return ReadToken(Mode.Normal); }
|
||||
public string ReadLine()
|
||||
{
|
||||
int len = str.Length;
|
||||
string ret = str.Substring(index, len - index);
|
||||
index = len;
|
||||
EOF = true;
|
||||
return ret;
|
||||
}
|
||||
|
||||
enum Mode
|
||||
{
|
||||
Normal, Quotable
|
||||
}
|
||||
|
||||
string ReadToken(Mode mode)
|
||||
{
|
||||
if (EOF) return null;
|
||||
|
||||
bool isPath = mode == Mode.Quotable;
|
||||
|
||||
int startIndex = index;
|
||||
bool inToken = false;
|
||||
bool inQuote = false;
|
||||
for (; ; )
|
||||
{
|
||||
bool done = false;
|
||||
char c = str[index];
|
||||
bool isWhiteSpace = (c == ' ' || c == '\t');
|
||||
|
||||
if (isWhiteSpace)
|
||||
{
|
||||
if (inQuote)
|
||||
index++;
|
||||
else
|
||||
{
|
||||
if (inToken)
|
||||
done = true;
|
||||
else
|
||||
index++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
bool startedQuote = false;
|
||||
if (!inToken)
|
||||
{
|
||||
startIndex = index;
|
||||
if (isPath && c == '"')
|
||||
startedQuote = inQuote = true;
|
||||
inToken = true;
|
||||
}
|
||||
switch (str[index])
|
||||
{
|
||||
case '"':
|
||||
index++;
|
||||
if (inQuote && !startedQuote)
|
||||
{
|
||||
done = true;
|
||||
}
|
||||
break;
|
||||
case '\\':
|
||||
index++;
|
||||
break;
|
||||
|
||||
default:
|
||||
index++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index == str.Length)
|
||||
{
|
||||
EOF = true;
|
||||
done = true;
|
||||
}
|
||||
if (done) break;
|
||||
}
|
||||
|
||||
string ret = str.Substring(startIndex, index - startIndex);
|
||||
|
||||
if (mode == Mode.Quotable)
|
||||
ret = ret.Trim('"');
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
internal void LoadFromString(ParseCueJob job)
|
||||
{
|
||||
string cueString = job.IN_CueString;
|
||||
TextReader tr = new StringReader(cueString);
|
||||
bool isPath = mode == Mode.Quotable;
|
||||
|
||||
int startIndex = index;
|
||||
bool inToken = false;
|
||||
bool inQuote = false;
|
||||
for (; ; )
|
||||
{
|
||||
job.CurrentLine++;
|
||||
string line = tr.ReadLine();
|
||||
if (line == null) break;
|
||||
line = line.Trim();
|
||||
if (line == "") continue;
|
||||
var clp = new CueLineParser(line);
|
||||
bool done = false;
|
||||
char c = str[index];
|
||||
bool isWhiteSpace = (c == ' ' || c == '\t');
|
||||
|
||||
string key = clp.ReadToken().ToUpperInvariant();
|
||||
if (key.StartsWith(";"))
|
||||
if (isWhiteSpace)
|
||||
{
|
||||
clp.EOF = true;
|
||||
Commands.Add(new Command.COMMENT() { Value = line });
|
||||
if (inQuote)
|
||||
index++;
|
||||
else
|
||||
{
|
||||
if (inToken)
|
||||
done = true;
|
||||
else
|
||||
index++;
|
||||
}
|
||||
}
|
||||
else switch (key)
|
||||
else
|
||||
{
|
||||
default:
|
||||
job.Warn("Unknown command: " + key);
|
||||
break;
|
||||
bool startedQuote = false;
|
||||
if (!inToken)
|
||||
{
|
||||
startIndex = index;
|
||||
if (isPath && c == '"')
|
||||
startedQuote = inQuote = true;
|
||||
inToken = true;
|
||||
}
|
||||
switch (str[index])
|
||||
{
|
||||
case '"':
|
||||
index++;
|
||||
if (inQuote && !startedQuote)
|
||||
{
|
||||
done = true;
|
||||
}
|
||||
break;
|
||||
case '\\':
|
||||
index++;
|
||||
break;
|
||||
|
||||
case "CATALOG":
|
||||
if (GlobalDiscInfo.Catalog != null)
|
||||
job.Warn("Multiple CATALOG commands detected. Subsequent ones are ignored.");
|
||||
else if (clp.EOF)
|
||||
job.Warn("Ignoring empty CATALOG command");
|
||||
else Commands.Add(GlobalDiscInfo.Catalog = new Command.CATALOG() { Value = clp.ReadToken() });
|
||||
break;
|
||||
default:
|
||||
index++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index == str.Length)
|
||||
{
|
||||
EOF = true;
|
||||
done = true;
|
||||
}
|
||||
if (done) break;
|
||||
}
|
||||
|
||||
case "CDTEXTFILE":
|
||||
if (GlobalDiscInfo.CDTextFile != null)
|
||||
job.Warn("Multiple CDTEXTFILE commands detected. Subsequent ones are ignored.");
|
||||
else if (clp.EOF)
|
||||
job.Warn("Ignoring empty CDTEXTFILE command");
|
||||
else Commands.Add(GlobalDiscInfo.CDTextFile = new Command.CDTEXTFILE() { Path = clp.ReadPath() });
|
||||
break;
|
||||
string ret = str.Substring(startIndex, index - startIndex);
|
||||
|
||||
case "FILE":
|
||||
if (mode == Mode.Quotable)
|
||||
ret = ret.Trim('"');
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
void LoadFromString(ParseCueJob job)
|
||||
{
|
||||
string cueString = job.IN_CueString;
|
||||
TextReader tr = new StringReader(cueString);
|
||||
|
||||
for (; ; )
|
||||
{
|
||||
job.CurrentLine++;
|
||||
string line = tr.ReadLine();
|
||||
if (line == null) break;
|
||||
line = line.Trim();
|
||||
if (line == "") continue;
|
||||
var clp = new CueLineParser(line);
|
||||
|
||||
string key = clp.ReadToken().ToUpperInvariant();
|
||||
if (key.StartsWith(";"))
|
||||
{
|
||||
clp.EOF = true;
|
||||
OUT_CueFile.Commands.Add(new CUE_File.Command.COMMENT() { Value = line });
|
||||
}
|
||||
else switch (key)
|
||||
{
|
||||
default:
|
||||
job.Warn("Unknown command: " + key);
|
||||
break;
|
||||
|
||||
case "CATALOG":
|
||||
if (OUT_CueFile.GlobalDiscInfo.Catalog != null)
|
||||
job.Warn("Multiple CATALOG commands detected. Subsequent ones are ignored.");
|
||||
else if (clp.EOF)
|
||||
job.Warn("Ignoring empty CATALOG command");
|
||||
else OUT_CueFile.Commands.Add(OUT_CueFile.GlobalDiscInfo.Catalog = new CUE_File.Command.CATALOG() { Value = clp.ReadToken() });
|
||||
break;
|
||||
|
||||
case "CDTEXTFILE":
|
||||
if (OUT_CueFile.GlobalDiscInfo.CDTextFile != null)
|
||||
job.Warn("Multiple CDTEXTFILE commands detected. Subsequent ones are ignored.");
|
||||
else if (clp.EOF)
|
||||
job.Warn("Ignoring empty CDTEXTFILE command");
|
||||
else OUT_CueFile.Commands.Add(OUT_CueFile.GlobalDiscInfo.CDTextFile = new CUE_File.Command.CDTEXTFILE() { Path = clp.ReadPath() });
|
||||
break;
|
||||
|
||||
case "FILE":
|
||||
{
|
||||
var path = clp.ReadPath();
|
||||
CueFileType ft;
|
||||
if (clp.EOF)
|
||||
{
|
||||
var path = clp.ReadPath();
|
||||
FileType ft;
|
||||
if (clp.EOF)
|
||||
{
|
||||
job.Error("FILE command is missing file type.");
|
||||
ft = FileType.Unspecified;
|
||||
}
|
||||
else
|
||||
{
|
||||
var strType = clp.ReadToken().ToUpperInvariant();
|
||||
switch (strType)
|
||||
{
|
||||
default:
|
||||
job.Error("Unknown FILE type: " + strType);
|
||||
ft = FileType.Unspecified;
|
||||
break;
|
||||
case "BINARY": ft = FileType.BINARY; break;
|
||||
case "MOTOROLA": ft = FileType.MOTOROLA; break;
|
||||
case "BINARAIFF": ft = FileType.AIFF; break;
|
||||
case "WAVE": ft = FileType.WAVE; break;
|
||||
case "MP3": ft = FileType.MP3; break;
|
||||
}
|
||||
}
|
||||
Commands.Add(new Command.FILE() { Path = path, Type = ft });
|
||||
job.Error("FILE command is missing file type.");
|
||||
ft = CueFileType.Unspecified;
|
||||
}
|
||||
break;
|
||||
|
||||
case "FLAGS":
|
||||
{
|
||||
var cmd = new Command.FLAGS();
|
||||
Commands.Add(cmd);
|
||||
while (!clp.EOF)
|
||||
{
|
||||
var flag = clp.ReadToken().ToUpperInvariant();
|
||||
switch (flag)
|
||||
{
|
||||
case "DATA":
|
||||
default:
|
||||
job.Warn("Unknown FLAG: " + flag);
|
||||
break;
|
||||
case "DCP": cmd.Flags |= TrackFlags.DCP; break;
|
||||
case "4CH": cmd.Flags |= TrackFlags._4CH; break;
|
||||
case "PRE": cmd.Flags |= TrackFlags.PRE; break;
|
||||
case "SCMS": cmd.Flags |= TrackFlags.SCMS; break;
|
||||
}
|
||||
}
|
||||
if (cmd.Flags == TrackFlags.None)
|
||||
job.Warn("Empty FLAG command");
|
||||
}
|
||||
break;
|
||||
|
||||
case "INDEX":
|
||||
{
|
||||
if (clp.EOF)
|
||||
{
|
||||
job.Error("Incomplete INDEX command");
|
||||
break;
|
||||
}
|
||||
string strindexnum = clp.ReadToken();
|
||||
int indexnum;
|
||||
if (!int.TryParse(strindexnum, out indexnum) || indexnum < 0 || indexnum > 99)
|
||||
{
|
||||
job.Error("Invalid INDEX number: " + strindexnum);
|
||||
break;
|
||||
}
|
||||
string str_timestamp = clp.ReadToken();
|
||||
var ts = new Timestamp(str_timestamp);
|
||||
if (!ts.Valid)
|
||||
{
|
||||
job.Error("Invalid INDEX timestamp: " + str_timestamp);
|
||||
break;
|
||||
}
|
||||
Commands.Add(new Command.INDEX() { Number = indexnum, Timestamp = ts });
|
||||
}
|
||||
break;
|
||||
|
||||
case "ISRC":
|
||||
if (GlobalDiscInfo.ISRC != null)
|
||||
job.Warn("Multiple ISRC commands detected. Subsequent ones are ignored.");
|
||||
else if (clp.EOF)
|
||||
job.Warn("Ignoring empty ISRC command");
|
||||
else
|
||||
{
|
||||
var isrc = clp.ReadToken();
|
||||
if (isrc.Length != 12)
|
||||
job.Warn("Invalid ISRC code ignored: " + isrc);
|
||||
else
|
||||
{
|
||||
Commands.Add(new Command.ISRC() { Value = isrc });
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "PERFORMER":
|
||||
Commands.Add(new Command.PERFORMER() { Value = clp.ReadPath() ?? "" });
|
||||
break;
|
||||
|
||||
case "POSTGAP":
|
||||
case "PREGAP":
|
||||
{
|
||||
var str_msf = clp.ReadToken();
|
||||
var msf = new Timestamp(str_msf);
|
||||
if (!msf.Valid)
|
||||
job.Error("Ignoring {0} with invalid length MSF: " + str_msf, key);
|
||||
else
|
||||
{
|
||||
if (key == "POSTGAP")
|
||||
Commands.Add(new Command.POSTGAP() { Length = msf });
|
||||
else
|
||||
Commands.Add(new Command.PREGAP() { Length = msf });
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "REM":
|
||||
Commands.Add(new Command.REM() { Value = clp.ReadLine() });
|
||||
break;
|
||||
|
||||
case "SONGWRITER":
|
||||
Commands.Add(new Command.SONGWRITER() { Value = clp.ReadPath() ?? "" });
|
||||
break;
|
||||
|
||||
case "TITLE":
|
||||
Commands.Add(new Command.TITLE() { Value = clp.ReadPath() ?? "" });
|
||||
break;
|
||||
|
||||
case "TRACK":
|
||||
{
|
||||
if (clp.EOF)
|
||||
{
|
||||
job.Error("Incomplete TRACK command");
|
||||
break;
|
||||
}
|
||||
string str_tracknum = clp.ReadToken();
|
||||
int tracknum;
|
||||
if (!int.TryParse(str_tracknum, out tracknum) || tracknum < 1 || tracknum > 99)
|
||||
{
|
||||
job.Error("Invalid TRACK number: " + str_tracknum);
|
||||
break;
|
||||
}
|
||||
|
||||
//TODO - check sequentiality? maybe as a warning
|
||||
|
||||
TrackType tt;
|
||||
var str_trackType = clp.ReadToken();
|
||||
switch (str_trackType.ToUpperInvariant())
|
||||
var strType = clp.ReadToken().ToUpperInvariant();
|
||||
switch (strType)
|
||||
{
|
||||
default:
|
||||
job.Error("Unknown TRACK type: " + str_trackType);
|
||||
tt = TrackType.Unknown;
|
||||
job.Error("Unknown FILE type: " + strType);
|
||||
ft = CueFileType.Unspecified;
|
||||
break;
|
||||
case "AUDIO": tt = TrackType.Audio; break;
|
||||
case "CDG": tt = TrackType.CDG; break;
|
||||
case "MODE1/2048": tt = TrackType.Mode1_2048; break;
|
||||
case "MODE1/2352": tt = TrackType.Mode1_2352; break;
|
||||
case "MODE2/2336": tt = TrackType.Mode2_2336; break;
|
||||
case "MODE2/2352": tt = TrackType.Mode2_2352; break;
|
||||
case "CDI/2336": tt = TrackType.CDI_2336; break;
|
||||
case "CDI/2352": tt = TrackType.CDI_2352; break;
|
||||
case "BINARY": ft = CueFileType.BINARY; break;
|
||||
case "MOTOROLA": ft = CueFileType.MOTOROLA; break;
|
||||
case "BINARAIFF": ft = CueFileType.AIFF; break;
|
||||
case "WAVE": ft = CueFileType.WAVE; break;
|
||||
case "MP3": ft = CueFileType.MP3; break;
|
||||
}
|
||||
|
||||
Commands.Add(new Command.TRACK() { Number = tracknum, Type = tt });
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (!clp.EOF)
|
||||
{
|
||||
var remainder = clp.ReadLine();
|
||||
if (remainder.TrimStart().StartsWith(";"))
|
||||
{
|
||||
//add a comment
|
||||
Commands.Add(new Command.COMMENT() { Value = remainder });
|
||||
OUT_CueFile.Commands.Add(new CUE_File.Command.FILE() { Path = path, Type = ft });
|
||||
}
|
||||
else job.Warn("Unknown text at end of line after processing command: " + key);
|
||||
break;
|
||||
|
||||
case "FLAGS":
|
||||
{
|
||||
var cmd = new CUE_File.Command.FLAGS();
|
||||
OUT_CueFile.Commands.Add(cmd);
|
||||
while (!clp.EOF)
|
||||
{
|
||||
var flag = clp.ReadToken().ToUpperInvariant();
|
||||
switch (flag)
|
||||
{
|
||||
case "DATA":
|
||||
default:
|
||||
job.Warn("Unknown FLAG: " + flag);
|
||||
break;
|
||||
case "DCP": cmd.Flags |= CueTrackFlags.DCP; break;
|
||||
case "4CH": cmd.Flags |= CueTrackFlags._4CH; break;
|
||||
case "PRE": cmd.Flags |= CueTrackFlags.PRE; break;
|
||||
case "SCMS": cmd.Flags |= CueTrackFlags.SCMS; break;
|
||||
}
|
||||
}
|
||||
if (cmd.Flags == CueTrackFlags.None)
|
||||
job.Warn("Empty FLAG command");
|
||||
}
|
||||
break;
|
||||
|
||||
case "INDEX":
|
||||
{
|
||||
if (clp.EOF)
|
||||
{
|
||||
job.Error("Incomplete INDEX command");
|
||||
break;
|
||||
}
|
||||
string strindexnum = clp.ReadToken();
|
||||
int indexnum;
|
||||
if (!int.TryParse(strindexnum, out indexnum) || indexnum < 0 || indexnum > 99)
|
||||
{
|
||||
job.Error("Invalid INDEX number: " + strindexnum);
|
||||
break;
|
||||
}
|
||||
string str_timestamp = clp.ReadToken();
|
||||
var ts = new Timestamp(str_timestamp);
|
||||
if (!ts.Valid)
|
||||
{
|
||||
job.Error("Invalid INDEX timestamp: " + str_timestamp);
|
||||
break;
|
||||
}
|
||||
OUT_CueFile.Commands.Add(new CUE_File.Command.INDEX() { Number = indexnum, Timestamp = ts });
|
||||
}
|
||||
break;
|
||||
|
||||
case "ISRC":
|
||||
if (OUT_CueFile.GlobalDiscInfo.ISRC != null)
|
||||
job.Warn("Multiple ISRC commands detected. Subsequent ones are ignored.");
|
||||
else if (clp.EOF)
|
||||
job.Warn("Ignoring empty ISRC command");
|
||||
else
|
||||
{
|
||||
var isrc = clp.ReadToken();
|
||||
if (isrc.Length != 12)
|
||||
job.Warn("Invalid ISRC code ignored: " + isrc);
|
||||
else
|
||||
{
|
||||
OUT_CueFile.Commands.Add(OUT_CueFile.GlobalDiscInfo.ISRC = new CUE_File.Command.ISRC() { Value = isrc });
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "PERFORMER":
|
||||
OUT_CueFile.Commands.Add(new CUE_File.Command.PERFORMER() { Value = clp.ReadPath() ?? "" });
|
||||
break;
|
||||
|
||||
case "POSTGAP":
|
||||
case "PREGAP":
|
||||
{
|
||||
var str_msf = clp.ReadToken();
|
||||
var msf = new Timestamp(str_msf);
|
||||
if (!msf.Valid)
|
||||
job.Error("Ignoring {0} with invalid length MSF: " + str_msf, key);
|
||||
else
|
||||
{
|
||||
if (key == "POSTGAP")
|
||||
OUT_CueFile.Commands.Add(new CUE_File.Command.POSTGAP() { Length = msf });
|
||||
else
|
||||
OUT_CueFile.Commands.Add(new CUE_File.Command.PREGAP() { Length = msf });
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "REM":
|
||||
OUT_CueFile.Commands.Add(new CUE_File.Command.REM() { Value = clp.ReadLine() });
|
||||
break;
|
||||
|
||||
case "SONGWRITER":
|
||||
OUT_CueFile.Commands.Add(new CUE_File.Command.SONGWRITER() { Value = clp.ReadPath() ?? "" });
|
||||
break;
|
||||
|
||||
case "TITLE":
|
||||
OUT_CueFile.Commands.Add(new CUE_File.Command.TITLE() { Value = clp.ReadPath() ?? "" });
|
||||
break;
|
||||
|
||||
case "TRACK":
|
||||
{
|
||||
if (clp.EOF)
|
||||
{
|
||||
job.Error("Incomplete TRACK command");
|
||||
break;
|
||||
}
|
||||
string str_tracknum = clp.ReadToken();
|
||||
int tracknum;
|
||||
if (!int.TryParse(str_tracknum, out tracknum) || tracknum < 1 || tracknum > 99)
|
||||
{
|
||||
job.Error("Invalid TRACK number: " + str_tracknum);
|
||||
break;
|
||||
}
|
||||
|
||||
//TODO - check sequentiality? maybe as a warning
|
||||
|
||||
CueTrackType tt;
|
||||
var str_trackType = clp.ReadToken();
|
||||
switch (str_trackType.ToUpperInvariant())
|
||||
{
|
||||
default:
|
||||
job.Error("Unknown TRACK type: " + str_trackType);
|
||||
tt = CueTrackType.Unknown;
|
||||
break;
|
||||
case "AUDIO": tt = CueTrackType.Audio; break;
|
||||
case "CDG": tt = CueTrackType.CDG; break;
|
||||
case "MODE1/2048": tt = CueTrackType.Mode1_2048; break;
|
||||
case "MODE1/2352": tt = CueTrackType.Mode1_2352; break;
|
||||
case "MODE2/2336": tt = CueTrackType.Mode2_2336; break;
|
||||
case "MODE2/2352": tt = CueTrackType.Mode2_2352; break;
|
||||
case "CDI/2336": tt = CueTrackType.CDI_2336; break;
|
||||
case "CDI/2352": tt = CueTrackType.CDI_2352; break;
|
||||
}
|
||||
|
||||
OUT_CueFile.Commands.Add(new CUE_File.Command.TRACK() { Number = tracknum, Type = tt });
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (!clp.EOF)
|
||||
{
|
||||
var remainder = clp.ReadLine();
|
||||
if (remainder.TrimStart().StartsWith(";"))
|
||||
{
|
||||
//add a comment
|
||||
OUT_CueFile.Commands.Add(new CUE_File.Command.COMMENT() { Value = remainder });
|
||||
}
|
||||
else job.Warn("Unknown text at end of line after processing command: " + key);
|
||||
}
|
||||
|
||||
} //end cue parsing loop
|
||||
} //end cue parsing loop
|
||||
|
||||
job.FinishLog();
|
||||
} //LoadFromString
|
||||
}
|
||||
job.FinishLog();
|
||||
} //LoadFromString
|
||||
|
||||
/// <summary>
|
||||
/// Performs minimum parse processing on a cue file
|
||||
/// </summary>
|
||||
public void ParseCueFile(ParseCueJob job)
|
||||
public void Run(ParseCueJob job)
|
||||
{
|
||||
job.OUT_CueFile = new CueFile();
|
||||
job.OUT_CueFile.LoadFromString(job);
|
||||
job.OUT_CueFile = new CUE_File();
|
||||
job.LoadFromString(job);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
} //partial class
|
||||
} //namespace
|
|
@ -2,155 +2,154 @@ using System;
|
|||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BizHawk.Emulation.DiscSystem
|
||||
namespace BizHawk.Emulation.DiscSystem.CUE
|
||||
{
|
||||
partial class CUE_Context
|
||||
internal abstract class SS_Base : ISectorSynthJob2448
|
||||
{
|
||||
public IBlob Blob;
|
||||
public long BlobOffset;
|
||||
|
||||
abstract class SS_Base : ISectorSynthJob2448
|
||||
public DiscMountPolicy Policy;
|
||||
|
||||
//subQ data
|
||||
public SubchannelQ sq;
|
||||
|
||||
//subP data
|
||||
public bool Pause;
|
||||
|
||||
public abstract void Synth(SectorSynthJob job);
|
||||
|
||||
protected void SynthSubchannelAsNeed(SectorSynthJob job)
|
||||
{
|
||||
public IBlob Blob;
|
||||
public long BlobOffset;
|
||||
|
||||
public DiscMountPolicy Policy;
|
||||
|
||||
//subQ data
|
||||
public SubchannelQ sq;
|
||||
|
||||
//subP data
|
||||
public bool Pause;
|
||||
|
||||
public abstract void Synth(SectorSynthJob job);
|
||||
|
||||
protected void SynthSubchannelAsNeed(SectorSynthJob job)
|
||||
//synth P if needed
|
||||
if ((job.Parts & ESectorSynthPart.SubchannelP) != 0)
|
||||
{
|
||||
//synth P if needed
|
||||
if ((job.Parts & ESectorSynthPart.SubchannelP) != 0)
|
||||
{
|
||||
SynthUtils.SubP(job.DestBuffer2448, job.DestOffset + 2352, Pause);
|
||||
}
|
||||
|
||||
//synth Q if needed
|
||||
//TODO - why not already have it serialized? Into a disc resource, even.
|
||||
if ((job.Parts & ESectorSynthPart.SubchannelQ) != 0)
|
||||
{
|
||||
SynthUtils.SubQ_Serialize(job.DestBuffer2448, job.DestOffset + 2352 + 12, ref sq);
|
||||
}
|
||||
|
||||
//clear R-W if needed
|
||||
if ((job.Parts & ESectorSynthPart.Subchannel_RSTUVW) != 0)
|
||||
{
|
||||
Array.Clear(job.DestBuffer2448, job.DestOffset + 2352 + 12 + 12, (12 * 6));
|
||||
}
|
||||
|
||||
//subcode has been generated deinterleaved; we may still need to interleave it
|
||||
if((job.Parts & ESectorSynthPart.SubcodeAny) != 0)
|
||||
if ((job.Parts & (ESectorSynthPart.SubcodeDeinterleave)) == 0)
|
||||
{
|
||||
SynthUtils.InterleaveSubcodeInplace(job.DestBuffer2448, job.DestOffset + 2352);
|
||||
}
|
||||
SynthUtils.SubP(job.DestBuffer2448, job.DestOffset + 2352, Pause);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a Mode1 2048-byte sector
|
||||
/// </summary>
|
||||
class SS_Mode1_2048 : SS_Base
|
||||
{
|
||||
public override void Synth(SectorSynthJob job)
|
||||
//synth Q if needed
|
||||
//TODO - why not already have it serialized? Into a disc resource, even.
|
||||
if ((job.Parts & ESectorSynthPart.SubchannelQ) != 0)
|
||||
{
|
||||
//read the sector user data
|
||||
if((job.Parts & ESectorSynthPart.User2048) != 0)
|
||||
Blob.Read(BlobOffset, job.DestBuffer2448, job.DestOffset + 16, 2048);
|
||||
SynthUtils.SubQ_Serialize(job.DestBuffer2448, job.DestOffset + 2352 + 12, ref sq);
|
||||
}
|
||||
|
||||
//clear R-W if needed
|
||||
if ((job.Parts & ESectorSynthPart.Subchannel_RSTUVW) != 0)
|
||||
{
|
||||
Array.Clear(job.DestBuffer2448, job.DestOffset + 2352 + 12 + 12, (12 * 6));
|
||||
}
|
||||
|
||||
//subcode has been generated deinterleaved; we may still need to interleave it
|
||||
if((job.Parts & ESectorSynthPart.SubcodeAny) != 0)
|
||||
if ((job.Parts & (ESectorSynthPart.SubcodeDeinterleave)) == 0)
|
||||
{
|
||||
SynthUtils.InterleaveSubcodeInplace(job.DestBuffer2448, job.DestOffset + 2352);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a Mode1 2048-byte sector
|
||||
/// </summary>
|
||||
class SS_Mode1_2048 : SS_Base
|
||||
{
|
||||
public override void Synth(SectorSynthJob job)
|
||||
{
|
||||
//read the sector user data
|
||||
if((job.Parts & ESectorSynthPart.User2048) != 0)
|
||||
Blob.Read(BlobOffset, job.DestBuffer2448, job.DestOffset + 16, 2048);
|
||||
|
||||
if ((job.Parts & ESectorSynthPart.Header16) != 0)
|
||||
SynthUtils.SectorHeader(job.DestBuffer2448, job.DestOffset + 0, job.LBA, 1);
|
||||
|
||||
if ((job.Parts & ESectorSynthPart.ECMAny) != 0)
|
||||
SynthUtils.ECM_Mode1(job.DestBuffer2448, job.DestOffset + 0, job.LBA);
|
||||
|
||||
SynthSubchannelAsNeed(job);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a 2352-byte sector of any sort
|
||||
/// </summary>
|
||||
class SS_2352 : SS_Base
|
||||
{
|
||||
public override void Synth(SectorSynthJob job)
|
||||
{
|
||||
//read the sector user data
|
||||
Blob.Read(BlobOffset, job.DestBuffer2448, job.DestOffset, 2352);
|
||||
|
||||
//if subcode is needed, synthesize it
|
||||
SynthSubchannelAsNeed(job);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes a pre-gap sector
|
||||
/// </summary>
|
||||
class SS_Gap : SS_Base
|
||||
{
|
||||
public CueTrackType TrackType;
|
||||
|
||||
public override void Synth(SectorSynthJob job)
|
||||
{
|
||||
//this isn't fully analyzed/optimized
|
||||
Array.Clear(job.DestBuffer2448, job.DestOffset, 2352);
|
||||
|
||||
byte mode = 255;
|
||||
int form = -1;
|
||||
switch (TrackType)
|
||||
{
|
||||
case CueTrackType.Audio:
|
||||
mode = 0;
|
||||
break;
|
||||
|
||||
case CueTrackType.CDI_2352:
|
||||
case CueTrackType.Mode1_2352:
|
||||
mode = 1;
|
||||
break;
|
||||
|
||||
case CueTrackType.Mode2_2352:
|
||||
mode = 2;
|
||||
if (Policy.CUE_PregapMode2_As_XA_Form2)
|
||||
{
|
||||
job.DestBuffer2448[job.DestOffset + 12 + 6] = 0x20;
|
||||
job.DestBuffer2448[job.DestOffset + 12 + 10] = 0x20;
|
||||
}
|
||||
form = 2; //no other choice right now really
|
||||
break;
|
||||
|
||||
case CueTrackType.Mode1_2048:
|
||||
mode = 1;
|
||||
Pause = true;
|
||||
break;
|
||||
|
||||
case CueTrackType.Mode2_2336:
|
||||
default:
|
||||
throw new InvalidOperationException("Not supported: " + TrackType);
|
||||
}
|
||||
|
||||
//audio has no sector header but the others do
|
||||
if (mode != 0)
|
||||
{
|
||||
if ((job.Parts & ESectorSynthPart.Header16) != 0)
|
||||
SynthUtils.SectorHeader(job.DestBuffer2448, job.DestOffset + 0, job.LBA, 1);
|
||||
SynthUtils.SectorHeader(job.DestBuffer2448, job.DestOffset + 0, job.LBA, mode);
|
||||
}
|
||||
|
||||
if (mode == 1)
|
||||
{
|
||||
if ((job.Parts & ESectorSynthPart.ECMAny) != 0)
|
||||
SynthUtils.ECM_Mode1(job.DestBuffer2448, job.DestOffset + 0, job.LBA);
|
||||
|
||||
SynthSubchannelAsNeed(job);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a 2352-byte sector of any sort
|
||||
/// </summary>
|
||||
class SS_2352 : SS_Base
|
||||
{
|
||||
public override void Synth(SectorSynthJob job)
|
||||
if (mode == 2 && form == 2)
|
||||
{
|
||||
//read the sector user data
|
||||
Blob.Read(BlobOffset, job.DestBuffer2448, job.DestOffset, 2352);
|
||||
|
||||
//if subcode is needed, synthesize it
|
||||
SynthSubchannelAsNeed(job);
|
||||
SynthUtils.EDC_Mode2_Form2(job.DestBuffer2448, job.DestOffset);
|
||||
}
|
||||
|
||||
SynthSubchannelAsNeed(job);
|
||||
}
|
||||
|
||||
class SS_Gap : SS_Base
|
||||
{
|
||||
public CueFile.TrackType TrackType;
|
||||
|
||||
public override void Synth(SectorSynthJob job)
|
||||
{
|
||||
//this isn't fully analyzed/optimized
|
||||
Array.Clear(job.DestBuffer2448, job.DestOffset, 2352);
|
||||
|
||||
byte mode = 255;
|
||||
int form = -1;
|
||||
switch (TrackType)
|
||||
{
|
||||
case CueFile.TrackType.Audio:
|
||||
mode = 0;
|
||||
break;
|
||||
|
||||
case CueFile.TrackType.CDI_2352:
|
||||
case CueFile.TrackType.Mode1_2352:
|
||||
mode = 1;
|
||||
break;
|
||||
|
||||
case CueFile.TrackType.Mode2_2352:
|
||||
mode = 2;
|
||||
if (Policy.CUE_PregapMode2_As_XA_Form2)
|
||||
{
|
||||
job.DestBuffer2448[job.DestOffset + 12 + 6] = 0x20;
|
||||
job.DestBuffer2448[job.DestOffset + 12 + 10] = 0x20;
|
||||
}
|
||||
form = 2; //no other choice right now really
|
||||
break;
|
||||
|
||||
case CueFile.TrackType.Mode1_2048:
|
||||
mode = 1;
|
||||
Pause = true;
|
||||
break;
|
||||
|
||||
case CueFile.TrackType.Mode2_2336:
|
||||
default:
|
||||
throw new InvalidOperationException("Not supported: " + TrackType);
|
||||
}
|
||||
|
||||
//audio has no sector header but the others do
|
||||
if (mode != 0)
|
||||
{
|
||||
if ((job.Parts & ESectorSynthPart.Header16) != 0)
|
||||
SynthUtils.SectorHeader(job.DestBuffer2448, job.DestOffset + 0, job.LBA, mode);
|
||||
}
|
||||
|
||||
if (mode == 1)
|
||||
{
|
||||
if ((job.Parts & ESectorSynthPart.ECMAny) != 0)
|
||||
SynthUtils.ECM_Mode1(job.DestBuffer2448, job.DestOffset + 0, job.LBA);
|
||||
}
|
||||
if (mode == 2 && form == 2)
|
||||
{
|
||||
SynthUtils.EDC_Mode2_Form2(job.DestBuffer2448, job.DestOffset);
|
||||
}
|
||||
|
||||
SynthSubchannelAsNeed(job);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
using System;
|
||||
|
||||
namespace BizHawk.Emulation.DiscSystem.CUE
|
||||
{
|
||||
[Flags]
|
||||
public enum CueTrackFlags
|
||||
{
|
||||
None = 0,
|
||||
PRE = 1, //Pre-emphasis enabled (audio tracks only)
|
||||
DCP = 2, //Digital copy permitted
|
||||
DATA = 4, //Set automatically by cue-processing equipment, here for completeness
|
||||
_4CH = 8, //Four channel audio
|
||||
SCMS = 64, //Serial copy management system (not supported by all recorders) (??)
|
||||
}
|
||||
|
||||
//All audio files (WAVE, AIFF, and MP3) must be in 44.1KHz 16-bit stereo format.
|
||||
//BUT NOTE: MP3 can be VBR and the length can't be known without decoding the whole thing.
|
||||
//But, some ideas:
|
||||
//1. we could operate ffmpeg differently to retrieve the length, which maybe it can do without having to decode the entire thing
|
||||
//2. we could retrieve it from an ID3 if present.
|
||||
//3. as a last resort, since MP3 is the annoying case usually, we could include my c# mp3 parser and sum the length (test the performance, this might be reasonably fast on par with ECM parsing)
|
||||
//NOTE: once deciding the length, we would have to stick with it! samples would have to be discarded or inserted to make the track work out
|
||||
//but we COULD effectively achieve stream-loading mp3 discs, with enough work.
|
||||
public enum CueFileType
|
||||
{
|
||||
Unspecified,
|
||||
BINARY, //Intel binary file (least significant byte first)
|
||||
MOTOROLA, //Motorola binary file (most significant byte first)
|
||||
AIFF, //Audio AIFF file
|
||||
WAVE, //Audio WAVE file
|
||||
MP3, //Audio MP3 file
|
||||
}
|
||||
|
||||
public enum CueTrackType
|
||||
{
|
||||
Unknown,
|
||||
Audio, //Audio/Music (2352)
|
||||
CDG, //Karaoke CD+G (2448)
|
||||
Mode1_2048, //CDROM Mode1 Data (cooked)
|
||||
Mode1_2352, //CDROM Mode1 Data (raw)
|
||||
Mode2_2336, //CDROM-XA Mode2 Data (could contain form 1 or form 2)
|
||||
Mode2_2352, //CDROM-XA Mode2 Data (but there's no reason to distinguish this from Mode1_2352 other than to alert us that the entire session should be XA
|
||||
CDI_2336, //CDI Mode2 Data
|
||||
CDI_2352 //CDI Mode2 Data
|
||||
}
|
||||
}
|
|
@ -4,121 +4,118 @@ using System.Text;
|
|||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BizHawk.Emulation.DiscSystem
|
||||
namespace BizHawk.Emulation.DiscSystem.CUE
|
||||
{
|
||||
partial class CUE_Context
|
||||
/// <summary>
|
||||
/// The CUE module user's hook for controlling how cue member file paths get resolved
|
||||
/// </summary>
|
||||
public class CueFileResolver
|
||||
{
|
||||
public bool caseSensitive = false;
|
||||
public bool IsHardcodedResolve { get; private set; }
|
||||
string baseDir;
|
||||
|
||||
/// <summary>
|
||||
/// The CUE module user's hook for controlling how cue member file paths get resolved
|
||||
/// Retrieving the FullName from a FileInfo can be slow (and probably other operations), so this will cache all the needed values
|
||||
/// TODO - could we treat it like an actual cache and only fill the FullName if it's null?
|
||||
/// </summary>
|
||||
public class CueFileResolver
|
||||
struct MyFileInfo
|
||||
{
|
||||
public bool caseSensitive = false;
|
||||
public bool IsHardcodedResolve { get; private set; }
|
||||
string baseDir;
|
||||
public string FullName;
|
||||
public FileInfo FileInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieving the FullName from a FileInfo can be slow (and probably other operations), so this will cache all the needed values
|
||||
/// TODO - could we treat it like an actual cache and only fill the FullName if it's null?
|
||||
/// </summary>
|
||||
struct MyFileInfo
|
||||
DirectoryInfo diBasedir;
|
||||
MyFileInfo[] fisBaseDir;
|
||||
|
||||
/// <summary>
|
||||
/// sets the base directory and caches the list of files in the directory
|
||||
/// </summary>
|
||||
public void SetBaseDirectory(string baseDir)
|
||||
{
|
||||
this.baseDir = baseDir;
|
||||
diBasedir = new DirectoryInfo(baseDir);
|
||||
//list all files, so we dont scan repeatedly.
|
||||
fisBaseDir = MyFileInfosFromFileInfos(diBasedir.GetFiles());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TODO - doesnt seem like we're using this...
|
||||
/// </summary>
|
||||
public void SetHardcodeResolve(IDictionary<string, string> hardcodes)
|
||||
{
|
||||
IsHardcodedResolve = true;
|
||||
fisBaseDir = new MyFileInfo[hardcodes.Count];
|
||||
int i = 0;
|
||||
foreach (var kvp in hardcodes)
|
||||
{
|
||||
public string FullName;
|
||||
public FileInfo FileInfo;
|
||||
fisBaseDir[i++] = new MyFileInfo { FullName = kvp.Key, FileInfo = new FileInfo(kvp.Value) };
|
||||
}
|
||||
}
|
||||
|
||||
MyFileInfo[] MyFileInfosFromFileInfos(FileInfo[] fis)
|
||||
{
|
||||
var myfis = new MyFileInfo[fis.Length];
|
||||
for (int i = 0; i < fis.Length; i++)
|
||||
{
|
||||
myfis[i].FileInfo = fis[i];
|
||||
myfis[i].FullName = fis[i].FullName;
|
||||
}
|
||||
return myfis;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs cue-intelligent logic to acquire a file requested by the cue.
|
||||
/// Returns the resulting full path(s).
|
||||
/// If there are multiple options, it returns them all
|
||||
/// </summary>
|
||||
public List<string> Resolve(string path)
|
||||
{
|
||||
string targetFile = Path.GetFileName(path);
|
||||
string targetFragment = Path.GetFileNameWithoutExtension(path);
|
||||
|
||||
DirectoryInfo di = null;
|
||||
MyFileInfo[] fileInfos;
|
||||
if (!string.IsNullOrEmpty(Path.GetDirectoryName(path)))
|
||||
{
|
||||
di = new FileInfo(path).Directory;
|
||||
//fileInfos = di.GetFiles(Path.GetFileNameWithoutExtension(path)); //does this work?
|
||||
fileInfos = MyFileInfosFromFileInfos(di.GetFiles()); //we (probably) have to enumerate all the files to do a search anyway, so might as well do this
|
||||
//TODO - dont do the search until a resolve fails
|
||||
}
|
||||
else
|
||||
{
|
||||
di = diBasedir;
|
||||
fileInfos = fisBaseDir;
|
||||
}
|
||||
|
||||
DirectoryInfo diBasedir;
|
||||
MyFileInfo[] fisBaseDir;
|
||||
|
||||
/// <summary>
|
||||
/// sets the base directory and caches the list of files in the directory
|
||||
/// </summary>
|
||||
public void SetBaseDirectory(string baseDir)
|
||||
var results = new List<FileInfo>();
|
||||
foreach (var fi in fileInfos)
|
||||
{
|
||||
this.baseDir = baseDir;
|
||||
diBasedir = new DirectoryInfo(baseDir);
|
||||
//list all files, so we dont scan repeatedly.
|
||||
fisBaseDir = MyFileInfosFromFileInfos(diBasedir.GetFiles());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TODO - doesnt seem like we're using this...
|
||||
/// </summary>
|
||||
public void SetHardcodeResolve(IDictionary<string, string> hardcodes)
|
||||
{
|
||||
IsHardcodedResolve = true;
|
||||
fisBaseDir = new MyFileInfo[hardcodes.Count];
|
||||
int i = 0;
|
||||
foreach (var kvp in hardcodes)
|
||||
{
|
||||
fisBaseDir[i++] = new MyFileInfo { FullName = kvp.Key, FileInfo = new FileInfo(kvp.Value) };
|
||||
}
|
||||
}
|
||||
|
||||
MyFileInfo[] MyFileInfosFromFileInfos(FileInfo[] fis)
|
||||
{
|
||||
var myfis = new MyFileInfo[fis.Length];
|
||||
for (int i = 0; i < fis.Length; i++)
|
||||
{
|
||||
myfis[i].FileInfo = fis[i];
|
||||
myfis[i].FullName = fis[i].FullName;
|
||||
}
|
||||
return myfis;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs cue-intelligent logic to acquire a file requested by the cue.
|
||||
/// Returns the resulting full path(s).
|
||||
/// If there are multiple options, it returns them all
|
||||
/// </summary>
|
||||
public List<string> Resolve(string path)
|
||||
{
|
||||
string targetFile = Path.GetFileName(path);
|
||||
string targetFragment = Path.GetFileNameWithoutExtension(path);
|
||||
|
||||
DirectoryInfo di = null;
|
||||
MyFileInfo[] fileInfos;
|
||||
if (!string.IsNullOrEmpty(Path.GetDirectoryName(path)))
|
||||
{
|
||||
di = new FileInfo(path).Directory;
|
||||
//fileInfos = di.GetFiles(Path.GetFileNameWithoutExtension(path)); //does this work?
|
||||
fileInfos = MyFileInfosFromFileInfos(di.GetFiles()); //we (probably) have to enumerate all the files to do a search anyway, so might as well do this
|
||||
//TODO - dont do the search until a resolve fails
|
||||
}
|
||||
else
|
||||
{
|
||||
di = diBasedir;
|
||||
fileInfos = fisBaseDir;
|
||||
}
|
||||
|
||||
var results = new List<FileInfo>();
|
||||
foreach (var fi in fileInfos)
|
||||
{
|
||||
var ext = Path.GetExtension(fi.FullName).ToLowerInvariant();
|
||||
|
||||
//some choices are always bad: (we're looking for things like .bin and .wav)
|
||||
//it's a little unclear whether we should go for a whitelist or a blacklist here.
|
||||
//there's similar numbers of cases either way.
|
||||
//perhaps we could code both (and prefer choices from the whitelist)
|
||||
if (ext == ".cue" || ext == ".sbi" || ext == ".ccd" || ext == ".sub")
|
||||
continue;
|
||||
|
||||
|
||||
string fragment = Path.GetFileNameWithoutExtension(fi.FullName);
|
||||
//match files with differing extensions
|
||||
int cmp = string.Compare(fragment, targetFragment, !caseSensitive);
|
||||
if (cmp != 0)
|
||||
//match files with another extension added on (likely to be mygame.bin.ecm)
|
||||
cmp = string.Compare(fragment, targetFile, !caseSensitive);
|
||||
if (cmp == 0)
|
||||
results.Add(fi.FileInfo);
|
||||
|
||||
}
|
||||
var ret = new List<string>();
|
||||
foreach (var fi in results)
|
||||
ret.Add(fi.FullName);
|
||||
return ret;
|
||||
var ext = Path.GetExtension(fi.FullName).ToLowerInvariant();
|
||||
|
||||
//some choices are always bad: (we're looking for things like .bin and .wav)
|
||||
//it's a little unclear whether we should go for a whitelist or a blacklist here.
|
||||
//there's similar numbers of cases either way.
|
||||
//perhaps we could code both (and prefer choices from the whitelist)
|
||||
if (ext == ".cue" || ext == ".sbi" || ext == ".ccd" || ext == ".sub")
|
||||
continue;
|
||||
|
||||
|
||||
string fragment = Path.GetFileNameWithoutExtension(fi.FullName);
|
||||
//match files with differing extensions
|
||||
int cmp = string.Compare(fragment, targetFragment, !caseSensitive);
|
||||
if (cmp != 0)
|
||||
//match files with another extension added on (likely to be mygame.bin.ecm)
|
||||
cmp = string.Compare(fragment, targetFile, !caseSensitive);
|
||||
if (cmp == 0)
|
||||
results.Add(fi.FileInfo);
|
||||
|
||||
}
|
||||
var ret = new List<string>();
|
||||
foreach (var fi in results)
|
||||
ret.Add(fi.FullName);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,6 +4,8 @@ using System.Text;
|
|||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using BizHawk.Emulation.DiscSystem.CUE;
|
||||
|
||||
namespace BizHawk.Emulation.DiscSystem
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -80,7 +82,7 @@ namespace BizHawk.Emulation.DiscSystem
|
|||
string infile = IN_FromPath;
|
||||
string cue_content = null;
|
||||
|
||||
var cfr = new CUE_Context.CueFileResolver();
|
||||
var cfr = new CueFileResolver();
|
||||
|
||||
RERUN:
|
||||
var ext = Path.GetExtension(infile).ToLowerInvariant();
|
||||
|
@ -109,18 +111,18 @@ namespace BizHawk.Emulation.DiscSystem
|
|||
if (!cfr.IsHardcodedResolve) cfr.SetBaseDirectory(Path.GetDirectoryName(infile));
|
||||
|
||||
//parse the cue file
|
||||
var parseJob = new CUE_Context.ParseCueJob();
|
||||
var parseJob = new ParseCueJob();
|
||||
if (cue_content == null)
|
||||
cue_content = File.ReadAllText(cuePath);
|
||||
parseJob.IN_CueString = cue_content;
|
||||
cue2.ParseCueFile(parseJob);
|
||||
parseJob.Run(parseJob);
|
||||
//TODO - need better handling of log output
|
||||
if (!string.IsNullOrEmpty(parseJob.OUT_Log)) Console.WriteLine(parseJob.OUT_Log);
|
||||
ConcatenateJobLog(parseJob);
|
||||
|
||||
//compile the cue file:
|
||||
//includes this work: resolve required bin files and find out what it's gonna take to load the cue
|
||||
var compileJob = new CUE_Context.CompileCueJob();
|
||||
var compileJob = new CompileCueJob();
|
||||
compileJob.IN_CueFormat = cue2;
|
||||
compileJob.IN_CueFile = parseJob.OUT_CueFile;
|
||||
compileJob.Run();
|
||||
|
@ -137,7 +139,7 @@ namespace BizHawk.Emulation.DiscSystem
|
|||
}
|
||||
|
||||
//actually load it all up
|
||||
var loadJob = new CUE_Context.LoadCueJob();
|
||||
var loadJob = new LoadCueJob();
|
||||
loadJob.IN_CompileJob = compileJob;
|
||||
loadJob.Run();
|
||||
//TODO - need better handling of log output
|
||||
|
|
Loading…
Reference in New Issue