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
2015-06-28 10:33:10 +00:00
//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 ;
2015-07-12 22:45:20 +00:00
namespace BizHawk.Emulation.DiscSystem.CUE
2015-06-23 18:57:11 +00:00
{
2015-07-12 22:45:20 +00:00
/// <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
2015-06-23 18:57:11 +00:00
{
/// <summary>
2015-07-12 22:45:20 +00:00
/// The results of the compile job, a prerequisite for this
2015-06-23 18:57:11 +00:00
/// </summary>
2015-07-12 22:45:20 +00:00
public CompileCueJob IN_CompileJob ;
2015-06-23 18:57:11 +00:00
2015-07-12 22:45:20 +00:00
/// <summary>
/// The resulting disc
/// </summary>
public Disc OUT_Disc ;
2015-06-23 18:57:11 +00:00
2015-07-12 22:45:20 +00:00
private enum BurnType
{
Normal , Pregap , Postgap
}
2015-06-23 18:57:11 +00:00
2015-07-12 22:45:20 +00:00
class BlobInfo
{
public IBlob Blob ;
public long Length ;
}
2015-06-23 18:57:11 +00:00
2015-07-12 22:45:20 +00:00
//not sure if we need this...
class TrackInfo
{
public int Length ;
2015-06-23 18:57:11 +00:00
2015-07-12 22:45:20 +00:00
public CompiledCueTrack CompiledCueTrack ;
}
2015-06-23 18:57:11 +00:00
2015-07-12 22:45:20 +00:00
List < BlobInfo > BlobInfos ;
List < TrackInfo > TrackInfos = new List < TrackInfo > ( ) ;
2015-06-23 18:57:11 +00:00
2015-06-28 10:33:10 +00:00
2015-07-12 22:45:20 +00:00
void MountBlobs ( )
{
IBlob file_blob = null ;
BlobInfos = new List < BlobInfo > ( ) ;
foreach ( var ccf in IN_CompileJob . OUT_CompiledCueFiles )
2015-07-01 06:09:20 +00:00
{
2015-07-12 22:45:20 +00:00
var bi = new BlobInfo ( ) ;
BlobInfos . Add ( bi ) ;
2015-07-01 07:56:55 +00:00
2015-07-12 22:45:20 +00:00
switch ( ccf . Type )
2015-07-01 06:09:20 +00:00
{
2015-07-12 22:45:20 +00:00
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 ( ) )
2015-07-01 06:09:20 +00:00
{
2015-07-12 22:45:20 +00:00
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.)" ) ;
2015-07-01 06:09:20 +00:00
}
2015-07-12 22:45:20 +00:00
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)
2015-07-01 07:56:55 +00:00
2015-07-12 22:45:20 +00:00
//wrap all the blobs with zero padding
bi . Blob = new Disc . Blob_ZeroPadAdapter ( file_blob , bi . Length ) ;
2015-07-01 06:09:20 +00:00
}
2015-07-12 22:45:20 +00:00
}
2015-07-01 06:09:20 +00:00
2015-07-01 07:56:55 +00:00
2015-07-12 22:45:20 +00:00
void AnalyzeTracks ( )
{
var compiledTracks = IN_CompileJob . OUT_CompiledCueTracks ;
2015-07-01 07:56:55 +00:00
2015-07-12 22:45:20 +00:00
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 07:56:55 +00:00
2015-07-12 22:45:20 +00:00
//}
2015-07-01 07:56:55 +00:00
}
2015-07-12 22:45:20 +00:00
}
2015-07-01 07:56:55 +00:00
2015-07-12 22:45:20 +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 ) ;
2015-07-19 04:23:15 +00:00
toc_sq . AP_Timestamp = OUT_Disc . _Sectors . Count ;
2015-07-12 22:45:20 +00:00
OUT_Disc . RawTOCEntries . Add ( new RawTOCEntry { QData = toc_sq } ) ;
}
public void Run ( )
{
//params
var compiled = IN_CompileJob ;
2015-07-12 23:04:55 +00:00
var context = compiled . IN_CueContext ;
2015-07-12 22:45:20 +00:00
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 + + )
2015-07-01 08:00:27 +00:00
{
2015-07-12 22:45:20 +00:00
TrackInfo ti = TrackInfos [ t ] ;
CompiledCueTrack cct = ti . CompiledCueTrack ;
2015-07-01 07:56:55 +00:00
2015-07-12 22:45:20 +00:00
//---------------------------------
//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 ;
2015-07-01 06:09:20 +00:00
2015-07-12 22:45:20 +00:00
//read more at policies declaration
//if (!context.DiscMountPolicy.CUE_PauseContradictionModeA)
// relMSF += 1;
//---------------------------------
2015-07-01 10:41:56 +00:00
2015-07-12 22:45:20 +00:00
//---------------------------------
//generate sectors for this track.
2015-07-01 06:09:20 +00:00
2015-07-12 22:45:20 +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 06:09:20 +00:00
2015-07-12 22:45:20 +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 ;
bool generateGap = false ;
2015-06-28 22:27:21 +00:00
2015-07-12 22:45:20 +00:00
if ( specifiedPregapLength > 0 )
2015-07-01 07:56:55 +00:00
{
2015-07-12 22:45:20 +00:00
//if burning through a specified pregap, count it down
generateGap = true ;
specifiedPregapLength - - ;
2015-07-01 07:56:55 +00:00
}
2015-07-12 22:45:20 +00:00
else
2015-07-01 07:56:55 +00:00
{
2015-07-12 22:45:20 +00:00
//if burning through the file, select the appropriate index by inspecting the next index and seeing if we've reached it
for ( ; ; )
2015-07-01 07:56:55 +00:00
{
2015-07-12 22:45:20 +00:00
if ( curr_index = = cct . Indexes . Count - 1 )
break ;
if ( curr_blobMSF > = cct . Indexes [ curr_index + 1 ] . FileMSF . Sector )
2015-07-01 07:56:55 +00:00
{
2015-07-12 22:45:20 +00:00
curr_index + + ;
if ( curr_index = = 1 )
2015-07-01 07:56:55 +00:00
{
2015-07-12 22:45:20 +00:00
//WE ARE NOW AT INDEX 1: generate the RawTOCEntry for this track
EmitRawTOCEntry ( cct ) ;
2015-07-01 07:56:55 +00:00
}
}
2015-07-12 22:45:20 +00:00
else break ;
2015-07-01 07:56:55 +00:00
}
2015-07-12 22:45:20 +00:00
}
2015-07-01 07:56:55 +00:00
2015-07-12 22:45:20 +00:00
//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 ! = CueTrackType . Audio & & TrackInfos [ t - 1 ] . CompiledCueTrack . TrackType = = CueTrackType . Audio )
2015-07-01 07:56:55 +00:00
{
2015-07-12 22:45:20 +00:00
if ( relMSF < - 150 )
2015-07-01 11:17:57 +00:00
{
2015-07-12 22:45:20 +00:00
qTrack = TrackInfos [ t - 1 ] ;
2015-07-01 11:17:57 +00:00
}
2015-07-01 11:34:37 +00:00
}
2015-07-12 22:45:20 +00:00
}
2015-07-01 11:34:37 +00:00
2015-07-12 22:45:20 +00:00
//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 )
2015-07-01 11:43:06 +00:00
{
2015-07-12 22:45:20 +00:00
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 ;
2015-07-02 01:48:49 +00:00
2015-07-12 22:45:20 +00:00
default :
case CueTrackType . Mode2_2336 :
throw new InvalidOperationException ( "Not supported: " + cct . TrackType ) ;
2015-07-01 07:56:55 +00:00
}
2015-07-12 22:45:20 +00:00
ss . Blob = curr_blobInfo . Blob ;
ss . BlobOffset = curr_blobOffset ;
curr_blobOffset + = sectorSize ;
curr_blobMSF + + ;
}
2015-07-02 01:28:02 +00:00
2015-07-12 22:45:20 +00:00
ss . Policy = context . DiscMountPolicy ;
2015-07-01 07:56:55 +00:00
2015-07-12 22:45:20 +00:00
//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 ) ;
2015-07-19 04:23:15 +00:00
ss . sq . AP_Timestamp = OUT_Disc . _Sectors . Count ;
ss . sq . Timestamp = qRelMSF ;
2015-07-01 08:34:25 +00:00
2015-07-12 22:45:20 +00:00
//setup subP
if ( curr_index = = 0 )
ss . Pause = true ;
2015-07-01 07:56:55 +00:00
2015-07-15 02:04:05 +00:00
OUT_Disc . _Sectors . Add ( ss ) ;
2015-07-12 22:45:20 +00:00
relMSF + + ;
2015-07-01 07:56:55 +00:00
2015-07-12 22:45:20 +00:00
if ( cct . IsFinalInFile )
{
//sometimes, break when the file is exhausted
if ( curr_blobOffset > = curr_blobInfo . Length )
trackDone = true ;
2015-07-01 07:56:55 +00:00
}
2015-07-12 22:45:20 +00:00
else
2015-07-01 09:18:48 +00:00
{
2015-07-12 22:45:20 +00:00
//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 ;
2015-07-01 09:18:48 +00:00
}
2015-07-12 22:45:20 +00:00
if ( trackDone )
break ;
}
//---------------------------------
//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 ) ;
2015-07-19 04:23:15 +00:00
ss . sq . AP_Timestamp = OUT_Disc . _Sectors . Count ;
ss . sq . Timestamp = relMSF ;
2015-07-12 22:45:20 +00:00
//-subP-
//always paused--is this good enough?
ss . Pause = true ;
2015-07-15 02:04:05 +00:00
OUT_Disc . _Sectors . Add ( ss ) ;
2015-07-12 22:45:20 +00:00
relMSF + + ;
}
2015-06-28 22:27:21 +00:00
2015-07-12 22:45:20 +00:00
} //end track loop
2015-06-28 22:27:21 +00:00
2015-07-12 22:45:20 +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 ,
2015-07-19 04:23:15 +00:00
IN_LeadoutTimestamp = OUT_Disc . _Sectors . Count
2015-07-12 22:45:20 +00:00
} ;
TOCMiscInfo . Run ( OUT_Disc . RawTOCEntries ) ;
2015-06-28 22:27:21 +00:00
2015-07-12 22:45:20 +00:00
//TODO - generate leadout, or delegates at least
2015-06-28 22:27:21 +00:00
2015-07-12 22:45:20 +00:00
//blech, old crap, maybe
//OUT_Disc.Structure.Synthesize_TOCPointsFromSessions();
2015-06-28 22:27:21 +00:00
2015-07-12 22:45:20 +00:00
//FinishLog();
2015-06-28 10:33:10 +00:00
2015-07-12 22:45:20 +00:00
} //Run()
} //class LoadCueJob
2015-07-01 07:56:55 +00:00
} //namespace BizHawk.Emulation.DiscSystem