/* Copyright 2019 flyinghead This file is part of reicast. reicast 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 2 of the License, or (at your option) any later version. reicast 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 reicast. If not, see . */ #include "common.h" #include "stdclass.h" #include "oslib/storage.h" #include "oslib/i18n.h" #include static u32 getSectorSize(const std::string& type) { if (type == "AUDIO") return 2352; // PCM Audio else if (type == "CDG") return 2352; // Karaoke cd+g else if (type == "MODE1/2048") return 2048; // CDROM Mode1 Data (cooked) else if (type == "MODE1/2352") return 2352; // CDROM Mode1 Data (raw) else if (type == "MODE2/2336") return 2336; // CDROM XA Mode2 Data else if (type == "MODE2/2352") return 2352; // CDROM XA Mode2 Data else if (type == "CDI/2336") return 2336; // CDI Mode2 Data else if (type == "CDI/2352") return 2352; // CDI Mode2 Data else return 0; } Disc* cue_parse(const char* file, std::vector *digest) { if (get_file_extension(file) != "cue") return nullptr; hostfs::File *fsource = hostfs::storage().openFile(file, "rb"); if (fsource == nullptr) { WARN_LOG(COMMON, "Cannot open file '%s' errno %d", file, errno); throw FlycastException(strprintf(i18n::T("Cannot open CUE file %s"), file)); } hostfs::FileInfo fileInfo = hostfs::storage().getFileInfo(file); size_t cue_len = fileInfo.size; char cue_data[64_KB] = { 0 }; if (cue_len >= sizeof(cue_data)) { delete fsource; throw FlycastException(i18n::Ts("CUE parse error: CUE file too big")); } if (fsource->read(cue_data, 1, cue_len) != cue_len) WARN_LOG(GDROM, "Failed or truncated read of cue file '%s'", file); delete fsource; std::istringstream istream(cue_data); istream.imbue(std::locale::classic()); std::string basepath = hostfs::storage().getParentPath(file); MD5Sum md5; std::unique_ptr disc = std::make_unique(); u32 currentFAD = 150; // SESSION context u32 session_number = 0; bool firstTrackOfSession = false; // FILE context std::string track_filename; u32 fileStartFAD = 0; // TRACK context u32 track_number = -1; std::string track_type; u32 track_secsize = 0; std::string track_isrc; u32 pregap = 0; std::string line; while (std::getline(istream, line)) { std::istringstream cuesheet(line); cuesheet.imbue(std::locale::classic()); std::string token; cuesheet >> token; if (token == "REM") { cuesheet >> token; if (token == "SESSION") { // Multi-session u32 cur_session; cuesheet >> cur_session; if (cur_session == 0) WARN_LOG(GDROM, "CUE parse error: invalid session number %s", token.c_str()); else if (cur_session != session_number) { session_number = cur_session; if (session_number == 2) { // session 1 lead-out: 01:30:00, session 2 lead-in: 01:00:00, pregap: 00:02:00 currentFAD += 6750 + 4500 + 150; firstTrackOfSession = true; } Session ses; ses.FirstTrack = (u8)disc->tracks.size() + 1; ses.StartFAD = currentFAD; disc->sessions.push_back(ses); DEBUG_LOG(GDROM, "session[%zd]: 1st track: %d FAD:%d", disc->sessions.size(), ses.FirstTrack, ses.StartFAD); } } else { // GD-Rom if (token == "HIGH-DENSITY") { currentFAD = 45000 + 150; disc->type = GdRom; } else if (token != "SINGLE-DENSITY") { INFO_LOG(GDROM, "CUE parse: unrecognized REM token %s. Expected SINGLE-DENSITY, HIGH-DENSITY or SESSION", token.c_str()); continue; } cuesheet >> token; if (token != "AREA") WARN_LOG(GDROM, "CUE parse error: unrecognized REM token %s. Assuming AREA", token.c_str()); } // Clear file context track_filename.clear(); fileStartFAD = 0; // Clear track context track_number = -1; track_type.clear(); track_isrc.clear(); track_secsize = 0; } else if (token == "FILE") { track_filename.clear(); char last; do { if (!(cuesheet >> last)) throw FlycastException(i18n::T("Invalid CUE file")); } while (isspace((unsigned char)last)); if (last == '"') { cuesheet >> std::noskipws; for (;;) { if (!(cuesheet >> last)) throw FlycastException(i18n::T("Invalid CUE file")); if (last == '"') break; track_filename += last; } cuesheet >> std::skipws; } else { cuesheet >> track_filename; track_filename = last + track_filename; } cuesheet >> token; // BINARY if (token != "BINARY") { WARN_LOG(GDROM, "CUE parse error: unsupported FILE format %s. Expected BINARY", token.c_str()); throw FlycastException(i18n::T("Invalid CUE file")); } track_filename = hostfs::storage().getSubPath(basepath, track_filename); hostfs::File *track_file = hostfs::storage().openFile(track_filename, "rb"); if (track_file == nullptr) throw FlycastException(strprintf(i18n::T("CUE file: cannot open track %s"), track_filename.c_str())); if (digest != nullptr) md5.add(track_file); delete track_file; fileInfo = hostfs::storage().getFileInfo(track_filename); fileStartFAD = currentFAD; // Clear track context track_number = -1; track_type.clear(); track_isrc.clear(); track_secsize = 0; } else if (token == "PREGAP") { cuesheet >> token; int min = 0, sec = 0, frame = 0; if (sscanf(token.c_str(), "%d:%d:%d", &min, &sec, &frame) == 3) pregap = frame + 75 * (sec + 60 * min); else throw FlycastException(i18n::T("Invalid PREGAP in CUE file")); } else if (token == "TRACK") { cuesheet >> track_number; cuesheet >> track_type; if (track_filename.empty()) { WARN_LOG(GDROM, "CUE parse error: TRACK %02d not in a FILE context", track_number); throw FlycastException(i18n::T("Invalid CUE file")); } if (track_number <= 0 || track_number > 99) { WARN_LOG(GDROM, "CUE parse error: Invalid track number %d", track_number); throw FlycastException(i18n::T("Invalid CUE file")); } bool firstTrackOfFile = track_secsize == 0; track_secsize = getSectorSize(track_type); if (track_secsize == 0) throw FlycastException(strprintf(i18n::T("CUE file: track has unknown sector type: %s"), track_type.c_str())); // Can happen if multiple tracks with different sector sizes share the same file if (fileInfo.size % track_secsize != 0) WARN_LOG(GDROM, "Warning: Size of track %s is not multiple of sector size %d", track_filename.c_str(), track_secsize); // FIXME This is wrong if tracks of different sector size share the same file if (firstTrackOfFile) currentFAD += fileInfo.size / track_secsize; } else if (token == "INDEX") { u32 index_num; cuesheet >> index_num; if (index_num == 1) { cuesheet >> token; int indexFAD = 0; int min = 0, sec = 0, frame = 0; if (sscanf(token.c_str(), "%d:%d:%d", &min, &sec, &frame) == 3) indexFAD = frame + 75 * (sec + 60 * min); if (firstTrackOfSession && indexFAD > 0) { // The session gap above already includes this track's pregap so don't // count the sectors stored before INDEX 01 twice. const u32 pregap = std::min(indexFAD, 150); fileStartFAD -= pregap; currentFAD -= pregap; } firstTrackOfSession = false; Track t; t.StartFAD = fileStartFAD + indexFAD; t.CTRL = (track_type == "AUDIO" || track_type == "CDG") ? 0 : 4; t.EndFAD = currentFAD - 1; t.isrc = track_isrc; if (pregap > 0) { t.StartFAD += pregap; t.EndFAD += pregap; pregap = 0; // reset pregap after track is added } DEBUG_LOG(GDROM, "file[%zd] \"%s\": session %d type %s FAD:%d -> %d %s", disc->tracks.size() + 1, track_filename.c_str(), session_number, track_type.c_str(), t.StartFAD, t.EndFAD, t.isrc.empty() ? "" : ("ISRC " + t.isrc).c_str()); hostfs::File *track_file = hostfs::storage().openFile(track_filename, "rb"); if (track_file == nullptr) throw FlycastException(strprintf(i18n::T("CUE file: cannot open track %s"), track_filename.c_str())); t.file = new RawTrackFile(track_file, indexFAD * track_secsize, t.StartFAD, track_secsize); disc->tracks.push_back(t); if (disc->tracks.size() >= 2) { Track& prevTrack = disc->tracks[disc->tracks.size() - 2]; prevTrack.EndFAD = std::min(prevTrack.EndFAD, t.StartFAD - 1); } } else if (index_num != 0) { WARN_LOG(GDROM, "INDEX %02d not supported", index_num); throw FlycastException(i18n::T("Invalid CUE file")); } } else if (token == "CATALOG") { cuesheet >> disc->catalog; } else if (token == "ISRC") { cuesheet >> track_isrc; } } if (disc->tracks.empty()) throw FlycastException(i18n::Ts("CUE parse error: failed to parse or invalid file with 0 tracks")); if (disc->tracks.size() > 99) { WARN_LOG(GDROM, "CUE: more than 99 tracks"); throw FlycastException(i18n::Ts("Invalid CUE file")); } if (session_number == 0) { if (disc->type == GdRom) { // GD-Rom if (disc->tracks.size() < 3) throw FlycastException(i18n::Ts("CUE parse error: less than 3 tracks")); if (!disc->tracks[0].isDataTrack()) { WARN_LOG(GDROM, "CUE: track 1 must be a data track"); throw FlycastException(i18n::Ts("Invalid CUE file")); } if (disc->tracks[1].isDataTrack()) { WARN_LOG(GDROM, "CUE: track 2 must be an audio track"); throw FlycastException(i18n::Ts("Invalid CUE file")); } if (!disc->tracks[2].isDataTrack()) { WARN_LOG(GDROM, "CUE: track 3 must be a data track"); throw FlycastException(i18n::Ts("Invalid CUE file")); } if (disc->tracks[2].StartFAD != 45150) { WARN_LOG(GDROM, "CUE: track 3 must start at FAD 45000+150, not %d", disc->tracks[2].StartFAD); throw FlycastException(i18n::Ts("Invalid CUE file")); } disc->FillGDSession(); } else { // CD-Rom Session ses; ses.FirstTrack = 1; ses.StartFAD = disc->tracks[0].StartFAD; disc->sessions.push_back(ses); } } else { // CD-Rom XA disc->type = CdRom_XA; } if (disc->type != GdRom) { disc->LeadOut.ADR = 1; disc->LeadOut.CTRL = 4; disc->EndFAD = disc->LeadOut.StartFAD = currentFAD; } if (digest != nullptr) *digest = md5.getDigest(); return disc.release(); }