diff options
author | 2020-12-18 03:48:47 +1100 | |
---|---|---|
committer | 2020-12-17 18:48:47 +0200 | |
commit | ccacd0f16550cd104c7a0b1b10358b7e18aeabf3 (patch) | |
tree | 1787d8117ee659590316925601a4736c635a98b8 | |
parent | 0e09ae932831a1f0907d330d5c392281340f3ef4 (diff) |
support Redump extended bin/cue format for Dreamcast discs (#7422)
* initial check-in of Redump bin/cue support for Dreamcast GDI
* correctly identifies multi-cue format and sets GDROM flags
* creates a working Crazy Taxi chd from a Redump bin/cue
* disabled debugging code and started tidying up
* simple tool to compare chdman bin/cue and bin/gdi conversions, should be identical
* final tidy up, the testing is going well
* testing failed for Aero Dancing i (Japan), didnt zero last track
* added some comments about .gdi compatibility
* addressing review feedback on pull request #7422
* match TOSEC layout for Pattern I discs (3 tracks)
* initial support for Pattern III discs
* Pattern III discs now work and match TOSEC layout
* reading datasize from wrong track, same result though
* identify the GDI pattern, makes the code clearer
* support for Pattern II and consecutive AUDIO tracks
* use C99 type not POSIX type to build on Windows
* support Redump tracks split across two .bin files
-rw-r--r-- | src/lib/util/cdrom.h | 4 | ||||
-rw-r--r-- | src/lib/util/chdcd.cpp | 516 | ||||
-rw-r--r-- | src/tools/chdman.cpp | 21 |
3 files changed, 539 insertions, 2 deletions
diff --git a/src/lib/util/cdrom.h b/src/lib/util/cdrom.h index 50b762d5009..a5b6beda806 100644 --- a/src/lib/util/cdrom.h +++ b/src/lib/util/cdrom.h @@ -82,11 +82,15 @@ struct cdrom_track_info /* fields used in CHDMAN only */ uint32_t padframes; /* number of frames of padding to add to the end of the track; needed for GDI */ + uint32_t splitframes; /* number of frames to read from the next file; needed for Redump split-bin GDI */ /* fields used in MAME/MESS only */ uint32_t logframeofs; /* logical frame of actual track data - offset by pregap size if pregap not physically present */ uint32_t physframeofs; /* physical frame of actual track data in CHD data */ uint32_t chdframeofs; /* frame number this track starts at on the CHD */ + + /* fields used in multi-cue GDI */ + uint32_t multicuearea; }; diff --git a/src/lib/util/chdcd.cpp b/src/lib/util/chdcd.cpp index b2b0e141499..605b629c623 100644 --- a/src/lib/util/chdcd.cpp +++ b/src/lib/util/chdcd.cpp @@ -36,6 +36,19 @@ #define TOKENIZE i = tokenize( linebuffer, i, sizeof(linebuffer), token, sizeof(token) ); +enum gdi_area { + SINGLE_DENSITY, + HIGH_DENSITY +}; + +enum gdi_pattern { + TYPE_UNKNOWN = 0, + TYPE_I, + TYPE_II, + TYPE_III, + TYPE_III_SPLIT +}; + /*************************************************************************** GLOBAL VARIABLES @@ -1129,6 +1142,504 @@ chd_error chdcd_parse_cue(const char *tocfname, cdrom_toc &outtoc, chdcd_track_i return CHDERR_NONE; } +/*--------------------------------------------------------------------------------------- + chdcd_is_gdicue - determine if CUE contains Redump multi-CUE format for Dreamcast GDI +----------------------------------------------------------------------------------------*/ + +/** + * Dreamcast GDI has two images on one disc, SINGLE-DENSITY and HIGH-DENSITY. + * + * Redump stores both images in a single .cue with a REM comment separating the images. + * This multi-cue format replaces the old flawed .gdi format. + * + * http://forum.redump.org/topic/19969/done-sega-dreamcast-multicue-gdi/ + * + * This function looks for strings "REM SINGLE-DENSITY AREA" & "REM HIGH-DENSITY AREA" + * indicating the Redump multi-cue format and therefore a Dreamcast GDI disc. + */ + +bool chdcd_is_gdicue(const char *tocfname) +{ + FILE *infile; + bool has_rem_singledensity = false; + bool has_rem_highdensity = false; + std::string path = std::string(tocfname); + + infile = fopen(tocfname, "rt"); + path = get_file_path(path); + if (infile == (FILE *)nullptr) + { + return false; + } + + while (!feof(infile)) + { + fgets(linebuffer, 511, infile); + + /* if EOF didn't hit, keep going */ + if (!feof(infile)) + { + has_rem_singledensity = has_rem_singledensity || !strncmp(linebuffer, "REM SINGLE-DENSITY AREA", 23); + has_rem_highdensity = has_rem_highdensity || !strncmp(linebuffer, "REM HIGH-DENSITY AREA", 21); + } + } + + fclose(infile); + + return has_rem_singledensity && has_rem_highdensity; +} + +/*----------------------------------------------------------------- + chdcd_parse_gdicue - parse a Redump multi-CUE for Dreamcast GDI +------------------------------------------------------------------*/ + +/** + * @fn chd_error chdcd_parse_gdicue(const char *tocfname, cdrom_toc &outtoc, chdcd_track_input_info &outinfo) + * + * @brief Chdcd parse cue. + * + * @param tocfname The tocfname. + * @param [in,out] outtoc The outtoc. + * @param [in,out] outinfo The outinfo. + * + * @return A chd_error. + * + * Dreamcast discs have two images on a single disc. The first image is SINGLE-DENSITY and the second image + * is HIGH-DENSITY. The SINGLE-DENSITY area starts 0 LBA and HIGH-DENSITY area starts 45000 LBA. + * + * There are three Dreamcast disc patterns. + * + * Pattern I - (SD) DATA + AUDIO, (HD) DATA + * Pattern II - (SD) DATA + AUDIO, (HD) DATA + ... + AUDIO + * Pattern III - (SD) DATA + AUDIO, (HD) DATA + ... + DATA + * + * TOSEC layout is preferred and this code adjusts the TOC and INFO generated by a Redump .cue to match the + * layout from a TOSEC .gdi. + */ + +chd_error chdcd_parse_gdicue(const char *tocfname, cdrom_toc &outtoc, chdcd_track_input_info &outinfo) +{ + FILE *infile; + int i, trknum; + static char token[512]; + std::string lastfname; + uint32_t wavlen, wavoffs; + std::string path = std::string(tocfname); + enum gdi_area current_area = SINGLE_DENSITY; + enum gdi_pattern disc_pattern = TYPE_UNKNOWN; + + infile = fopen(tocfname, "rt"); + path = get_file_path(path); + if (infile == (FILE *)nullptr) + { + return CHDERR_FILE_NOT_FOUND; + } + + /* clear structures */ + memset(&outtoc, 0, sizeof(outtoc)); + outinfo.reset(); + + trknum = -1; + wavoffs = wavlen = 0; + + outtoc.flags = CD_FLAG_GDROM; + + while (!feof(infile)) + { + /* get the next line */ + fgets(linebuffer, 511, infile); + + /* if EOF didn't hit, keep going */ + if (!feof(infile)) + { + /* single-density area starts LBA = 0 */ + if (!strncmp(linebuffer, "REM SINGLE-DENSITY AREA", 23)) + { + current_area = SINGLE_DENSITY; + continue; + } + + /* high-density area starts LBA = 45000 */ + if (!strncmp(linebuffer, "REM HIGH-DENSITY AREA", 21)) + { + current_area = HIGH_DENSITY; + continue; + } + + i = 0; + + TOKENIZE + + if (!strcmp(token, "FILE")) + { + /* found the data file for a track */ + TOKENIZE + + /* keep the filename */ + lastfname.assign(path).append(token); + + /* get the file type */ + TOKENIZE + + if (!strcmp(token, "BINARY")) + { + outinfo.track[trknum+1].swap = false; + } + else if (!strcmp(token, "MOTOROLA")) + { + outinfo.track[trknum+1].swap = true; + } + else if (!strcmp(token, "WAVE")) + { + wavlen = parse_wav_sample(lastfname.c_str(), &wavoffs); + if (!wavlen) + { + fclose(infile); + printf("ERROR: couldn't read [%s] or not a valid .WAV\n", lastfname.c_str()); + return CHDERR_INVALID_DATA; + } + } + else + { + fclose(infile); + printf("ERROR: Unhandled track type %s\n", token); + return CHDERR_UNSUPPORTED_FORMAT; + } + } + else if (!strcmp(token, "TRACK")) + { + /* get the track number */ + TOKENIZE + trknum = strtoul(token, nullptr, 10) - 1; + + /* next token on the line is the track type */ + TOKENIZE + + if (wavlen != 0) + { + outtoc.tracks[trknum].trktype = CD_TRACK_AUDIO; + outtoc.tracks[trknum].frames = wavlen/2352; + outinfo.track[trknum].offset = wavoffs; + wavoffs = wavlen = 0; + } + else + { + outtoc.tracks[trknum].trktype = CD_TRACK_MODE1; + outtoc.tracks[trknum].datasize = 0; + outinfo.track[trknum].offset = 0; + } + outtoc.tracks[trknum].subtype = CD_SUB_NONE; + outtoc.tracks[trknum].subsize = 0; + outtoc.tracks[trknum].pgsub = CD_SUB_NONE; + outtoc.tracks[trknum].pregap = 0; + outtoc.tracks[trknum].padframes = 0; + outtoc.tracks[trknum].multicuearea = current_area; + outinfo.track[trknum].idx0offs = -1; + outinfo.track[trknum].idx1offs = 0; + + outinfo.track[trknum].fname.assign(lastfname); // default filename to the last one + +#if 0 + printf("trk %d: fname %s offset %d area %d\n", trknum, outinfo.track[trknum].fname.c_str(), outinfo.track[trknum].offset, outtoc.tracks[trknum].multicuearea); +#endif + + cdrom_convert_type_string_to_track_info(token, &outtoc.tracks[trknum]); + if (outtoc.tracks[trknum].datasize == 0) + { + fclose(infile); + printf("ERROR: Unknown track type [%s]. Contact MAMEDEV.\n", token); + return CHDERR_UNSUPPORTED_FORMAT; + } + + /* next (optional) token on the line is the subcode type */ + TOKENIZE + + cdrom_convert_subtype_string_to_track_info(token, &outtoc.tracks[trknum]); + } + else if (!strcmp(token, "INDEX")) /* only in bin/cue files */ + { + int idx, frames; + + /* get index number */ + TOKENIZE + idx = strtoul(token, nullptr, 10); + + /* get index */ + TOKENIZE + frames = msf_to_frames( token ); + + if (idx == 0) + { + outinfo.track[trknum].idx0offs = frames; + } + else if (idx == 1) + { + outinfo.track[trknum].idx1offs = frames; + if ((outtoc.tracks[trknum].pregap == 0) && (outinfo.track[trknum].idx0offs != -1)) + { + outtoc.tracks[trknum].pregap = frames - outinfo.track[trknum].idx0offs; + outtoc.tracks[trknum].pgtype = outtoc.tracks[trknum].trktype; + switch (outtoc.tracks[trknum].pgtype) + { + case CD_TRACK_MODE1: + case CD_TRACK_MODE2_FORM1: + outtoc.tracks[trknum].pgdatasize = 2048; + break; + + case CD_TRACK_MODE1_RAW: + case CD_TRACK_MODE2_RAW: + case CD_TRACK_AUDIO: + outtoc.tracks[trknum].pgdatasize = 2352; + break; + + case CD_TRACK_MODE2: + case CD_TRACK_MODE2_FORM_MIX: + outtoc.tracks[trknum].pgdatasize = 2336; + break; + + case CD_TRACK_MODE2_FORM2: + outtoc.tracks[trknum].pgdatasize = 2324; + break; + } + } + else // pregap sectors not in file, but we're always using idx0ofs for track length calc now + { + outinfo.track[trknum].idx0offs = frames; + } + } + } + else if (!strcmp(token, "PREGAP")) + { + int frames; + + /* get index */ + TOKENIZE + frames = msf_to_frames( token ); + + outtoc.tracks[trknum].pregap = frames; + } + else if (!strcmp(token, "POSTGAP")) + { + int frames; + + /* get index */ + TOKENIZE + frames = msf_to_frames( token ); + + outtoc.tracks[trknum].postgap = frames; + } + } + } + + /* close the input CUE */ + fclose(infile); + + /* store the number of tracks found */ + outtoc.numtrks = trknum + 1; + + /* now go over the files again and set the lengths */ + for (trknum = 0; trknum < outtoc.numtrks; trknum++) + { + uint64_t tlen = 0; + + // this is true for cue/bin and cue/iso, and we need it for cue/wav since .WAV is little-endian + if (outtoc.tracks[trknum].trktype == CD_TRACK_AUDIO) + { + outinfo.track[trknum].swap = true; + } + + // don't do this for .WAV tracks, we already have their length and offset filled out + if (outinfo.track[trknum].offset == 0) + { + // is this the last track? + if (trknum == (outtoc.numtrks-1)) + { + /* if we have the same filename as the last track, do it that way */ + if (trknum != 0 && (outinfo.track[trknum].fname.compare(outinfo.track[trknum-1].fname)==0)) + { + tlen = get_file_size(outinfo.track[trknum].fname.c_str()); + if (tlen == 0) + { + printf("ERROR: couldn't find bin file [%s]\n", outinfo.track[trknum-1].fname.c_str()); + return CHDERR_FILE_NOT_FOUND; + } + outinfo.track[trknum].offset = outinfo.track[trknum-1].offset + outtoc.tracks[trknum-1].frames * (outtoc.tracks[trknum-1].datasize + outtoc.tracks[trknum-1].subsize); + outtoc.tracks[trknum].frames = (tlen - outinfo.track[trknum].offset) / (outtoc.tracks[trknum].datasize + outtoc.tracks[trknum].subsize); + } + else /* data files are different */ + { + tlen = get_file_size(outinfo.track[trknum].fname.c_str()); + if (tlen == 0) + { + printf("ERROR: couldn't find bin file [%s]\n", outinfo.track[trknum-1].fname.c_str()); + return CHDERR_FILE_NOT_FOUND; + } + tlen /= (outtoc.tracks[trknum].datasize + outtoc.tracks[trknum].subsize); + outtoc.tracks[trknum].frames = tlen; + outinfo.track[trknum].offset = 0; + } + } + else + { + /* if we have the same filename as the next track, do it that way */ + if (outinfo.track[trknum].fname.compare(outinfo.track[trknum+1].fname)==0) + { + outtoc.tracks[trknum].frames = outinfo.track[trknum+1].idx0offs - outinfo.track[trknum].idx0offs; + + if (trknum == 0) // track 0 offset is 0 + { + outinfo.track[trknum].offset = 0; + } + else + { + outinfo.track[trknum].offset = outinfo.track[trknum-1].offset + outtoc.tracks[trknum-1].frames * (outtoc.tracks[trknum-1].datasize + outtoc.tracks[trknum-1].subsize); + } + + if (!outtoc.tracks[trknum].frames) + { + printf("ERROR: unable to determine size of track %d, missing INDEX 01 markers?\n", trknum+1); + return CHDERR_INVALID_DATA; + } + } + else /* data files are different */ + { + tlen = get_file_size(outinfo.track[trknum].fname.c_str()); + if (tlen == 0) + { + printf("ERROR: couldn't find bin file [%s]\n", outinfo.track[trknum].fname.c_str()); + return CHDERR_FILE_NOT_FOUND; + } + tlen /= (outtoc.tracks[trknum].datasize + outtoc.tracks[trknum].subsize); + outtoc.tracks[trknum].frames = tlen; + outinfo.track[trknum].offset = 0; + } + } + } + } + + /* + * Dreamcast patterns are identified by track types and number of tracks + */ + if (outtoc.numtrks > 4 && outtoc.tracks[outtoc.numtrks-1].pgtype == CD_TRACK_MODE1_RAW) + { + if (outtoc.tracks[outtoc.numtrks-2].pgtype == CD_TRACK_AUDIO) + disc_pattern = TYPE_III_SPLIT; + else + disc_pattern = TYPE_III; + } + else if (outtoc.numtrks > 3) + { + if (outtoc.tracks[outtoc.numtrks-1].pgtype == CD_TRACK_AUDIO) + disc_pattern = TYPE_II; + else + disc_pattern = TYPE_III; + } + else if (outtoc.numtrks == 3) + { + disc_pattern = TYPE_I; + } + + /* + * Strip pregaps from Redump tracks and adjust the LBA offset to match TOSEC layout + */ + for (trknum = 1; trknum < outtoc.numtrks; trknum++) + { + uint32_t prev_pregap = outtoc.tracks[trknum-1].pregap; + uint32_t prev_offset = prev_pregap * (outtoc.tracks[trknum-1].datasize + outtoc.tracks[trknum-1].subsize); + uint32_t this_pregap = outtoc.tracks[trknum].pregap; + uint32_t this_offset = this_pregap * (outtoc.tracks[trknum].datasize + outtoc.tracks[trknum].subsize); + + if (outtoc.tracks[trknum-1].pgtype != CD_TRACK_AUDIO) + { + // pad previous DATA track to match TOSEC layout + outtoc.tracks[trknum-1].frames += this_pregap; + outtoc.tracks[trknum-1].padframes += this_pregap; + } + + if (outtoc.tracks[trknum-1].pgtype == CD_TRACK_AUDIO && outtoc.tracks[trknum].pgtype == CD_TRACK_AUDIO) + { + // shift previous AUDIO track to match TOSEC layout + outinfo.track[trknum-1].offset += prev_offset; + outtoc.tracks[trknum-1].splitframes += prev_pregap; + } + + if (outtoc.tracks[trknum-1].pgtype == CD_TRACK_AUDIO && outtoc.tracks[trknum].pgtype != CD_TRACK_AUDIO) + { + // shrink previous AUDIO track to match TOSEC layout + outtoc.tracks[trknum-1].frames -= prev_pregap; + outinfo.track[trknum-1].offset += prev_offset; + } + + if (outtoc.tracks[trknum].pgtype == CD_TRACK_AUDIO && trknum == outtoc.numtrks-1) + { + // shrink final AUDIO track to match TOSEC layout + outtoc.tracks[trknum].frames -= this_pregap; + outinfo.track[trknum].offset += this_offset; + } + } + + /* + * Special handling for TYPE_III_SPLIT, pregap in last track contains 75 frames audio and 150 frames data + */ + if (disc_pattern == TYPE_III_SPLIT) + { + assert(outtoc.tracks[outtoc.numtrks-1].pregap == 225); + + // grow the AUDIO track into DATA track by 75 frames as per Pattern III + outtoc.tracks[outtoc.numtrks-2].frames += 225; + outtoc.tracks[outtoc.numtrks-2].padframes += 150; + outinfo.track[outtoc.numtrks-2].offset = 150 * (outtoc.tracks[outtoc.numtrks-2].datasize+outtoc.tracks[outtoc.numtrks-2].subsize); + outtoc.tracks[outtoc.numtrks-2].splitframes = 75; + + // skip the pregap when reading the DATA track + outtoc.tracks[outtoc.numtrks-1].frames -= 225; + outinfo.track[outtoc.numtrks-1].offset += 225 * (outtoc.tracks[outtoc.numtrks-1].datasize+outtoc.tracks[outtoc.numtrks-1].subsize); + } + + /* + * TOC now matches TOSEC layout, set LBA for every track with HIGH-DENSITY area @ LBA 45000 + */ + for (trknum = 1; trknum < outtoc.numtrks; trknum++) + { + if (outtoc.tracks[trknum].multicuearea == HIGH_DENSITY && outtoc.tracks[trknum-1].multicuearea == SINGLE_DENSITY) + { + outtoc.tracks[trknum].physframeofs = 45000; + int dif=outtoc.tracks[trknum].physframeofs-(outtoc.tracks[trknum-1].frames+outtoc.tracks[trknum-1].physframeofs); + outtoc.tracks[trknum-1].frames += dif; + outtoc.tracks[trknum-1].padframes = dif; + } + else + { + outtoc.tracks[trknum].physframeofs = outtoc.tracks[trknum-1].physframeofs + outtoc.tracks[trknum-1].frames; + } + + // no longer need the pregap info, zeroed out to match TOSEC layout + outtoc.tracks[trknum].pregap = 0; + outtoc.tracks[trknum].pgtype = 0; + } + +#if 0 + for (trknum = 0; trknum < outtoc.numtrks; trknum++) + { + printf("trk %d: %d frames @ offset %d, pad=%d, split=%d, area=%d, phys=%d, pregap=%d, pgtype=%d, idx0=%d, idx1=%d, (true %d)\n", + trknum+1, + outtoc.tracks[trknum].frames, + outinfo.track[trknum].offset, + outtoc.tracks[trknum].padframes, + outtoc.tracks[trknum].splitframes, + outtoc.tracks[trknum].multicuearea, + outtoc.tracks[trknum].physframeofs, + outtoc.tracks[trknum].pregap, + outtoc.tracks[trknum].pgtype, + outinfo.track[trknum].idx0offs, + outinfo.track[trknum].idx1offs, + outtoc.tracks[trknum].frames - outtoc.tracks[trknum].padframes); + } +#endif + + return CHDERR_NONE; +} + /*------------------------------------------------- chdcd_parse_toc - parse a CDRDAO format TOC file -------------------------------------------------*/ @@ -1165,7 +1676,10 @@ chd_error chdcd_parse_toc(const char *tocfname, cdrom_toc &outtoc, chdcd_track_i if (strstr(tocftemp,".cue")) { - return chdcd_parse_cue(tocfname, outtoc, outinfo); + if (chdcd_is_gdicue(tocfname)) + return chdcd_parse_gdicue(tocfname, outtoc, outinfo); + else + return chdcd_parse_cue(tocfname, outtoc, outinfo); } if (strstr(tocftemp,".nrg")) diff --git a/src/tools/chdman.cpp b/src/tools/chdman.cpp index 06f2373f5ca..516e2f5b057 100644 --- a/src/tools/chdman.cpp +++ b/src/tools/chdman.cpp @@ -395,10 +395,27 @@ public: uint64_t src_track_start = m_info.track[tracknum].offset; uint64_t src_track_end = src_track_start + bytesperframe * (uint64_t)trackinfo.frames; uint64_t pad_track_start = src_track_end - ((uint64_t)m_toc.tracks[tracknum].padframes * bytesperframe); + uint64_t split_track_start = pad_track_start - ((uint64_t)m_toc.tracks[tracknum].splitframes * bytesperframe); + + // dont split when split-bin read not required + if ((uint64_t)m_toc.tracks[tracknum].splitframes == 0L) + split_track_start = UINT64_MAX; + while (length_remaining != 0 && offset < endoffs) { // determine start of current frame uint64_t src_frame_start = src_track_start + ((offset - startoffs) / CD_FRAME_SIZE) * bytesperframe; + + // auto-advance next track for split-bin read + if (src_frame_start == split_track_start && m_lastfile.compare(m_info.track[tracknum+1].fname)!=0) + { + m_file.reset(); + m_lastfile = m_info.track[tracknum+1].fname; + osd_file::error filerr = util::core_file::open(m_lastfile, OPEN_FLAG_READ, m_file); + if (filerr != osd_file::error::NONE) + report_error(1, "Error opening input file (%s)'", m_lastfile.c_str()); + } + if (src_frame_start < src_track_end) { // read it in, or pad if we're into the padframes @@ -408,7 +425,9 @@ public: } else { - m_file->seek(src_frame_start, SEEK_SET); + m_file->seek((src_frame_start >= split_track_start) + ? src_frame_start - split_track_start + : src_frame_start, SEEK_SET); uint32_t count = m_file->read(dest, bytesperframe); if (count != bytesperframe) report_error(1, "Error reading input file (%s)'", m_lastfile.c_str()); |