BizHawk/BizHawk.Emulation.DiscSystem/CUE/CUE_Load.cs

391 lines
14 KiB
C#
Raw Normal View History

2015-06-23 18:57:11 +00:00
//TODO:
//"The first index of a file must start at 00:00:00" - if this isnt the case, we'll be doing nonsense for sure. so catch it
//Recover the idea of TOCPoints maybe, as it's a more flexible way of generating the structure.
//TODO
//check for flags changing after a PREGAP is processed. the PREGAP can't correctly run if the flags aren't set
//IN GENERAL: validate more pedantically (but that code gets in the way majorly)
// - perhaps isolate validation checks into a different pass distinct from a Burn pass
//NEW IDEA:
//a cue file is a compressed representation of a more verbose format which is easier to understand
//most fundamentally, it is organized with TRACK and INDEX commands alternating.
//these should be flattened into individual records with CURRTRACK and CURRINDEX fields.
//more generally, it's organized with 'register' settings and INDEX commands alternating.
//whenever an INDEX command is received from the cue file, individual flattened records are written with the current 'register' settings
//and an incrementing timestamp until the INDEX command appears (or the EOF happens)
//PREGAP commands are special : at the moment it is received, emit flat records with a different pregap structure
//POSTGAP commands are special : TBD
2015-06-23 18:57:11 +00:00
using System;
using System.Linq;
using System.Text;
using System.IO;
using System.Collections.Generic;
namespace BizHawk.Emulation.DiscSystem
{
partial class CUE_Format2
{
/// <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 : LoggedJob
2015-06-23 18:57:11 +00:00
{
/// <summary>
/// The results of the compile job, a prerequisite for this
2015-06-23 18:57:11 +00:00
/// </summary>
public CompileCueJob IN_CompileJob;
2015-06-23 18:57:11 +00:00
/// <summary>
/// The resulting disc
/// </summary>
public Disc OUT_Disc;
private enum BurnType
2015-06-23 18:57:11 +00:00
{
Normal, Pregap, Postgap
2015-06-23 18:57:11 +00:00
}
2015-07-01 07:56:55 +00:00
class BlobInfo
{
2015-07-01 07:56:55 +00:00
public IBlob Blob;
public long Length;
}
2015-06-23 18:57:11 +00:00
2015-07-01 07:56:55 +00:00
//not sure if we need this...
class TrackInfo
{
2015-07-01 07:56:55 +00:00
public int Length;
2015-06-23 18:57:11 +00:00
2015-07-01 07:56:55 +00:00
public CompiledCueTrack CompiledCueTrack;
}
2015-06-23 18:57:11 +00:00
2015-07-01 07:56:55 +00:00
List<BlobInfo> BlobInfos;
List<TrackInfo> TrackInfos = new List<TrackInfo>();
2015-06-23 18:57:11 +00:00
void MountBlobs()
{
2015-07-01 07:56:55 +00:00
IBlob file_blob = null;
BlobInfos = new List<BlobInfo>();
foreach (var ccf in IN_CompileJob.OUT_CompiledCueFiles)
{
2015-07-01 07:56:55 +00:00
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);
2015-07-01 07:56:55 +00:00
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);
2015-07-01 07:56:55 +00:00
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);
2015-07-01 07:56:55 +00:00
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));
2015-07-01 07:56:55 +00:00
bi.Length = buf.Length;
break;
}
default:
throw new InvalidOperationException();
2015-07-01 07:56:55 +00:00
} //switch(file type)
//wrap all the blobs with zero padding
bi.Blob = new Disc.Blob_ZeroPadAdapter(file_blob, bi.Length);
}
}
2015-07-01 07:56:55 +00:00
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
//}
}
}
2015-07-01 08:00:27 +00:00
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 });
}
2015-07-01 07:56:55 +00:00
public void Run()
2015-06-23 18:57:11 +00:00
{
//params
var compiled = IN_CompileJob;
var context = compiled.IN_CueFormat;
OUT_Disc = new Disc();
2015-07-01 07:56:55 +00:00
//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();
2015-07-01 07:56:55 +00:00
//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)
2015-07-01 07:56:55 +00:00
for (int t = 1; t < TrackInfos.Count; t++)
{
2015-07-01 07:56:55 +00:00
TrackInfo ti = TrackInfos[t];
CompiledCueTrack cct = ti.CompiledCueTrack;
//---------------------------------
//generate track pregap
//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;
//total pregap is needed for subQ addressing of the entire pregap area
int totalPregapLength = specifiedPregapLength + impliedPregapLength;
for (int s = 0; s < specifiedPregapLength; s++)
{
2015-07-01 08:34:25 +00:00
//TODO - do a better job synthesizing Q
var se_pregap = new SectorEntry(null);
var ss_pregap = new SS_Pregap();
//pregaps set pause flag
//TODO - do a better job synthesizing P
ss_pregap.Pause = true;
se_pregap.SectorSynth = ss_pregap;
OUT_Disc.Sectors.Add(se_pregap);
}
2015-07-01 08:34:25 +00:00
//after this, pregap sectors are generated like a normal sector, but the subQ is specified as a pregap instead of a normal track (actually, TBD)
//---------------------------------
//---------------------------------
2015-07-01 07:56:55 +00:00
//generate sectors for this track.
2015-07-01 07:56:55 +00:00
//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];
}
2015-07-01 07:56:55 +00:00
//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;
2015-07-01 07:56:55 +00:00
//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)
{
2015-07-01 08:00:27 +00:00
//WE ARE NOW AT INDEX 1: generate the RawTOCEntry for this track
EmitRawTOCEntry(cct);
2015-07-01 07:56:55 +00:00
}
}
else break;
}
//generate a sector:
SS_Base ss = null;
switch (cct.TrackType)
{
case CueFile.TrackType.Mode2_2352:
case CueFile.TrackType.Audio:
ss = new SS_2352() { Blob = curr_blobInfo.Blob, BlobOffset = curr_blobOffset };
curr_blobOffset += 2352;
break;
}
//make the subcode
//TODO - according to policies, or better yet, defer this til it's needed (user delivers a policies object to disc reader apis)
//at any rate, we'd paste this logic into there so let's go ahead and write it here
var subcode = new BufferedSubcodeSector(); //(its lame that we have to use this; make a static method when we delete this class)
SubchannelQ sq = new SubchannelQ();
byte ADR = 1; //absent some kind of policy for how to set it, this is a safe assumption:
sq.SetStatus(ADR, (EControlQ)(int)cct.Flags);
sq.q_tno = BCD2.FromDecimal(cct.Number);
sq.q_index = BCD2.FromDecimal(curr_index);
int LBA = OUT_Disc.Sectors.Count;
sq.ap_min = BCD2.FromDecimal(new Timestamp(LBA).MIN);
sq.ap_sec = BCD2.FromDecimal(new Timestamp(LBA).SEC);
sq.ap_frame = BCD2.FromDecimal(new Timestamp(LBA).FRAC);
int track_relative_msf = curr_blobMSF - cct.Indexes[1].FileMSF.Sector;
2015-07-01 08:34:25 +00:00
//for index 0, negative MSF required and encoded oppositely. Read more at Policies declaration
2015-07-01 08:34:25 +00:00
if (curr_index == 0)
{
if (!context.DiscMountPolicy.CUE_PauseContradictionModeA)
track_relative_msf += 1;
if (track_relative_msf > 0) throw new InvalidOperationException("Severe error generating cue subQ (positive relMSF for pregap)");
2015-07-01 08:34:25 +00:00
track_relative_msf = -track_relative_msf;
}
else
if (track_relative_msf < 0) throw new InvalidOperationException("Severe error generating cue subQ (negative relMSF for non-pregap)");
2015-07-01 08:34:25 +00:00
2015-07-01 07:56:55 +00:00
sq.min = BCD2.FromDecimal(new Timestamp(track_relative_msf).MIN);
sq.sec = BCD2.FromDecimal(new Timestamp(track_relative_msf).SEC);
sq.frame = BCD2.FromDecimal(new Timestamp(track_relative_msf).FRAC);
//finally we're done: synthesize subchannel
subcode.Synthesize_SubchannelQ(ref sq, true);
2015-07-01 08:34:25 +00:00
//generate subP
if (curr_index == 0)
ss.Pause = true;
2015-07-01 07:56:55 +00:00
//make the SectorEntry (some temporary bullshit here)
var se = new SectorEntry(null);
se.SectorSynth = ss;
ss.sq = sq;
OUT_Disc.Sectors.Add(se);
curr_blobMSF++;
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;
}
2015-07-01 07:56:55 +00:00
//TODO - POSTGAP
2015-07-01 07:56:55 +00:00
} //end track loop
2015-07-01 07:56:55 +00:00
//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);
2015-07-01 07:56:55 +00:00
//generate the TOCRaw from the RawTocEntries
var tocSynth = new DiscTOCRaw.SynthesizeFromRawTOCEntriesJob() { Entries = OUT_Disc.RawTOCEntries };
tocSynth.Run();
OUT_Disc.TOCRaw = tocSynth.Result;
////generate lead-out track with some canned number of sectors
////TODO - move this somewhere else and make it controllable depending on which console is loading up the disc
////TODO - we're not doing this yet
////var synthLeadoutJob = new Disc.SynthesizeLeadoutJob { Disc = disc, Length = 150 };
////synthLeadoutJob.Run();
////blech, old crap, maybe
//OUT_Disc.Structure.Synthesize_TOCPointsFromSessions();
//FinishLog();
} //Run()
} //class LoadCueJob
} //partial class CUE_Format2
2015-07-01 07:56:55 +00:00
} //namespace BizHawk.Emulation.DiscSystem
//TODO:
//if (index_num == 0 || type == SectorWriteType.Pregap)
//{
// //PREGAP:
// //things are negative here.
// if (track_relative_msf > 0) throw new InvalidOperationException("Perplexing internal error with non-negative pregap MSF");
// track_relative_msf = -track_relative_msf;
// //now for something special.
// //yellow-book says:
// //pre-gap for "first part of a digital data track not containing user data and encoded as a pause"
// //first interval: at least 75 sectors coded as preceding track
// //second interval: at least 150 sectors coded as user data track.
// //so... we ASSUME the 150 sector pregap is more important. so if thats all there is, theres no 75 sector pregap like the old track
// //if theres a longer pregap, then we generate weird old track pregap to contain the rest.
// if (track_relative_msf > 150)
// {
// //only if we're burning a data track now
// if((track_flags & CueFile.TrackFlags.DATA)!=0)
// sq.q_status = priorSubchannelQ.q_status;
// }
//}