2011-05-08 09:07:46 +00:00
using System ;
using System.Text ;
using System.Text.RegularExpressions ;
using System.IO ;
using System.Collections.Generic ;
2011-06-20 09:09:21 +00:00
//this rule is not supported correctly: `The first track number can be greater than one, but all track numbers after the first must be sequential.`
2011-05-08 09:07:46 +00:00
namespace BizHawk.Disc
{
partial class Disc
{
void FromCuePathInternal ( string cuePath )
{
string cueDir = Path . GetDirectoryName ( cuePath ) ;
var cue = new Cue ( ) ;
cue . LoadFromPath ( cuePath ) ;
var session = new DiscTOC . Session ( ) ;
session . num = 1 ;
TOC . Sessions . Add ( session ) ;
2011-06-20 09:09:21 +00:00
var pregap_sector = new Sector_Zero ( ) ;
int curr_track = 1 ;
2011-05-08 09:07:46 +00:00
foreach ( var cue_file in cue . Files )
{
2011-06-20 09:09:21 +00:00
//structural validation
if ( cue_file . Tracks . Count < 1 ) throw new Cue . CueBrokenException ( "`You must specify at least one track per file.`" ) ;
int blob_sectorsize = Cue . BINSectorSizeForTrackType ( cue_file . Tracks [ 0 ] . TrackType ) ;
//make a blob for the file
2011-05-08 09:07:46 +00:00
Blob_RawFile blob = new Blob_RawFile ( ) ;
blob . PhysicalPath = Path . Combine ( cueDir , cue_file . Path ) ;
Blobs . Add ( blob ) ;
2011-06-20 09:09:21 +00:00
int blob_length_lba = ( int ) ( blob . Length / blob_sectorsize ) ;
int blob_leftover = ( int ) ( blob . Length - blob_length_lba * blob_sectorsize ) ;
2011-05-08 09:07:46 +00:00
2011-06-20 09:09:21 +00:00
//TODO - make CueTimestamp better, and also make it a struct, and also just make it DiscTimestamp
//TODO - wav handling
//TODO - mp3 decode
//start timekeeping for the blob. every time we hit an index, this will advance
int blob_timestamp = 0 ;
2011-05-08 09:07:46 +00:00
2011-06-20 09:09:21 +00:00
//for each track within the file, create an index 0 if it is missing.
//also check to make sure there is an index 1
for ( int t = 0 ; t < cue_file . Tracks . Count ; t + + )
2011-05-08 09:07:46 +00:00
{
2011-06-20 09:09:21 +00:00
var cue_track = cue_file . Tracks [ t ] ;
if ( ! cue_track . Indexes . ContainsKey ( 1 ) )
throw new Cue . CueBrokenException ( "Track was missing an index 01" ) ;
if ( ! cue_track . Indexes . ContainsKey ( 0 ) )
{
//index 0 will default to the same as index 1.
//i am not sure whether it is valid to have two indexes with the same timestamp.
//we will do this to simplify some processing, but we can purge it in a later pass if we need to.
var cti = new Cue . CueTrackIndex ( 0 ) ;
cue_track . Indexes [ 0 ] = cti ;
cti . Timestamp = cue_track . Indexes [ 1 ] . Timestamp ;
}
}
2011-05-08 09:07:46 +00:00
2011-06-20 09:09:21 +00:00
//validate that the first index in the file is 00:00:00
if ( cue_file . Tracks [ 0 ] . Indexes [ 0 ] . Timestamp . LBA ! = 0 ) throw new Cue . CueBrokenException ( "`The first index of a file must start at 00:00:00.`" ) ;
2011-05-08 09:07:46 +00:00
2011-06-20 09:09:21 +00:00
//for each track within the file:
for ( int t = 0 ; t < cue_file . Tracks . Count ; t + + )
2011-05-08 09:07:46 +00:00
{
2011-06-20 09:09:21 +00:00
var cue_track = cue_file . Tracks [ t ] ;
2011-05-08 09:07:46 +00:00
2011-06-20 09:09:21 +00:00
//record the disc LBA that this sector started on
int track_disc_lba_start = Sectors . Count ;
2011-05-08 09:07:46 +00:00
2011-06-20 09:09:21 +00:00
//record the pregap location. it will default to the start of the track unless we supplied a pregap command
int track_disc_pregap_lba = track_disc_lba_start ;
2011-05-08 09:07:46 +00:00
2011-06-20 09:09:21 +00:00
//enforce a rule of our own: every track within the file must have the same sector size
//we do know that files can change between track types within a file, but we're not sure what to do if the sector size changes
if ( Cue . BINSectorSizeForTrackType ( cue_track . TrackType ) ! = blob_sectorsize ) throw new Cue . CueBrokenException ( "Found different sector sizes within a cue file. We don't know how to handle that." ) ;
2011-05-08 09:07:46 +00:00
2011-06-20 09:09:21 +00:00
//check integrity of track sequence and setup data structures
//TODO - check for skipped tracks in cue parser instead
if ( cue_track . TrackNum ! = curr_track ) throw new Cue . CueBrokenException ( "Found a cue with skipped tracks" ) ;
var toc_track = new DiscTOC . Track ( ) ;
toc_track . num = curr_track ;
toc_track . TrackType = cue_track . TrackType ;
session . Tracks . Add ( toc_track ) ;
2011-05-08 09:07:46 +00:00
2011-06-20 09:09:21 +00:00
//check whether a pregap is requested.
//when this happens for the first track in a file, some empty sectors are generated
//when it happens for any other track, its just another way of specifying index 0 LBA
if ( cue_track . PreGap . LBA > 0 )
2011-05-08 09:07:46 +00:00
{
2011-06-20 09:09:21 +00:00
if ( t = = 0 )
for ( int i = 0 ; i < cue_track . PreGap . LBA ; i + + )
{
Sectors . Add ( new SectorEntry ( pregap_sector ) ) ;
}
else track_disc_pregap_lba - = cue_track . PreGap . LBA ;
2011-05-08 09:07:46 +00:00
}
2011-06-20 09:09:21 +00:00
//look ahead to the next track's index 0 so we can see how long this track's last index is
//or, for the last track, use the length of the file
int track_length_lba ;
if ( t = = cue_file . Tracks . Count - 1 )
track_length_lba = blob_length_lba - blob_timestamp ;
else track_length_lba = cue_file . Tracks [ t + 1 ] . Indexes [ 0 ] . Timestamp . LBA - blob_timestamp ;
//toc_track.length_lba = track_length_lba; //xxx
//find out how many indexes we have
int num_indexes = 0 ;
for ( num_indexes = 0 ; num_indexes < = 99 ; num_indexes + + )
if ( ! cue_track . Indexes . ContainsKey ( num_indexes ) ) break ;
//for each index, calculate length of index and then emit it
for ( int index = 0 ; index < num_indexes ; index + + )
2011-05-08 09:07:46 +00:00
{
2011-06-20 09:09:21 +00:00
bool is_last_index = index = = num_indexes - 1 ;
2011-05-08 09:07:46 +00:00
2011-06-20 09:09:21 +00:00
//install index into hierarchy
var toc_index = new DiscTOC . Index ( ) ;
toc_index . num = index ;
toc_track . Indexes . Add ( toc_index ) ;
if ( index = = 0 ) toc_index . lba = track_disc_pregap_lba ;
else toc_index . lba = Sectors . Count ;
//toc_index.lba += 150; //TODO - consider whether to add 150 here
//calculate length of the index
//if it is the last index then we use our calculation from before, otherwise we check the next index
int index_length_lba ;
if ( is_last_index )
index_length_lba = track_disc_lba_start + track_length_lba - blob_timestamp ;
else index_length_lba = cue_track . Indexes [ index + 1 ] . Timestamp . LBA - blob_timestamp ;
//emit sectors
for ( int lba = 0 ; lba < index_length_lba ; lba + + )
{
bool is_last_lba_in_index = ( lba = = index_length_lba - 1 ) ;
bool is_last_lba_in_track = is_last_lba_in_index & & is_last_index ;
switch ( cue_track . TrackType )
2011-05-08 09:07:46 +00:00
{
2011-06-20 09:09:21 +00:00
case ETrackType . Audio : //all 2352 bytes are present
case ETrackType . Mode1_2352 : //2352 bytes are present, containing 2048 bytes of user data as well as ECM
case ETrackType . Mode2_2352 : //2352 bytes are present, containing 2336 bytes of user data, with no ECM
{
//these cases are all 2352 bytes
//in all these cases, either no ECM is present or ECM is provided.
//so we just emit a Sector_Raw
Sector_RawBlob sector_rawblob = new Sector_RawBlob ( ) ;
sector_rawblob . Blob = blob ;
sector_rawblob . Offset = ( long ) blob_timestamp * 2352 ;
Sector_Raw sector_raw = new Sector_Raw ( ) ;
sector_raw . BaseSector = sector_rawblob ;
//take care to handle final sectors that are too short.
if ( is_last_lba_in_track & & blob_leftover > 0 )
{
Sector_ZeroPad sector_zeropad = new Sector_ZeroPad ( ) ;
sector_zeropad . BaseSector = sector_rawblob ;
sector_zeropad . BaseLength = 2352 - blob_leftover ;
sector_raw . BaseSector = sector_zeropad ;
Sectors . Add ( new SectorEntry ( sector_raw ) ) ;
}
Sectors . Add ( new SectorEntry ( sector_raw ) ) ;
break ;
}
case ETrackType . Mode1_2048 :
//2048 bytes are present. ECM needs to be generated to create a full sector
{
//ECM needs to know the sector number so we have to record that here
int curr_disc_lba = Sectors . Count ;
var sector_2048 = new Sector_Mode1_2048 ( curr_disc_lba + 150 ) ;
sector_2048 . Blob = new ECMCacheBlob ( blob ) ;
sector_2048 . Offset = ( long ) blob_timestamp * 2048 ;
if ( blob_leftover > 0 ) throw new Cue . CueBrokenException ( "TODO - Incomplete 2048 byte/sector bin files (iso files) not yet supported." ) ;
break ;
}
} //switch(TrackType)
//we've emitted an LBA, so consume it from the blob
blob_timestamp + + ;
} //lba emit loop
} //index loop
//check whether a postgap is requested. if it is, we need to generate silent sectors
for ( int i = 0 ; i < cue_track . PostGap . LBA ; i + + )
{
Sectors . Add ( new SectorEntry ( pregap_sector ) ) ;
2011-05-08 09:07:46 +00:00
}
2011-06-20 09:09:21 +00:00
//we're done with the track now.
//record its length:
toc_track . length_lba = Sectors . Count - track_disc_lba_start ;
curr_track + + ;
} //track loop
} //file loop
2011-05-08 09:07:46 +00:00
2011-06-20 09:09:21 +00:00
//finally, analyze the length of the sessions and the entire disc by summing the lengths of the tracks
//this is a little more complex than it looks, because the length of a thing is not determined by summing it
//but rather by the difference in lbas between start and end
TOC . length_lba = 0 ;
foreach ( var toc_session in TOC . Sessions )
{
var firstTrack = toc_session . Tracks [ 0 ] ;
var lastTrack = toc_session . Tracks [ toc_session . Tracks . Count - 1 ] ;
session . length_lba = lastTrack . Indexes [ 1 ] . lba + lastTrack . length_lba - firstTrack . Indexes [ 0 ] . lba ;
TOC . length_lba + = toc_session . length_lba ;
}
2011-05-08 09:07:46 +00:00
}
}
public class Cue
{
//TODO - export from isobuster and observe the SESSION directive, as well as the MSF directive.
public string DebugPrint ( )
{
StringBuilder sb = new StringBuilder ( ) ;
foreach ( CueFile cf in Files )
{
sb . AppendFormat ( "FILE \"{0}\"" , cf . Path ) ;
if ( cf . Binary ) sb . Append ( " BINARY" ) ;
sb . AppendLine ( ) ;
foreach ( CueTrack ct in cf . Tracks )
{
sb . AppendFormat ( " TRACK {0:D2} {1}\n" , ct . TrackNum , ct . TrackType . ToString ( ) . Replace ( "_" , "/" ) . ToUpper ( ) ) ;
foreach ( CueTrackIndex cti in ct . Indexes . Values )
{
sb . AppendFormat ( " INDEX {0:D2} {1}\n" , cti . IndexNum , cti . Timestamp . Value ) ;
}
}
}
return sb . ToString ( ) ;
}
public class CueFile
{
public string Path ;
public bool Binary ;
public List < CueTrack > Tracks = new List < CueTrack > ( ) ;
}
public List < CueFile > Files = new List < CueFile > ( ) ;
2011-06-20 09:09:21 +00:00
public static int BINSectorSizeForTrackType ( ETrackType type )
{
switch ( type )
{
case ETrackType . Mode1_2352 :
case ETrackType . Mode2_2352 :
case ETrackType . Audio :
return 2352 ;
case ETrackType . Mode1_2048 :
return 2048 ;
default :
throw new ArgumentOutOfRangeException ( ) ;
}
}
public static string TrackTypeStringForTrackType ( ETrackType type )
2011-05-08 09:07:46 +00:00
{
2011-06-20 09:09:21 +00:00
switch ( type )
{
case ETrackType . Mode1_2352 : return "MODE1/2352" ;
case ETrackType . Mode2_2352 : return "MODE2/2352" ;
case ETrackType . Audio : return "AUDIO" ;
case ETrackType . Mode1_2048 : return "MODE1/2048" ;
default :
throw new ArgumentOutOfRangeException ( ) ;
}
}
public static string RedumpTypeStringForTrackType ( ETrackType type )
{
switch ( type )
{
case ETrackType . Mode1_2352 : return "Data/Mode 1" ;
case ETrackType . Mode1_2048 : throw new InvalidOperationException ( "guh dunno what to put here" ) ;
case ETrackType . Mode2_2352 : return "Data/Mode 2" ;
case ETrackType . Audio : return "Audio" ;
default :
throw new ArgumentOutOfRangeException ( ) ;
}
2011-05-08 09:07:46 +00:00
}
public class CueTrack
{
2011-06-20 09:09:21 +00:00
public ETrackType TrackType ;
2011-05-08 09:07:46 +00:00
public int TrackNum ;
2011-06-20 09:09:21 +00:00
public CueTimestamp PreGap = new CueTimestamp ( ) ;
public CueTimestamp PostGap = new CueTimestamp ( ) ;
2011-05-08 09:07:46 +00:00
public Dictionary < int , CueTrackIndex > Indexes = new Dictionary < int , CueTrackIndex > ( ) ;
}
public class CueTimestamp
{
2011-06-20 09:09:21 +00:00
/// <summary>
/// creates timestamp of 00:00:00
/// </summary>
public CueTimestamp ( )
{
Value = "00:00:00" ;
}
/// <summary>
/// creates a timestamp from a string in the form mm:ss:ff
/// </summary>
2011-05-08 09:07:46 +00:00
public CueTimestamp ( string value ) {
this . Value = value ;
MIN = int . Parse ( value . Substring ( 0 , 2 ) ) ;
SEC = int . Parse ( value . Substring ( 3 , 2 ) ) ;
FRAC = int . Parse ( value . Substring ( 6 , 2 ) ) ;
LBA = MIN * 60 * 75 + SEC * 75 + FRAC ;
}
public readonly string Value ;
public readonly int MIN , SEC , FRAC , LBA ;
2011-06-20 09:09:21 +00:00
/// <summary>
/// creates timestamp from supplied LBA
/// </summary>
public CueTimestamp ( int LBA )
{
this . LBA = LBA ;
MIN = LBA / ( 60 * 75 ) ;
SEC = ( LBA / 75 ) % 60 ;
FRAC = LBA % 75 ;
Value = string . Format ( "{0:D2}:{1:D2}:{2:D2}" , MIN , SEC , FRAC ) ;
}
2011-05-08 09:07:46 +00:00
}
public class CueTrackIndex
{
2011-06-20 09:09:21 +00:00
public CueTrackIndex ( int num ) { IndexNum = num ; }
2011-05-08 09:07:46 +00:00
public int IndexNum ;
public CueTimestamp Timestamp ;
2011-06-20 09:09:21 +00:00
public int ZeroLBA ;
2011-05-08 09:07:46 +00:00
}
public class CueBrokenException : Exception
{
public CueBrokenException ( string why )
: base ( why )
{
}
}
public void LoadFromPath ( string cuePath )
{
FileInfo fiCue = new FileInfo ( cuePath ) ;
if ( ! fiCue . Exists ) throw new FileNotFoundException ( ) ;
File . ReadAllText ( cuePath ) ;
TextReader tr = new StreamReader ( cuePath ) ;
2011-06-20 09:09:21 +00:00
bool track_has_pregap = false ;
bool track_has_postgap = false ;
int last_index_num = - 1 ;
2011-05-08 09:07:46 +00:00
CueFile currFile = null ;
CueTrack currTrack = null ;
int state = 0 ;
for ( ; ; )
{
string line = tr . ReadLine ( ) ;
if ( line = = null ) break ;
if ( line = = "" ) continue ;
line = line . Trim ( ) ;
var clp = new CueLineParser ( line ) ;
string key = clp . ReadToken ( ) . ToUpper ( ) ;
switch ( key )
{
case "REM" :
break ;
case "FILE" :
{
currTrack = null ;
currFile = new CueFile ( ) ;
Files . Add ( currFile ) ;
currFile . Path = clp . ReadPath ( ) . Trim ( '"' ) ;
if ( ! clp . EOF )
{
string temp = clp . ReadToken ( ) . ToUpper ( ) ;
if ( temp = = "BINARY" )
currFile . Binary = true ;
}
break ;
}
case "TRACK" :
{
if ( currFile = = null ) throw new CueBrokenException ( "invalid cue structure" ) ;
if ( clp . EOF ) throw new CueBrokenException ( "invalid cue structure" ) ;
string strtracknum = clp . ReadToken ( ) ;
int tracknum ;
if ( ! int . TryParse ( strtracknum , out tracknum ) )
throw new CueBrokenException ( "malformed track number" ) ;
if ( clp . EOF ) throw new CueBrokenException ( "invalid cue structure" ) ;
2011-06-20 09:09:21 +00:00
if ( tracknum < 0 | | tracknum > 99 ) throw new CueBrokenException ( "`All track numbers must be between 1 and 99 inclusive.`" ) ;
2011-05-08 09:07:46 +00:00
string strtracktype = clp . ReadToken ( ) . ToUpper ( ) ;
currTrack = new CueTrack ( ) ;
switch ( strtracktype )
{
2011-06-20 09:09:21 +00:00
case "MODE1/2352" : currTrack . TrackType = ETrackType . Mode1_2352 ; break ;
case "MODE1/2048" : currTrack . TrackType = ETrackType . Mode1_2048 ; break ;
case "MODE2/2352" : currTrack . TrackType = ETrackType . Mode2_2352 ; break ;
case "AUDIO" : currTrack . TrackType = ETrackType . Audio ; break ;
2011-05-08 09:07:46 +00:00
default :
throw new CueBrokenException ( "unhandled track type" ) ;
}
currTrack . TrackNum = tracknum ;
currFile . Tracks . Add ( currTrack ) ;
2011-06-20 09:09:21 +00:00
track_has_pregap = false ;
track_has_postgap = false ;
last_index_num = - 1 ;
2011-05-08 09:07:46 +00:00
break ;
}
case "INDEX" :
{
if ( currTrack = = null ) throw new CueBrokenException ( "invalid cue structure" ) ;
if ( clp . EOF ) throw new CueBrokenException ( "invalid cue structure" ) ;
2011-06-20 09:09:21 +00:00
if ( track_has_postgap ) throw new CueBrokenException ( "`The POSTGAP command must appear after all INDEX commands for the current track.`" ) ;
2011-05-08 09:07:46 +00:00
string strindexnum = clp . ReadToken ( ) ;
int indexnum ;
if ( ! int . TryParse ( strindexnum , out indexnum ) )
throw new CueBrokenException ( "malformed index number" ) ;
if ( clp . EOF ) throw new CueBrokenException ( "invalid cue structure (missing index timestamp)" ) ;
string str_timestamp = clp . ReadToken ( ) ;
2011-06-20 09:09:21 +00:00
if ( indexnum < 0 | | indexnum > 99 ) throw new CueBrokenException ( "`All index numbers must be between 0 and 99 inclusive.`" ) ;
if ( indexnum ! = 1 & & indexnum ! = last_index_num + 1 ) throw new CueBrokenException ( "`The first index must be 0 or 1 with all other indexes being sequential to the first one.`" ) ;
last_index_num = indexnum ;
CueTrackIndex cti = new CueTrackIndex ( indexnum ) ;
cti . Timestamp = new CueTimestamp ( str_timestamp ) ;
2011-05-08 09:07:46 +00:00
cti . IndexNum = indexnum ;
currTrack . Indexes [ indexnum ] = cti ;
break ;
}
case "PREGAP" :
2011-06-20 09:09:21 +00:00
if ( track_has_pregap ) throw new CueBrokenException ( "`Only one PREGAP command is allowed per track.`" ) ;
if ( currTrack . Indexes . Count > 0 ) throw new CueBrokenException ( "`The PREGAP command must appear after a TRACK command, but before any INDEX commands.`" ) ;
currTrack . PreGap = new CueTimestamp ( clp . ReadToken ( ) ) ;
track_has_pregap = true ;
break ;
2011-05-08 09:07:46 +00:00
case "POSTGAP" :
2011-06-20 09:09:21 +00:00
if ( track_has_pregap ) throw new CueBrokenException ( "`Only one POSTGAP command is allowed per track.`" ) ;
track_has_postgap = true ;
currTrack . PostGap = new CueTimestamp ( clp . ReadToken ( ) ) ;
break ;
2011-05-08 09:07:46 +00:00
default :
throw new CueBrokenException ( "unsupported cue command: " + key ) ;
}
2011-06-20 09:09:21 +00:00
} //end cue parsing loop
2011-05-08 09:07:46 +00:00
}
class CueLineParser
{
int index ;
string str ;
public bool EOF ;
public CueLineParser ( string line )
{
this . str = line ;
}
public string ReadPath ( ) { return ReadToken ( true ) ; }
public string ReadToken ( ) { return ReadToken ( false ) ; }
public string ReadToken ( bool isPath )
{
if ( EOF ) return null ;
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 ;
}
return str . Substring ( startIndex , index - startIndex ) ;
}
}
}
}