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 ;
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>
2015-06-28 22:27:21 +00:00
internal class LoadCueJob : LoggedJob
2015-06-23 18:57:11 +00:00
{
/// <summary>
2015-06-28 22:27:21 +00:00
/// The results of the compile job, a prerequisite for this
2015-06-23 18:57:11 +00:00
/// </summary>
2015-06-28 22:27:21 +00:00
public CompileCueJob IN_CompileJob ;
2015-06-23 18:57:11 +00:00
/// <summary>
/// The resulting disc
/// </summary>
public Disc OUT_Disc ;
2015-06-28 10:33:10 +00:00
private enum BurnType
2015-06-23 18:57:11 +00:00
{
2015-06-28 10:33:10 +00:00
Normal , Pregap , Postgap
2015-06-23 18:57:11 +00:00
}
2015-06-28 10:33:10 +00:00
//current blob file state
int file_cfi_index = - 1 ;
IBlob file_blob = null ;
CueFile . Command . FILE file_currentCommand = null ;
long file_ofs = 0 , file_len = 0 ;
int file_msf = - 1 ;
//current track, flags, and index state
CueFile . Command . TRACK track_pendingCommand = null ;
CueFile . Command . TRACK track_currentCommand = null ;
CueFile . TrackFlags track_pendingFlags = CueFile . TrackFlags . None ;
CueFile . TrackFlags track_currentFlags = CueFile . TrackFlags . None ;
//burn state.
//TODO - separate burner into another class?
BurnType burntype_current ;
Timestamp burn_pregap_timestamp ;
2015-06-23 18:57:11 +00:00
2015-06-28 10:33:10 +00:00
void BeginBurnPregap ( )
{
//TODO?
}
2015-06-23 18:57:11 +00:00
2015-06-28 10:33:10 +00:00
void BurnPregap ( Timestamp length )
{
burntype_current = BurnType . Pregap ;
burn_pregap_timestamp = length ;
int length_lba = length . Sector ;
//TODO: read [IEC10149] 20, 20.1, & 20.2 to assign pre-gap and post-gap types correctly depending on track number and previous track
//ALSO, if the last track is data, we need to make a post-gap
//we can grab the previously generated sector in order to figure out how to encode new pregap sectors
for ( int i = 0 ; i < length_lba ; i + + )
BurnSector ( ) ;
}
2015-06-23 18:57:11 +00:00
2015-06-28 10:33:10 +00:00
void ProcessFile ( CueFile . Command . FILE file )
{
2015-06-28 22:27:21 +00:00
////if we're currently in a file, finish it
//if (file_currentCommand != null)
// BurnToEOF();
////open the new blob
//file_currentCommand = file;
//file_msf = 0;
//var cfi = IN_CompileJob.OUT_FileInfos[++file_cfi_index];
////mount the file
//if (cfi.Type == AnalyzeCueJob.CueFileType.BIN || cfi.Type == AnalyzeCueJob.CueFileType.Unknown)
//{
// //raw files:
// var blob = new Disc.Blob_RawFile { PhysicalPath = cfi.FullPath };
// OUT_Disc.DisposableResources.Add(file_blob = blob);
// file_len = blob.Length;
//}
//else if (cfi.Type == AnalyzeCueJob.CueFileType.ECM)
//{
// var blob = new Disc.Blob_ECM();
// OUT_Disc.DisposableResources.Add(file_blob = blob);
// blob.Load(cfi.FullPath);
// file_len = blob.Length;
//}
//else if (cfi.Type == AnalyzeCueJob.CueFileType.WAVE)
//{
// var blob = new Disc.Blob_WaveFile();
// OUT_Disc.DisposableResources.Add(file_blob = blob);
// blob.Load(cfi.FullPath);
// file_len = blob.Length;
//}
//else if (cfi.Type == AnalyzeCueJob.CueFileType.DecodeAudio)
//{
// FFMpeg ffmpeg = new FFMpeg();
// if (!ffmpeg.QueryServiceAvailable())
// {
// throw new DiscReferenceException(cfi.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(cfi.FullPath);
// var blob = new Disc.Blob_WaveFile();
// OUT_Disc.DisposableResources.Add(file_blob = blob);
// blob.Load(new MemoryStream(buf));
//}
2015-06-23 18:57:11 +00:00
}
2015-06-28 10:33:10 +00:00
void BurnToEOF ( )
{
while ( file_ofs < file_len )
BurnSector ( ) ;
2015-06-23 18:57:11 +00:00
2015-06-28 10:33:10 +00:00
//TODO - if a postgap was requested, do it now
}
2015-06-23 18:57:11 +00:00
2015-06-28 10:33:10 +00:00
void ProcessIndex ( CueFile . Command . INDEX index )
2015-06-23 18:57:11 +00:00
{
2015-06-28 10:33:10 +00:00
//burn sectors with the previous registers until we reach the current index MSF
int index_file_msf = index . Timestamp . Sector ;
while ( file_msf < index_file_msf )
BurnSector ( ) ;
//latch current track settings
track_currentCommand = track_pendingCommand ;
track_currentFlags = track_pendingFlags ;
//index 0 is annoying. we have to code its subchannels while knowing the index that comes next
//this is the main reason for transforming the cue file into a CueGrid (any index 0 can easily reference the index 1 that comes after it)
if ( index . Number = = 0 )
{
}
2015-06-23 18:57:11 +00:00
}
2015-06-28 10:33:10 +00:00
void EatBlobFileSector ( int required , out IBlob blob , out long blobOffset )
2015-06-23 18:57:11 +00:00
{
2015-06-28 10:33:10 +00:00
blob = file_blob ;
blobOffset = file_ofs ;
2015-06-23 18:57:11 +00:00
if ( file_ofs + required > file_len )
{
2015-06-28 10:33:10 +00:00
Warn ( "Zero-padding mis-sized cue blob file: " + Path . GetFileName ( file_currentCommand . Path ) ) ;
2015-06-23 18:57:11 +00:00
blob = Disc . Blob_ZeroPadBuffer . MakeBufferFrom ( file_blob , file_ofs , required ) ;
2015-06-28 10:33:10 +00:00
OUT_Disc . DisposableResources . Add ( blob ) ;
blobOffset = 0 ;
2015-06-23 18:57:11 +00:00
}
file_ofs + = required ;
2015-06-28 10:33:10 +00:00
}
2015-06-23 18:57:11 +00:00
2015-06-28 10:33:10 +00:00
void BurnSector_Normal ( )
2015-06-23 18:57:11 +00:00
{
2015-06-28 08:51:45 +00:00
SS_Base ss = null ;
2015-06-28 10:33:10 +00:00
switch ( track_currentCommand . Type )
2015-06-23 18:57:11 +00:00
{
2015-06-28 10:33:10 +00:00
case CueFile . TrackType . Mode2_2352 :
ss = new SS_2352 ( ) ;
EatBlobFileSector ( 2352 , out ss . Blob , out ss . BlobOffset ) ;
break ;
case CueFile . TrackType . Audio :
ss = new SS_2352 ( ) ;
EatBlobFileSector ( 2352 , out ss . Blob , out ss . BlobOffset ) ;
break ;
2015-06-23 18:57:11 +00:00
}
2015-06-28 08:51:45 +00:00
var se = new SectorEntry ( null ) ;
se . SectorSynth = ss ;
2015-06-28 10:33:10 +00:00
OUT_Disc . Sectors . Add ( se ) ;
}
2015-06-23 18:57:11 +00:00
2015-06-28 10:33:10 +00:00
void BurnSector_Pregap ( )
2015-06-23 18:57:11 +00:00
{
2015-06-28 10:33:10 +00:00
var se = new SectorEntry ( null ) ;
se . SectorSynth = new SS_Mode1_2048 ( ) ; //TODO - actually burn the right thing
OUT_Disc . Sectors . Add ( se ) ;
2015-06-23 18:57:11 +00:00
2015-06-28 10:33:10 +00:00
burn_pregap_timestamp = new Timestamp ( burn_pregap_timestamp . Sector - 1 ) ;
}
void BurnSector ( )
2015-06-23 18:57:11 +00:00
{
2015-06-28 10:33:10 +00:00
switch ( burntype_current )
2015-06-23 18:57:11 +00:00
{
2015-06-28 10:33:10 +00:00
case BurnType . Normal :
BurnSector_Normal ( ) ;
break ;
case BurnType . Pregap :
BurnSector_Pregap ( ) ;
break ;
2015-06-23 18:57:11 +00:00
}
2015-06-28 10:33:10 +00:00
}
2015-07-01 06:09:20 +00:00
void MountBlobs ( )
{
foreach ( var ccf in IN_CompileJob . OUT_CompiledCueFiles )
{
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 ) ;
file_len = blob . Length ;
break ;
}
case CompiledCueFileType . ECM :
{
var blob = new Disc . Blob_ECM ( ) ;
OUT_Disc . DisposableResources . Add ( file_blob = blob ) ;
blob . Load ( ccf . FullPath ) ;
file_len = blob . Length ;
break ;
}
case CompiledCueFileType . WAVE :
{
var blob = new Disc . Blob_WaveFile ( ) ;
OUT_Disc . DisposableResources . Add ( file_blob = blob ) ;
blob . Load ( ccf . FullPath ) ;
file_len = 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 ) ) ;
break ;
}
default :
throw new InvalidOperationException ( ) ;
}
}
}
2015-06-28 10:33:10 +00:00
public void Run ( )
2015-06-23 18:57:11 +00:00
{
2015-07-01 06:09:20 +00:00
//params
var compiled = IN_CompileJob ;
OUT_Disc = new Disc ( ) ;
//mount all input files
MountBlobs ( ) ;
//make a lookup from track number to CompiledCueTrack
Dictionary < int , CompiledCueTrack > trackLookup = new Dictionary < int , CompiledCueTrack > ( ) ;
foreach ( var cct in compiled . OUT_CompiledCueTracks )
trackLookup [ cct . Number ] = cct ;
//loop from track 1 to 99
//(track 0 isnt handled yet, that's way distant work)
for ( int t = 1 ; t < = 99 ; t + + )
{
CompiledCueTrack cct ;
if ( ! trackLookup . TryGetValue ( t , out cct ) )
continue ;
//---------------------------------
//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 + + )
{
//TODO - do a better job synthesizing
var zero_sector = new Sector_Zero ( ) ;
var zero_subSector = new ZeroSubcodeSector ( ) ;
var se_pregap = new SectorEntry ( zero_sector ) ;
se_pregap . SubcodeSector = zero_subSector ;
se_pregap . SectorSynth = new SS_Mode1_2048 ( ) ;
OUT_Disc . Sectors . Add ( se_pregap ) ;
}
//after this, pregap sectors are generated like a normal sector, but the subQ is specified as a pregap instead of a normal track
//---------------------------------
//---------------------------------
//WE ARE NOW AT INDEX 1
//---------------------------------
//---------------------------------
//generate the RawTOCEntry for this track
SubchannelQ sq = new SubchannelQ ( ) ;
//absent some kind of policy for how to set it, this is a safe assumption:
byte ADR = 1 ;
sq . SetStatus ( ADR , ( EControlQ ) ( int ) cct . Flags ) ;
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.
sq . q_index = BCD2 . FromDecimal ( cct . Number ) ;
//not too sure about these yet
sq . min = BCD2 . FromDecimal ( 0 ) ;
sq . sec = BCD2 . FromDecimal ( 0 ) ;
sq . frame = BCD2 . FromDecimal ( 0 ) ;
sq . AP_Timestamp = new Timestamp ( OUT_Disc . Sectors . Count + 150 ) ; //its supposed to be an absolute timestamp
OUT_Disc . RawTOCEntries . Add ( new RawTOCEntry { QData = sq } ) ;
}
2015-06-28 22:27:21 +00:00
2015-07-01 06:09:20 +00:00
2015-06-28 22:27:21 +00:00
////now for the magic. Process commands in order
//for (int i = 0; i < cue.Commands.Count; i++)
//{
// var cmd = cue.Commands[i];
// //these commands get dealt with globally. nothing to be done here
// 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;
// //handle cdtext and ISRC state updates, theyre kind of like little registers
// if (cmd is CueFile.Command.PERFORMER)
// cdtext_performer = (cmd as CueFile.Command.PERFORMER).Value;
// if (cmd is CueFile.Command.SONGWRITER)
// cdtext_songwriter = (cmd as CueFile.Command.SONGWRITER).Value;
// if (cmd is CueFile.Command.TITLE)
// cdtext_title = (cmd as CueFile.Command.TITLE).Value;
// if (cmd is CueFile.Command.ISRC)
// isrc = (cmd as CueFile.Command.ISRC).Value;
// //flags are also a kind of a register. but the flags value is reset by the track command
// if (cmd is CueFile.Command.FLAGS)
// {
// track_pendingFlags = (cmd as CueFile.Command.FLAGS).Flags;
// }
// if (cmd is CueFile.Command.TRACK)
// {
// var track = cmd as CueFile.Command.TRACK;
// //register the track for further processing when an GENERATION command appears
// track_pendingCommand = track;
// track_pendingFlags = CueFile.TrackFlags.None;
2015-06-28 10:33:10 +00:00
2015-06-28 22:27:21 +00:00
// }
// if (cmd is CueFile.Command.FILE)
// {
// ProcessFile(cmd as CueFile.Command.FILE);
// }
// if (cmd is CueFile.Command.INDEX)
// {
// ProcessIndex(cmd as CueFile.Command.INDEX);
// }
//}
//BurnToEOF();
////add RawTOCEntries A0 A1 A2 to round out the TOC
//var TOCMiscInfo = new Synthesize_A0A1A2_Job {
// IN_FirstRecordedTrackNumber = sloshy_firstRecordedTrackNumber,
// IN_LastRecordedTrackNumber = sloshy_lastRecordedTrackNumber,
// IN_Session1Format = sloshy_session1Format,
// IN_LeadoutTimestamp = new Timestamp(OUT_Disc.Sectors.Count)
//};
//TOCMiscInfo.Run(OUT_Disc.RawTOCEntries);
////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();
2015-06-28 10:33:10 +00:00
} //Run()
} //class LoadCueJob
} //partial class CUE_Format2
} //namespace BizHawk.Emulation.DiscSystem