BizHawk/BizHawk.Emulation/Disc/CUE_format.cs

416 lines
12 KiB
C#

using System;
using System.Text;
using System.Text.RegularExpressions;
using System.IO;
using System.Collections.Generic;
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);
int track_counter = 1;
int curr_lba = 0;
foreach (var cue_file in cue.Files)
{
//make a raw file blob for the source binfile
Blob_RawFile blob = new Blob_RawFile();
blob.PhysicalPath = Path.Combine(cueDir, cue_file.Path);
Blobs.Add(blob);
//structural validation
if (cue_file.Tracks.Count < 1) throw new Cue.CueBrokenException("Found a cue file with no tracks");
//structural validation: every track must be the same type with cue/bin (i think)
var trackType = cue_file.Tracks[0].TrackType;
for (int i = 1; i < cue_file.Tracks.Count; i++)
{
if (cue_file.Tracks[i].TrackType != trackType) throw new Cue.CueBrokenException("cue has different track types per datafile (not supported now; maybe never)");
}
//structural validaton: make sure file is a correct size and analyze its length
long flen = new FileInfo(blob.PhysicalPath).Length;
int numlba;
int leftover = 0;
switch (trackType)
{
case Cue.ECueTrackType.Audio:
numlba = (int)(flen / 2352);
leftover = (int)(flen - numlba * 2352);
break;
case Cue.ECueTrackType.Mode1_2352:
case Cue.ECueTrackType.Mode2_2352:
if (flen % 2352 != 0) throw new Cue.CueBrokenException("Found a modeN_2352 cue file that is wrongly-sized");
numlba = (int)(flen / 2352);
break;
case Cue.ECueTrackType.Mode1_2048:
if (flen % 2048 != 0) throw new Cue.CueBrokenException("Found a modeN_2048 cue file that is wrongly-sized");
numlba = (int)(flen / 2048);
break;
default: throw new InvalidOperationException();
}
List<DiscTOC.Track> new_toc_tracks = new List<DiscTOC.Track>();
for (int i = 0; i < cue_file.Tracks.Count; i++)
{
bool last_track = (i == cue_file.Tracks.Count - 1);
var cue_track = cue_file.Tracks[i];
if (cue_track.TrackNum != track_counter) throw new Cue.CueBrokenException("Missing a track in the middle of the cue");
track_counter++;
DiscTOC.Track toc_track = new DiscTOC.Track();
toc_track.num = track_counter;
session.Tracks.Add(toc_track);
new_toc_tracks.Add(toc_track);
//analyze indices
int idx;
for (idx = 0; idx <= 99; idx++)
{
if (!cue_track.Indexes.ContainsKey(idx))
{
if (idx == 0) continue;
if (idx == 1) throw new Cue.CueBrokenException("cue track is missing an index 1");
break;
}
var cue_index = cue_track.Indexes[idx];
//todo - add pregap/postgap from cue?
DiscTOC.Index toc_index = new DiscTOC.Index();
toc_index.num = idx;
toc_track.Indexes.Add(toc_index);
toc_index.lba = cue_index.Timestamp.LBA + curr_lba;
}
//look for extra indices (i.e. gaps)
for (; idx <= 99; idx++)
{
if (cue_track.Indexes.ContainsKey(idx))
throw new Cue.CueBrokenException("cue track is has an index gap");
}
} //track loop
//analyze length of each track and index
DiscTOC.Index last_toc_index = null;
foreach (var toc_track in new_toc_tracks)
{
foreach (var toc_index in toc_track.Indexes)
{
if (last_toc_index != null)
last_toc_index.length_lba = toc_index.lba - last_toc_index.lba;
last_toc_index = toc_index;
}
}
if (last_toc_index != null)
last_toc_index.length_lba = (curr_lba + numlba) - last_toc_index.lba;
//generate the sectors from this file
long curr_src_addr = 0;
for (int i = 0; i < numlba; i++)
{
ISector sector;
switch (trackType)
{
case Cue.ECueTrackType.Audio:
//all 2352 bytes are present
case Cue.ECueTrackType.Mode1_2352:
//2352 bytes are present, containing 2048 bytes of user data as well as ECM
case Cue.ECueTrackType.Mode2_2352:
//2352 bytes are present, containing 2336 bytes of user data, replacing ECM
{
Sector_RawBlob sector_rawblob = new Sector_RawBlob();
sector_rawblob.Blob = blob;
sector_rawblob.Offset = curr_src_addr;
curr_src_addr += 2352;
Sector_Raw sector_raw = new Sector_Raw();
sector_raw.BaseSector = sector_rawblob;
if (i == numlba - 1 && leftover != 0)
{
Sector_ZeroPad sector_zeropad = new Sector_ZeroPad();
sector_zeropad.BaseSector = sector_rawblob;
sector_zeropad.BaseLength = 2352 - leftover;
sector_raw.BaseSector = sector_zeropad;
}
sector = sector_raw;
break;
}
case Cue.ECueTrackType.Mode1_2048:
//2048 bytes are present. ECM needs to be generated to create a full sector
{
var sector_2048 = new Sector_Mode1_2048(i + 150);
sector_2048.Blob = new ECMCacheBlob(blob);
sector_2048.Offset = curr_src_addr;
curr_src_addr += 2048;
sector = sector_2048;
break;
}
default: throw new InvalidOperationException();
}
SectorEntry se = new SectorEntry();
se.Sector = sector;
Sectors.Add(se);
}
curr_lba += numlba;
} //done analyzing cue datafiles
TOC.AnalyzeLengthsFromIndexLengths();
}
}
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>();
public enum ECueTrackType
{
Mode1_2352,
Mode1_2048,
Mode2_2352,
Audio
}
public class CueTrack
{
public ECueTrackType TrackType;
public int TrackNum;
public Dictionary<int, CueTrackIndex> Indexes = new Dictionary<int, CueTrackIndex>();
}
public class CueTimestamp
{
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;
}
public class CueTrackIndex
{
public int IndexNum;
public CueTimestamp Timestamp;
}
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);
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");
string strtracktype = clp.ReadToken().ToUpper();
currTrack = new CueTrack();
switch (strtracktype)
{
case "MODE1/2352": currTrack.TrackType = ECueTrackType.Mode1_2352; break;
case "MODE1/2048": currTrack.TrackType = ECueTrackType.Mode1_2048; break;
case "MODE2/2352": currTrack.TrackType = ECueTrackType.Mode2_2352; break;
case "AUDIO": currTrack.TrackType = ECueTrackType.Audio; break;
default:
throw new CueBrokenException("unhandled track type");
}
currTrack.TrackNum = tracknum;
currFile.Tracks.Add(currTrack);
break;
}
case "INDEX":
{
if (currTrack == null) throw new CueBrokenException("invalid cue structure");
if (clp.EOF) throw new CueBrokenException("invalid cue structure");
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();
CueTrackIndex cti = new CueTrackIndex();
cti.Timestamp = new CueTimestamp(str_timestamp); ;
cti.IndexNum = indexnum;
currTrack.Indexes[indexnum] = cti;
break;
}
case "PREGAP":
case "POSTGAP":
throw new CueBrokenException("cue postgap/pregap command not supported yet");
default:
throw new CueBrokenException("unsupported cue command: " + key);
}
}
}
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);
}
}
}
}