2015-06-23 18:57:11 +00:00
|
|
|
//TODO - object initialization syntax cleanup
|
|
|
|
|
|
|
|
using System;
|
|
|
|
using System.Linq;
|
|
|
|
using System.Text;
|
2015-09-16 19:37:42 +00:00
|
|
|
using System.Text.RegularExpressions;
|
2015-06-23 18:57:11 +00:00
|
|
|
using System.IO;
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
|
|
//http://digitalx.org/cue-sheet/index.html "all cue sheet information is a straight 1:1 copy from the cdrwin helpfile"
|
|
|
|
//http://www.gnu.org/software/libcdio/libcdio.html#Sectors
|
|
|
|
//this is actually a great reference. they use LSN instead of LBA.. maybe a good idea for us
|
|
|
|
|
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>
|
|
|
|
/// Performs minimum parse processing on a cue file
|
|
|
|
/// </summary>
|
|
|
|
class ParseCueJob : DiscJob
|
2015-06-23 18:57:11 +00:00
|
|
|
{
|
|
|
|
/// <summary>
|
2015-07-12 22:45:20 +00:00
|
|
|
/// input: the cue string to parse
|
2015-06-23 18:57:11 +00:00
|
|
|
/// </summary>
|
2015-07-12 22:45:20 +00:00
|
|
|
public string IN_CueString;
|
2015-06-23 18:57:11 +00:00
|
|
|
|
2015-07-12 22:45:20 +00:00
|
|
|
/// <summary>
|
|
|
|
/// output: the resulting minimally-processed cue file
|
|
|
|
/// </summary>
|
|
|
|
public CUE_File OUT_CueFile;
|
2015-06-23 18:57:11 +00:00
|
|
|
|
2015-09-16 19:37:42 +00:00
|
|
|
/// <summary>
|
|
|
|
/// Indicates whether parsing will be strict or lenient
|
|
|
|
/// </summary>
|
|
|
|
public bool IN_Strict = false;
|
|
|
|
|
2015-06-23 18:57:11 +00:00
|
|
|
|
2015-07-12 22:45:20 +00:00
|
|
|
class CueLineParser
|
|
|
|
{
|
|
|
|
int index;
|
|
|
|
string str;
|
|
|
|
public bool EOF;
|
|
|
|
public CueLineParser(string line)
|
2015-06-23 18:57:11 +00:00
|
|
|
{
|
2015-07-12 22:45:20 +00:00
|
|
|
str = line;
|
2015-06-23 18:57:11 +00:00
|
|
|
}
|
|
|
|
|
2015-07-12 22:45:20 +00:00
|
|
|
public string ReadPath() { return ReadToken(Mode.Quotable); }
|
|
|
|
public string ReadToken() { return ReadToken(Mode.Normal); }
|
|
|
|
public string ReadLine()
|
2015-06-23 18:57:11 +00:00
|
|
|
{
|
2015-07-12 22:45:20 +00:00
|
|
|
int len = str.Length;
|
|
|
|
string ret = str.Substring(index, len - index);
|
|
|
|
index = len;
|
|
|
|
EOF = true;
|
|
|
|
return ret;
|
2015-06-23 18:57:11 +00:00
|
|
|
}
|
|
|
|
|
2015-07-12 22:45:20 +00:00
|
|
|
enum Mode
|
2015-06-23 18:57:11 +00:00
|
|
|
{
|
2015-07-12 22:45:20 +00:00
|
|
|
Normal, Quotable
|
2015-06-23 18:57:11 +00:00
|
|
|
}
|
|
|
|
|
2015-07-12 22:45:20 +00:00
|
|
|
string ReadToken(Mode mode)
|
2015-06-23 18:57:11 +00:00
|
|
|
{
|
2015-07-12 22:45:20 +00:00
|
|
|
if (EOF) return null;
|
2015-06-23 18:57:11 +00:00
|
|
|
|
2015-07-12 22:45:20 +00:00
|
|
|
bool isPath = mode == Mode.Quotable;
|
2015-06-23 18:57:11 +00:00
|
|
|
|
2015-07-12 22:45:20 +00:00
|
|
|
int startIndex = index;
|
|
|
|
bool inToken = false;
|
|
|
|
bool inQuote = false;
|
|
|
|
for (; ; )
|
2015-06-23 18:57:11 +00:00
|
|
|
{
|
2015-07-12 22:45:20 +00:00
|
|
|
bool done = false;
|
|
|
|
char c = str[index];
|
|
|
|
bool isWhiteSpace = (c == ' ' || c == '\t');
|
2015-06-23 18:57:11 +00:00
|
|
|
|
2015-07-12 22:45:20 +00:00
|
|
|
if (isWhiteSpace)
|
2015-06-23 18:57:11 +00:00
|
|
|
{
|
2015-07-12 22:45:20 +00:00
|
|
|
if (inQuote)
|
|
|
|
index++;
|
|
|
|
else
|
2015-06-23 18:57:11 +00:00
|
|
|
{
|
2015-07-12 22:45:20 +00:00
|
|
|
if (inToken)
|
|
|
|
done = true;
|
2015-06-23 18:57:11 +00:00
|
|
|
else
|
2015-07-12 22:45:20 +00:00
|
|
|
index++;
|
2015-06-23 18:57:11 +00:00
|
|
|
}
|
2015-07-12 22:45:20 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
bool startedQuote = false;
|
|
|
|
if (!inToken)
|
2015-06-23 18:57:11 +00:00
|
|
|
{
|
2015-07-12 22:45:20 +00:00
|
|
|
startIndex = index;
|
|
|
|
if (isPath && c == '"')
|
|
|
|
startedQuote = inQuote = true;
|
|
|
|
inToken = true;
|
2015-06-23 18:57:11 +00:00
|
|
|
}
|
2015-07-12 22:45:20 +00:00
|
|
|
switch (str[index])
|
2015-06-23 18:57:11 +00:00
|
|
|
{
|
2015-07-12 22:45:20 +00:00
|
|
|
case '"':
|
|
|
|
index++;
|
|
|
|
if (inQuote && !startedQuote)
|
|
|
|
{
|
|
|
|
done = true;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case '\\':
|
|
|
|
index++;
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
index++;
|
|
|
|
break;
|
2015-06-23 18:57:11 +00:00
|
|
|
}
|
|
|
|
}
|
2015-07-12 22:45:20 +00:00
|
|
|
if (index == str.Length)
|
|
|
|
{
|
|
|
|
EOF = true;
|
|
|
|
done = true;
|
|
|
|
}
|
|
|
|
if (done) break;
|
|
|
|
}
|
2015-06-23 18:57:11 +00:00
|
|
|
|
2015-07-12 22:45:20 +00:00
|
|
|
string ret = str.Substring(startIndex, index - startIndex);
|
2015-06-23 18:57:11 +00:00
|
|
|
|
2015-07-12 22:45:20 +00:00
|
|
|
if (mode == Mode.Quotable)
|
|
|
|
ret = ret.Trim('"');
|
2015-06-23 18:57:11 +00:00
|
|
|
|
2015-07-12 22:45:20 +00:00
|
|
|
return ret;
|
2015-06-23 18:57:11 +00:00
|
|
|
}
|
2015-07-12 22:45:20 +00:00
|
|
|
}
|
2015-06-23 18:57:11 +00:00
|
|
|
|
2015-07-12 22:45:20 +00:00
|
|
|
void LoadFromString(ParseCueJob job)
|
|
|
|
{
|
|
|
|
string cueString = job.IN_CueString;
|
|
|
|
TextReader tr = new StringReader(cueString);
|
2015-06-23 18:57:11 +00:00
|
|
|
|
2015-07-12 22:45:20 +00:00
|
|
|
for (; ; )
|
|
|
|
{
|
|
|
|
job.CurrentLine++;
|
|
|
|
string line = tr.ReadLine();
|
|
|
|
if (line == null) break;
|
|
|
|
line = line.Trim();
|
|
|
|
if (line == "") continue;
|
|
|
|
var clp = new CueLineParser(line);
|
|
|
|
|
|
|
|
string key = clp.ReadToken().ToUpperInvariant();
|
2015-09-16 19:37:42 +00:00
|
|
|
|
|
|
|
//remove nonsense at beginning
|
|
|
|
if (!IN_Strict)
|
|
|
|
{
|
|
|
|
while (key.Length > 0)
|
|
|
|
{
|
|
|
|
char c = key[0];
|
|
|
|
if(c == ';') break;
|
|
|
|
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) break;
|
|
|
|
key = key.Substring(1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool startsWithSemicolon = key.StartsWith(";");
|
|
|
|
|
|
|
|
if (startsWithSemicolon)
|
2015-06-23 18:57:11 +00:00
|
|
|
{
|
2015-07-12 22:45:20 +00:00
|
|
|
clp.EOF = true;
|
|
|
|
OUT_CueFile.Commands.Add(new CUE_File.Command.COMMENT() { Value = line });
|
|
|
|
}
|
|
|
|
else switch (key)
|
|
|
|
{
|
|
|
|
default:
|
|
|
|
job.Warn("Unknown command: " + key);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "CATALOG":
|
|
|
|
if (OUT_CueFile.GlobalDiscInfo.Catalog != null)
|
|
|
|
job.Warn("Multiple CATALOG commands detected. Subsequent ones are ignored.");
|
|
|
|
else if (clp.EOF)
|
|
|
|
job.Warn("Ignoring empty CATALOG command");
|
|
|
|
else OUT_CueFile.Commands.Add(OUT_CueFile.GlobalDiscInfo.Catalog = new CUE_File.Command.CATALOG() { Value = clp.ReadToken() });
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "CDTEXTFILE":
|
|
|
|
if (OUT_CueFile.GlobalDiscInfo.CDTextFile != null)
|
|
|
|
job.Warn("Multiple CDTEXTFILE commands detected. Subsequent ones are ignored.");
|
|
|
|
else if (clp.EOF)
|
|
|
|
job.Warn("Ignoring empty CDTEXTFILE command");
|
|
|
|
else OUT_CueFile.Commands.Add(OUT_CueFile.GlobalDiscInfo.CDTextFile = new CUE_File.Command.CDTEXTFILE() { Path = clp.ReadPath() });
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "FILE":
|
|
|
|
{
|
|
|
|
var path = clp.ReadPath();
|
|
|
|
CueFileType ft;
|
|
|
|
if (clp.EOF)
|
2015-06-23 18:57:11 +00:00
|
|
|
{
|
2015-07-12 22:45:20 +00:00
|
|
|
job.Error("FILE command is missing file type.");
|
|
|
|
ft = CueFileType.Unspecified;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
var strType = clp.ReadToken().ToUpperInvariant();
|
|
|
|
switch (strType)
|
2015-06-23 18:57:11 +00:00
|
|
|
{
|
2015-07-12 22:45:20 +00:00
|
|
|
default:
|
|
|
|
job.Error("Unknown FILE type: " + strType);
|
|
|
|
ft = CueFileType.Unspecified;
|
|
|
|
break;
|
|
|
|
case "BINARY": ft = CueFileType.BINARY; break;
|
|
|
|
case "MOTOROLA": ft = CueFileType.MOTOROLA; break;
|
|
|
|
case "BINARAIFF": ft = CueFileType.AIFF; break;
|
|
|
|
case "WAVE": ft = CueFileType.WAVE; break;
|
|
|
|
case "MP3": ft = CueFileType.MP3; break;
|
2015-06-23 18:57:11 +00:00
|
|
|
}
|
|
|
|
}
|
2015-07-12 22:45:20 +00:00
|
|
|
OUT_CueFile.Commands.Add(new CUE_File.Command.FILE() { Path = path, Type = ft });
|
|
|
|
}
|
|
|
|
break;
|
2015-06-23 18:57:11 +00:00
|
|
|
|
2015-07-12 22:45:20 +00:00
|
|
|
case "FLAGS":
|
|
|
|
{
|
|
|
|
var cmd = new CUE_File.Command.FLAGS();
|
|
|
|
OUT_CueFile.Commands.Add(cmd);
|
|
|
|
while (!clp.EOF)
|
2015-06-23 18:57:11 +00:00
|
|
|
{
|
2015-07-12 22:45:20 +00:00
|
|
|
var flag = clp.ReadToken().ToUpperInvariant();
|
|
|
|
switch (flag)
|
2015-06-23 18:57:11 +00:00
|
|
|
{
|
2015-07-12 22:45:20 +00:00
|
|
|
case "DATA":
|
|
|
|
default:
|
|
|
|
job.Warn("Unknown FLAG: " + flag);
|
|
|
|
break;
|
|
|
|
case "DCP": cmd.Flags |= CueTrackFlags.DCP; break;
|
|
|
|
case "4CH": cmd.Flags |= CueTrackFlags._4CH; break;
|
|
|
|
case "PRE": cmd.Flags |= CueTrackFlags.PRE; break;
|
|
|
|
case "SCMS": cmd.Flags |= CueTrackFlags.SCMS; break;
|
2015-06-23 18:57:11 +00:00
|
|
|
}
|
|
|
|
}
|
2015-07-12 22:45:20 +00:00
|
|
|
if (cmd.Flags == CueTrackFlags.None)
|
|
|
|
job.Warn("Empty FLAG command");
|
|
|
|
}
|
|
|
|
break;
|
2015-06-23 18:57:11 +00:00
|
|
|
|
2015-07-12 22:45:20 +00:00
|
|
|
case "INDEX":
|
|
|
|
{
|
|
|
|
if (clp.EOF)
|
2015-06-23 18:57:11 +00:00
|
|
|
{
|
2015-07-12 22:45:20 +00:00
|
|
|
job.Error("Incomplete INDEX command");
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
string strindexnum = clp.ReadToken();
|
|
|
|
int indexnum;
|
|
|
|
if (!int.TryParse(strindexnum, out indexnum) || indexnum < 0 || indexnum > 99)
|
|
|
|
{
|
|
|
|
job.Error("Invalid INDEX number: " + strindexnum);
|
|
|
|
break;
|
2015-06-23 18:57:11 +00:00
|
|
|
}
|
2015-07-12 22:45:20 +00:00
|
|
|
string str_timestamp = clp.ReadToken();
|
|
|
|
var ts = new Timestamp(str_timestamp);
|
2015-09-16 19:37:42 +00:00
|
|
|
if (!ts.Valid && !IN_Strict)
|
|
|
|
{
|
|
|
|
//try cleaning it up
|
|
|
|
str_timestamp = Regex.Replace(str_timestamp, "[^0-9:]", "");
|
|
|
|
ts = new Timestamp(str_timestamp);
|
|
|
|
}
|
2015-07-12 22:45:20 +00:00
|
|
|
if (!ts.Valid)
|
|
|
|
{
|
2015-09-16 19:37:42 +00:00
|
|
|
if (IN_Strict)
|
|
|
|
job.Error("Invalid INDEX timestamp: " + str_timestamp);
|
2015-07-12 22:45:20 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
OUT_CueFile.Commands.Add(new CUE_File.Command.INDEX() { Number = indexnum, Timestamp = ts });
|
|
|
|
}
|
|
|
|
break;
|
2015-06-23 18:57:11 +00:00
|
|
|
|
2015-07-12 22:45:20 +00:00
|
|
|
case "ISRC":
|
|
|
|
if (OUT_CueFile.GlobalDiscInfo.ISRC != null)
|
|
|
|
job.Warn("Multiple ISRC commands detected. Subsequent ones are ignored.");
|
|
|
|
else if (clp.EOF)
|
|
|
|
job.Warn("Ignoring empty ISRC command");
|
|
|
|
else
|
|
|
|
{
|
|
|
|
var isrc = clp.ReadToken();
|
|
|
|
if (isrc.Length != 12)
|
|
|
|
job.Warn("Invalid ISRC code ignored: " + isrc);
|
2015-06-23 18:57:11 +00:00
|
|
|
else
|
|
|
|
{
|
2015-07-12 22:45:20 +00:00
|
|
|
OUT_CueFile.Commands.Add(OUT_CueFile.GlobalDiscInfo.ISRC = new CUE_File.Command.ISRC() { Value = isrc });
|
2015-06-23 18:57:11 +00:00
|
|
|
}
|
2015-07-12 22:45:20 +00:00
|
|
|
}
|
|
|
|
break;
|
2015-06-23 18:57:11 +00:00
|
|
|
|
2015-07-12 22:45:20 +00:00
|
|
|
case "PERFORMER":
|
|
|
|
OUT_CueFile.Commands.Add(new CUE_File.Command.PERFORMER() { Value = clp.ReadPath() ?? "" });
|
|
|
|
break;
|
2015-06-23 18:57:11 +00:00
|
|
|
|
2015-07-12 22:45:20 +00:00
|
|
|
case "POSTGAP":
|
|
|
|
case "PREGAP":
|
|
|
|
{
|
|
|
|
var str_msf = clp.ReadToken();
|
|
|
|
var msf = new Timestamp(str_msf);
|
|
|
|
if (!msf.Valid)
|
|
|
|
job.Error("Ignoring {0} with invalid length MSF: " + str_msf, key);
|
|
|
|
else
|
2015-06-23 18:57:11 +00:00
|
|
|
{
|
2015-07-12 22:45:20 +00:00
|
|
|
if (key == "POSTGAP")
|
|
|
|
OUT_CueFile.Commands.Add(new CUE_File.Command.POSTGAP() { Length = msf });
|
2015-06-23 18:57:11 +00:00
|
|
|
else
|
2015-07-12 22:45:20 +00:00
|
|
|
OUT_CueFile.Commands.Add(new CUE_File.Command.PREGAP() { Length = msf });
|
2015-06-23 18:57:11 +00:00
|
|
|
}
|
2015-07-12 22:45:20 +00:00
|
|
|
}
|
|
|
|
break;
|
2015-06-23 18:57:11 +00:00
|
|
|
|
2015-07-12 22:45:20 +00:00
|
|
|
case "REM":
|
|
|
|
OUT_CueFile.Commands.Add(new CUE_File.Command.REM() { Value = clp.ReadLine() });
|
|
|
|
break;
|
2015-06-23 18:57:11 +00:00
|
|
|
|
2015-07-12 22:45:20 +00:00
|
|
|
case "SONGWRITER":
|
|
|
|
OUT_CueFile.Commands.Add(new CUE_File.Command.SONGWRITER() { Value = clp.ReadPath() ?? "" });
|
|
|
|
break;
|
2015-06-23 18:57:11 +00:00
|
|
|
|
2015-07-12 22:45:20 +00:00
|
|
|
case "TITLE":
|
|
|
|
OUT_CueFile.Commands.Add(new CUE_File.Command.TITLE() { Value = clp.ReadPath() ?? "" });
|
|
|
|
break;
|
2015-06-23 18:57:11 +00:00
|
|
|
|
2015-07-12 22:45:20 +00:00
|
|
|
case "TRACK":
|
|
|
|
{
|
|
|
|
if (clp.EOF)
|
2015-06-23 18:57:11 +00:00
|
|
|
{
|
2015-07-12 22:45:20 +00:00
|
|
|
job.Error("Incomplete TRACK command");
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
string str_tracknum = clp.ReadToken();
|
|
|
|
int tracknum;
|
|
|
|
if (!int.TryParse(str_tracknum, out tracknum) || tracknum < 1 || tracknum > 99)
|
|
|
|
{
|
|
|
|
job.Error("Invalid TRACK number: " + str_tracknum);
|
|
|
|
break;
|
|
|
|
}
|
2015-06-23 18:57:11 +00:00
|
|
|
|
2015-07-12 22:45:20 +00:00
|
|
|
//TODO - check sequentiality? maybe as a warning
|
2015-06-23 18:57:11 +00:00
|
|
|
|
2015-07-12 22:45:20 +00:00
|
|
|
CueTrackType tt;
|
|
|
|
var str_trackType = clp.ReadToken();
|
|
|
|
switch (str_trackType.ToUpperInvariant())
|
|
|
|
{
|
|
|
|
default:
|
|
|
|
job.Error("Unknown TRACK type: " + str_trackType);
|
|
|
|
tt = CueTrackType.Unknown;
|
|
|
|
break;
|
|
|
|
case "AUDIO": tt = CueTrackType.Audio; break;
|
|
|
|
case "CDG": tt = CueTrackType.CDG; break;
|
|
|
|
case "MODE1/2048": tt = CueTrackType.Mode1_2048; break;
|
|
|
|
case "MODE1/2352": tt = CueTrackType.Mode1_2352; break;
|
|
|
|
case "MODE2/2336": tt = CueTrackType.Mode2_2336; break;
|
|
|
|
case "MODE2/2352": tt = CueTrackType.Mode2_2352; break;
|
|
|
|
case "CDI/2336": tt = CueTrackType.CDI_2336; break;
|
|
|
|
case "CDI/2352": tt = CueTrackType.CDI_2352; break;
|
2015-06-23 18:57:11 +00:00
|
|
|
}
|
|
|
|
|
2015-07-12 22:45:20 +00:00
|
|
|
OUT_CueFile.Commands.Add(new CUE_File.Command.TRACK() { Number = tracknum, Type = tt });
|
2015-06-23 18:57:11 +00:00
|
|
|
}
|
2015-07-12 22:45:20 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!clp.EOF)
|
|
|
|
{
|
|
|
|
var remainder = clp.ReadLine();
|
|
|
|
if (remainder.TrimStart().StartsWith(";"))
|
|
|
|
{
|
|
|
|
//add a comment
|
|
|
|
OUT_CueFile.Commands.Add(new CUE_File.Command.COMMENT() { Value = remainder });
|
2015-06-23 18:57:11 +00:00
|
|
|
}
|
2015-07-12 22:45:20 +00:00
|
|
|
else job.Warn("Unknown text at end of line after processing command: " + key);
|
|
|
|
}
|
2015-06-23 18:57:11 +00:00
|
|
|
|
2015-07-12 22:45:20 +00:00
|
|
|
} //end cue parsing loop
|
2015-06-23 18:57:11 +00:00
|
|
|
|
2015-07-12 22:45:20 +00:00
|
|
|
job.FinishLog();
|
|
|
|
} //LoadFromString
|
2015-06-23 18:57:11 +00:00
|
|
|
|
2015-07-12 22:45:20 +00:00
|
|
|
public void Run(ParseCueJob job)
|
2015-06-23 18:57:11 +00:00
|
|
|
{
|
2015-07-12 22:45:20 +00:00
|
|
|
job.OUT_CueFile = new CUE_File();
|
|
|
|
job.LoadFromString(job);
|
2015-06-23 18:57:11 +00:00
|
|
|
}
|
2015-07-12 22:45:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-06-23 18:57:11 +00:00
|
|
|
|
|
|
|
} //namespace
|