mirror of
https://github.com/scummvm/scummvm.git
synced 2026-06-20 05:45:29 +00:00
330 lines
9.4 KiB
C++
330 lines
9.4 KiB
C++
/* ScummVM - Graphic Adventure Engine
|
|
*
|
|
* ScummVM is the legal property of its developers, whose names
|
|
* are too numerous to list here. Please refer to the COPYRIGHT
|
|
* file distributed with this source distribution.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
#include "common/debug.h"
|
|
#include "common/stream.h"
|
|
#include "common/formats/cue.h"
|
|
|
|
namespace Common {
|
|
|
|
enum {
|
|
kContextHeader,
|
|
kContextFiles,
|
|
kContextTracks,
|
|
};
|
|
|
|
static String nexttok(const char *s, const char **newP = nullptr) {
|
|
String res;
|
|
|
|
// Scan first non-whitespace
|
|
while (*s && (*s == ' ' || *s == '\t')) // If we see a whitespace
|
|
s++;
|
|
|
|
if (*s == '"') { // If it is a string then scan till end quote
|
|
s++; // skip the quote
|
|
|
|
while (*s && *s != '"')
|
|
res += *s++;
|
|
|
|
if (*s == '"')
|
|
s++;
|
|
} else {
|
|
while (*s && *s != ' ' && *s != '\t' && *s != '\n' && *s != '\r')
|
|
res += *s++;
|
|
}
|
|
|
|
if (newP)
|
|
*newP = s;
|
|
|
|
return res;
|
|
}
|
|
|
|
|
|
CueSheet::CueSheet(const char *sheet) {
|
|
parse(sheet);
|
|
}
|
|
|
|
CueSheet::CueSheet(SeekableReadStream *stream) {
|
|
int size = stream->size();
|
|
char *data = (char *)calloc(size + 1, 1); // null-terminated string
|
|
|
|
stream->read(data, size);
|
|
|
|
parse(data);
|
|
|
|
free(data);
|
|
}
|
|
|
|
void CueSheet::parse(const char *sheet) {
|
|
String line;
|
|
_context = kContextHeader;
|
|
const char *s = sheet;
|
|
|
|
while (*s) {
|
|
line.clear();
|
|
|
|
// Get a line
|
|
while (*s && *s != '\n' && *s != '\r') // If we see a whitespace
|
|
line += *s++;
|
|
|
|
// Skip any newlines and line feeds
|
|
while (*s == '\n' || *s == '\r') {
|
|
if (*s == '\n')
|
|
_lineNum++;
|
|
|
|
s++;
|
|
}
|
|
|
|
|
|
if (line.empty())
|
|
continue;
|
|
|
|
String firstToken = nexttok(line.c_str()); // Peek into comments
|
|
|
|
// Skip non-conformant comments
|
|
if (firstToken[0] == ';' || (firstToken.size() > 1 && firstToken[0] == '/' && firstToken[1] == '/'))
|
|
continue;
|
|
|
|
switch (_context) {
|
|
case kContextHeader:
|
|
parseHeaderContext(line.c_str());
|
|
break;
|
|
|
|
case kContextFiles:
|
|
parseFilesContext(line.c_str());
|
|
break;
|
|
|
|
case kContextTracks:
|
|
parseTracksContext(line.c_str());
|
|
break;
|
|
|
|
default:
|
|
error("CueSheet: bad context %d at line %d", _context, _lineNum);
|
|
}
|
|
}
|
|
}
|
|
|
|
struct CueSheet::LookupTable {
|
|
const char *key;
|
|
int value;
|
|
};
|
|
|
|
int CueSheet::lookupInTable(LookupTable *table, const char *key) {
|
|
while (table->key) {
|
|
if (!strcmp(key, table->key))
|
|
return table->value;
|
|
|
|
table++;
|
|
}
|
|
|
|
error("CueSheet::lookupInTable(): Unknown lookup token %s at line %d", key, _lineNum);
|
|
}
|
|
|
|
int CueSheet::parseMSF(const char *str) {
|
|
int min = 0, sec = 0, frm = 0;
|
|
|
|
if (sscanf(str, "%d:%d:%d", &min, &sec, &frm) != 3)
|
|
warning("Malformed MSF at line %d: %s", _lineNum, str);
|
|
|
|
return frm + 75 * (sec + 60 * min);
|
|
}
|
|
|
|
CueSheet::LookupTable fileTypes[] = {
|
|
{ "BINARY", CueSheet::kFileTypeBinary },
|
|
{ "AIFF", CueSheet::kFileTypeAIFF },
|
|
{ "WAVE", CueSheet::kFileTypeWave },
|
|
{ "MP3", CueSheet::kFileTypeMP3 },
|
|
{ "MOTOROLA", CueSheet::kFileTypeMotorola },
|
|
{ 0, 0 }
|
|
};
|
|
|
|
void CueSheet::parseHeaderContext(const char *line) {
|
|
const char *s = line;
|
|
|
|
String command = nexttok(s, &s);
|
|
|
|
if (command == "FILE") {
|
|
_files.push_back(CueFile());
|
|
|
|
_currentFile++;
|
|
|
|
_files[_currentFile].name = nexttok(s, &s);
|
|
|
|
String type = nexttok(s, &s);
|
|
|
|
_files[_currentFile].type = (FileType)lookupInTable(fileTypes, type.c_str());
|
|
|
|
_context = kContextFiles;
|
|
|
|
_currentTrack = -1;
|
|
|
|
debug(5, "File: %s, type: %s (%d)", _files[_currentFile].name.c_str(), type.c_str(), _files[_currentFile].type);
|
|
} else if (command == "TITLE") {
|
|
_metadata.title = nexttok(s, &s);
|
|
|
|
debug(5, "Title: %s", _metadata.title.c_str());
|
|
} else if (command == "PERFORMER") {
|
|
_metadata.performer = nexttok(s, &s);
|
|
|
|
debug(5, "Performer: %s", _metadata.performer.c_str());
|
|
} else if (command == "CATALOG") {
|
|
_metadata.catalog = nexttok(s, &s);
|
|
|
|
debug(5, "Catalog: %s", _metadata.catalog.c_str());
|
|
} else if (command == "REM") {
|
|
String subcommand = nexttok(s, &s);
|
|
|
|
if (subcommand == "DATE") {
|
|
_metadata.date = nexttok(s, &s);
|
|
debug(5, "Date: %s", _metadata.date.c_str());
|
|
} else if (subcommand == "GENRE") {
|
|
_metadata.genre = nexttok(s, &s);
|
|
debug(5, "Genre: %s", _metadata.genre.c_str());
|
|
} else if (subcommand == "COMMENT") {
|
|
debug(5, "Skipping Comment: %s", s);
|
|
} else {
|
|
warning("CueSheet: Unprocessed REM subcommand %s at line %d", subcommand.c_str(), _lineNum);
|
|
}
|
|
} else {
|
|
warning("CueSheet: Unprocessed command %s at line %d", command.c_str(), _lineNum);
|
|
}
|
|
}
|
|
|
|
CueSheet::LookupTable trackTypes[] = {
|
|
{ "AUDIO", CueSheet::kTrackTypeAudio }, // Audio (sector size: 2352)
|
|
{ "CDG", CueSheet::kTrackTypeCDG }, // Karaoke CD+G (sector size: 2448)
|
|
{ "MODE1_RAW", CueSheet::kTrackTypeMode1_Raw }, // CD-ROM Mode 1 data (raw) (sector size: 2352), used by cdrdao
|
|
{ "MODE1/2048", CueSheet::kTrackTypeMode1_2048 }, // CD-ROM Mode 1 data (cooked) (sector size: 2048)
|
|
{ "MODE1/2352", CueSheet::kTrackTypeMode1_2352 }, // CD-ROM Mode 1 data (raw) (sector size: 2352)
|
|
{ "MODE2_RAW", CueSheet::kTrackTypeMode2_Raw }, // CD-ROM Mode 2 data (raw) (sector size: 2352), used by cdrdao
|
|
{ "MODE2/2048", CueSheet::kTrackTypeMode2_2048 }, // CD-ROM Mode 2 XA form-1 data (sector size: 2048)
|
|
{ "MODE2/2324", CueSheet::kTrackTypeMode2_2324 }, // CD-ROM Mode 2 XA form-2 data (sector size: 2324)
|
|
{ "MODE2/2336", CueSheet::kTrackTypeMode2_2366 }, // CD-ROM Mode 2 data (sector size: 2336)
|
|
{ "MODE2/2352", CueSheet::kTrackTypeMode2_2352 }, // CD-ROM Mode 2 data (raw) (sector size: 2352)
|
|
{ "CDI/2336", CueSheet::kTrackTypeCDI_2336 }, // CDI Mode 2 data
|
|
{ "CDI/2352", CueSheet::kTrackTypeCDI_2352 }, // CDI Mode 2 data
|
|
{ 0, 0 }
|
|
};
|
|
|
|
CueSheet::LookupTable trackTypesSectorSizes[] = {
|
|
{ "AUDIO", 2352 },
|
|
{ "CDG", 2448 },
|
|
{ "MODE1_RAW", 2352 },
|
|
{ "MODE1/2048", 2048 },
|
|
{ "MODE1/2352", 2352 },
|
|
{ "MODE2_RAW", 2352 },
|
|
{ "MODE2/2048", 2048 },
|
|
{ "MODE2/2324", 2324 },
|
|
{ "MODE2/2336", 2336 },
|
|
{ "MODE2/2352", 2352 },
|
|
{ "CDI/2336", 2336 },
|
|
{ "CDI/2352", 2352 },
|
|
{ 0, 0 }
|
|
};
|
|
|
|
void CueSheet::parseFilesContext(const char *line) {
|
|
const char *s = line;
|
|
|
|
String command = nexttok(s, &s);
|
|
|
|
if (command == "TRACK") {
|
|
int trackNum = atoi(nexttok(s, &s).c_str());
|
|
String trackType = nexttok(s, &s);
|
|
|
|
if (trackNum < 0 || (_currentTrack > 0 && _currentTrack + 1 != trackNum)) {
|
|
warning("CueSheet: Incorrect track number. Expected %d but got %d at line %d", _currentTrack + 1, trackNum, _lineNum);
|
|
} else {
|
|
for (int i = (int)_files[_currentFile].tracks.size(); i <= trackNum; i++)
|
|
_files[_currentFile].tracks.push_back(CueTrack());
|
|
|
|
_currentTrack = trackNum;
|
|
_files[_currentFile].tracks[_currentTrack].type = (TrackType)lookupInTable(trackTypes, trackType.c_str());
|
|
_files[_currentFile].tracks[_currentTrack].size = lookupInTable(trackTypesSectorSizes, trackType.c_str());
|
|
|
|
debug(5, "Track: %d type: %s (%d)", trackNum, trackType.c_str(), _files[_currentFile].tracks[_currentTrack].type);
|
|
}
|
|
|
|
_context = kContextTracks;
|
|
} else {
|
|
warning("CueSheet: Unprocessed file command %s at line %d", command.c_str(), _lineNum);
|
|
}
|
|
|
|
}
|
|
|
|
CueSheet::LookupTable trackFlags[] = {
|
|
{ "4CH", CueSheet::kTrackFlag4ch },
|
|
{ "DCP", CueSheet::kTrackFlagDCP },
|
|
{ "PRE", CueSheet::kTrackFlagPre },
|
|
{ "SCMS", CueSheet::kTrackFlagSCMS },
|
|
{ 0, 0 }
|
|
};
|
|
|
|
void CueSheet::parseTracksContext(const char *line) {
|
|
const char *s = line;
|
|
|
|
String command = nexttok(s, &s);
|
|
|
|
if (command == "TRACK") {
|
|
parseFilesContext(line);
|
|
} else if (command == "TITLE") {
|
|
_files[_currentFile].tracks[_currentTrack].title = nexttok(s, &s);
|
|
|
|
debug(5, "Track title: %s", _files[_currentFile].tracks[_currentTrack].title.c_str());
|
|
} else if (command == "INDEX") {
|
|
int indexNum = atoi(nexttok(s, &s).c_str());
|
|
int frames = parseMSF(nexttok(s, &s).c_str());
|
|
|
|
for (int i = (int)_files[_currentFile].tracks[_currentTrack].indices.size(); i <= indexNum; i++)
|
|
_files[_currentFile].tracks[_currentTrack].indices.push_back(0);
|
|
|
|
_files[_currentFile].tracks[_currentTrack].indices[indexNum] = frames;
|
|
|
|
debug(5, "Index: %d, frames: %d", indexNum, frames);
|
|
} else if (command == "PREGAP") {
|
|
_files[_currentFile].tracks[_currentTrack].pregap = parseMSF(nexttok(s, &s).c_str());
|
|
|
|
debug(5, "Track pregap: %d", _files[_currentFile].tracks[_currentTrack].pregap);
|
|
} else if (command == "FLAGS") {
|
|
String flag;
|
|
uint32 flags = 0;
|
|
|
|
while (*s) {
|
|
flag = nexttok(s, &s);
|
|
|
|
flags |= lookupInTable(trackFlags, flag.c_str());
|
|
}
|
|
|
|
_files[_currentFile].tracks[_currentTrack].flags = flags;
|
|
|
|
debug(5, "Track flags: %d", _files[_currentFile].tracks[_currentTrack].flags);
|
|
} else if (command == "FILE") {
|
|
parseHeaderContext(line);
|
|
} else if (command == "PERFORMER") {
|
|
_files[_currentFile].tracks[_currentTrack].performer = nexttok(s, &s);
|
|
|
|
debug(5, "Track performer: %s", _files[_currentFile].tracks[_currentTrack].performer.c_str());
|
|
} else {
|
|
warning("CueSheet: Unprocessed track command %s at line %d", command.c_str(), _lineNum);
|
|
}
|
|
}
|
|
|
|
} // End of namespace Common
|