diff --git a/docs/source/tools/chdman.rst b/docs/source/tools/chdman.rst index 0461264276426..cbf16fcba7571 100644 --- a/docs/source/tools/chdman.rst +++ b/docs/source/tools/chdman.rst @@ -319,6 +319,7 @@ Common options supported: Additional options: * ``--outputbin `` / ``-ob `` +* ``--splitbin`` / ``-sb`` extractdvd ~~~~~~~~~~ diff --git a/src/tools/chdman.cpp b/src/tools/chdman.cpp index 4ef41387117a0..639aec22a12d8 100644 --- a/src/tools/chdman.cpp +++ b/src/tools/chdman.cpp @@ -30,8 +30,9 @@ #include #include #include -#include #include +#include +#include #include #include #include @@ -96,6 +97,7 @@ constexpr int MODE_GDI = 2; #define OPTION_INPUT "input" #define OPTION_OUTPUT "output" #define OPTION_OUTPUT_BIN "outputbin" +#define OPTION_OUTPUT_SPLITBIN "splitbin" #define OPTION_OUTPUT_FORCE "force" #define OPTION_INPUT_START_BYTE "inputstartbyte" #define OPTION_INPUT_START_HUNK "inputstarthunk" @@ -628,6 +630,7 @@ static const option_description s_options[] = { OPTION_INPUT_PARENT, "ip", true, " : parent file name for input CHD" }, { OPTION_OUTPUT, "o", true, " : output file name" }, { OPTION_OUTPUT_BIN, "ob", true, " : output file name for binary data" }, + { OPTION_OUTPUT_SPLITBIN, "sb", false, ": output one binary file per track" }, { OPTION_OUTPUT_FORCE, "f", false, ": force overwriting an existing file" }, { OPTION_OUTPUT_PARENT, "op", true, " : parent file name for output CHD" }, { OPTION_INPUT_START_BYTE, "isb", true, " : starting byte offset within the input" }, @@ -782,6 +785,7 @@ static const command_description s_commands[] = { REQUIRED OPTION_OUTPUT, OPTION_OUTPUT_BIN, + OPTION_OUTPUT_SPLITBIN, OPTION_OUTPUT_FORCE, REQUIRED OPTION_INPUT, OPTION_INPUT_PARENT, @@ -1462,62 +1466,19 @@ static void compress_common(chd_file_compressor &chd) // to a CUE file //------------------------------------------------- -void output_track_metadata(int mode, util::core_file &file, int tracknum, const cdrom_file::track_info &info, const std::string &filename, uint32_t frameoffs, uint64_t discoffs) +void output_track_metadata(int mode, util::core_file &file, int tracknum, const cdrom_file::track_info &info, const std::string &filename, uint32_t frameoffs, uint64_t outputoffs) { if (mode == MODE_GDI) { - int mode = 0, size = 2048; - - switch (info.trktype) - { - case cdrom_file::CD_TRACK_MODE1: - mode = 4; - size = 2048; - break; - - case cdrom_file::CD_TRACK_MODE1_RAW: - mode = 4; - size = 2352; - break; - - case cdrom_file::CD_TRACK_MODE2: - mode = 4; - size = 2336; - break; - - case cdrom_file::CD_TRACK_MODE2_FORM1: - mode = 4; - size = 2048; - break; - - case cdrom_file::CD_TRACK_MODE2_FORM2: - mode = 4; - size = 2324; - break; - - case cdrom_file::CD_TRACK_MODE2_FORM_MIX: - mode = 4; - size = 2336; - break; - - case cdrom_file::CD_TRACK_MODE2_RAW: - mode = 4; - size = 2352; - break; - - case cdrom_file::CD_TRACK_AUDIO: - mode = 0; - size = 2352; - break; - } + const int tracktype = info.trktype == cdrom_file::CD_TRACK_AUDIO ? 0 : 4; const bool needquote = filename.find(' ') != std::string::npos; const char *const quotestr = needquote ? "\"" : ""; - file.printf("%d %d %d %d %s%s%s %d\n", tracknum+1, frameoffs, mode, size, quotestr, filename, quotestr, discoffs); + file.printf("%d %d %d %d %s%s%s %d\n", tracknum+1, frameoffs, tracktype, info.datasize, quotestr, filename, quotestr, outputoffs); } else if (mode == MODE_CUEBIN) { - // first track specifies the file - if (tracknum == 0) + // specify a new file when writing to the beginning of a file + if (outputoffs == 0) file.printf("FILE \"%s\" BINARY\n", filename); // determine submode @@ -1592,11 +1553,10 @@ void output_track_metadata(int mode, util::core_file &file, int tracknum, const if (info.pregap > 0) file.printf("ZERO %s %s\n", modesubmode, msf_string_from_frames(info.pregap)); - // all tracks but the first one have a file offset - if (tracknum > 0) - file.printf("DATAFILE \"%s\" #%d %s // length in bytes: %d\n", filename, uint32_t(discoffs), msf_string_from_frames(info.frames), info.frames * (info.datasize + info.subsize)); - else + if (outputoffs == 0) file.printf("DATAFILE \"%s\" %s // length in bytes: %d\n", filename, msf_string_from_frames(info.frames), info.frames * (info.datasize + info.subsize)); + else + file.printf("DATAFILE \"%s\" #%d %s // length in bytes: %d\n", filename, uint32_t(outputoffs), msf_string_from_frames(info.frames), info.frames * (info.datasize + info.subsize)); // tracks with pregaps get a START marker too if (info.pregap > 0) @@ -2591,66 +2551,158 @@ static void do_extract_cd(parameters_map ¶ms) if (output_file_str != params.end()) check_existing_output_file(params, *output_file_str->second); - // verify output BIN file doesn't exist + // determine output type based on the specified file extension + int mode = MODE_NORMAL; + if (core_filename_ends_with(*output_file_str->second, ".cue")) + mode = MODE_CUEBIN; + else if (core_filename_ends_with(*output_file_str->second, ".gdi")) + mode = MODE_GDI; + + // determine the output bin filename based on provided input parameters auto output_bin_file_fnd = params.find(OPTION_OUTPUT_BIN); std::string default_name(*output_file_str->second); + + // split path and extension int chop = default_name.find_last_of('.'); - if (chop != -1) + if (chop != std::string::npos) default_name.erase(chop, default_name.size()); - std::string basename = default_name; - default_name.append(".bin"); + + // GDIs will always output as split bin + bool is_splitbin = mode == MODE_GDI || params.find(OPTION_OUTPUT_SPLITBIN) != params.end(); + if (!is_splitbin && cdrom->is_gdrom() && mode == MODE_CUEBIN) + { + // GD-ROM cue/bin is in Redump format which should always be split by tracks + util::stream_format(std::cout, "Warning: --%s is required for this specific combination of input disc type and output format, enabling automatically\n", OPTION_OUTPUT_SPLITBIN); + is_splitbin = true; + } + + if (is_splitbin) + { + if (mode == MODE_GDI) + { + default_name += "%02t"; + } + else + { + const std::string format = toc.numtrks >= 10 ? "%02t" : "%t"; + default_name += " (Track " + format + ")"; + } + } + + std::string output_bin_file_ext = ".bin"; std::string *output_bin_file_str; if (output_bin_file_fnd == params.end()) + { output_bin_file_str = &default_name; + } else + { output_bin_file_str = output_bin_file_fnd->second; - check_existing_output_file(params, *output_bin_file_str); + chop = output_bin_file_str->find_last_of('.'); + if (chop != std::string::npos) + { + output_bin_file_ext = output_bin_file_str->substr(chop, output_bin_file_str->size() - chop); + output_bin_file_str->erase(chop, output_bin_file_str->size()); + } + } + + if (output_bin_file_str->find('"') != std::string::npos || output_bin_file_ext.find('"') != std::string::npos) + report_error(1, "Output bin filename (%s%s) must not contain quotation marks", *output_bin_file_str, output_bin_file_ext); // print some info - util::stream_format(std::cout, "Output TOC: %s\n", *output_file_str->second); - util::stream_format(std::cout, "Output Data: %s\n", *output_bin_file_str); util::stream_format(std::cout, "Input CHD: %s\n", *params.find(OPTION_INPUT)->second); + util::stream_format(std::cout, "Output TOC: %s\n", *output_file_str->second); // catch errors so we can close & delete the output file + std::vector track_filenames; util::core_file::ptr output_bin_file; util::core_file::ptr output_toc_file; + std::vector output_bin_filenames; + std::string trackbin_name; try { - int mode = MODE_NORMAL; - - if (output_file_str->second->find(".cue") != -1) - { - mode = MODE_CUEBIN; - } - else if (output_file_str->second->find(".gdi") != -1) - { - mode = MODE_GDI; - } - - if (cdrom->is_gdrom() && (mode == MODE_CUEBIN)) - { - util::stream_format(std::cout, "Warning: extracting GD-ROM CHDs as bin/cue is not fully supported and will result in an unusable CD-ROM cue file.\n"); - } - // process output file std::error_condition filerr = util::core_file::open(*output_file_str->second, OPEN_FLAG_WRITE | OPEN_FLAG_CREATE | OPEN_FLAG_NO_BOM, output_toc_file); if (filerr) report_error(1, "Unable to open file (%s): %s", *output_file_str->second, filerr.message()); - // process output BIN file - if (mode != MODE_GDI) - { - filerr = util::core_file::open(*output_bin_file_str, OPEN_FLAG_WRITE | OPEN_FLAG_CREATE, output_bin_file); - if (filerr) - report_error(1, "Unable to open file (%s): %s", *output_bin_file_str, filerr.message()); - } - - // determine total frames uint64_t total_bytes = 0; for (int tracknum = 0; tracknum < toc.numtrks; tracknum++) + { + // determine total frames total_bytes += toc.tracks[tracknum].frames * (toc.tracks[tracknum].datasize + toc.tracks[tracknum].subsize); + // generate output filename for each track + std::string new_trackbin_name = *output_bin_file_str; + + if (mode == MODE_GDI && toc.tracks[tracknum].trktype == cdrom_file::CD_TRACK_AUDIO) + new_trackbin_name += ".raw"; + else + new_trackbin_name += output_bin_file_ext; + + // variable replacement in output filename + const std::regex variables_regex("(%*)(%([+-]?\\d+)?([a-zA-Z]))"); + std::string::const_iterator new_trackbin_name_itr = new_trackbin_name.begin(); + std::string::const_iterator new_trackbin_name_end = new_trackbin_name.end(); + std::string filename_formatted = new_trackbin_name; + std::smatch variable_matches; + bool found_track_variable = false; + + while (std::regex_search(new_trackbin_name_itr, new_trackbin_name_end, variable_matches, variables_regex)) + { + // full_match will always have one leading %, so if leading_escape has an even number of %s then + // we can know that we're working on an unescaped % + const std::string leading_escape = variable_matches[1].str(); + const std::string full_match = variable_matches[2].str(); + const std::string format_part = variable_matches[3].str(); + const std::string format_type = variable_matches[4].str(); + + if ((leading_escape.size() % 2) == 0) + { + std::string replacement; + + if (format_type == "t") + { + // track number + if (is_splitbin) + { + replacement = util::string_format("%" + format_part + "d", tracknum+1); + found_track_variable = true; + } + } + else + { + util::stream_format(std::cout, "Warning: encountered unknown format value '%s', ignoring\n", format_type); + } + + if (!replacement.empty()) + { + // replace all instances of encountered full format token + size_t index = std::string::npos; + while ((index = filename_formatted.find(full_match)) != std::string::npos) + filename_formatted.replace(index, full_match.size(), replacement); + } + } + + new_trackbin_name_itr = variable_matches.suffix().first; // move past match for next loop + } + + if (is_splitbin && !found_track_variable) + { + report_error(1, "A track number variable (%%t) must be specified in the output bin filename when --%s is enabled\n", OPTION_OUTPUT_SPLITBIN); + } + + // verify output BIN file doesn't exist + check_existing_output_file(params, filename_formatted); + + // display all new filenames to the user + if (track_filenames.empty() || filename_formatted != track_filenames.back()) + util::stream_format(std::cout, "Output Data: %s\n", filename_formatted); + + track_filenames.push_back(filename_formatted); + } + // GDI must start with the # of tracks if (mode == MODE_GDI) { @@ -2693,42 +2745,111 @@ static void do_extract_cd(parameters_map ¶ms) output_toc_file->printf("CD_ROM\n\n\n"); } + if (cdrom->is_gdrom() && mode == MODE_CUEBIN) + { + // modify TOC to match Redump cue/bin format as best as possible + cdrom_file::toc *trackinfo = (cdrom_file::toc*)&toc; + + // TOSEC GDI-based CHDs have the padframes field set to non-0 where the pregaps for the next track would be + const bool has_physical_pregap = trackinfo->tracks[0].padframes == 0; + + for (int tracknum = 1; tracknum < toc.numtrks; tracknum++) + { + // pgdatasize should never be set in GD-ROMs currently, so if it is set then assume the TOC has proper pregap values + if (trackinfo->tracks[tracknum].pgdatasize != 0) + break; + + // don't adjust the first track of the single-density and high-density areas + if (toc.tracks[tracknum].physframeofs == 45000) + continue; + + if (!has_physical_pregap) + { + // NOTE: This will generate a cue with PREGAP commands instead of INDEX 00 because the pregap data isn't baked into the bins + trackinfo->tracks[tracknum].pregap += trackinfo->tracks[tracknum-1].padframes; + + // "type 1" (only one data track in high-density area) and "type 2" (1 data and then the rest of the tracks being audio tracks in high-density area) don't require any adjustments + if (tracknum + 1 >= toc.numtrks && toc.tracks[tracknum].trktype != cdrom_file::CD_TRACK_AUDIO) + { + if (toc.tracks[tracknum-1].trktype != cdrom_file::CD_TRACK_AUDIO) + { + // "type 3" where the high-density area is just two data tracks + // there shouldn't be any pregap in the padframes from the previous track in this case, and the full 3s pregap is baked into the previous track + // Only known to be used by Shenmue II JP's discs 2, 3, 4 and Virtua Fighter History & VF4 + trackinfo->tracks[tracknum-1].padframes += 225; + + trackinfo->tracks[tracknum].pregap += 225; + trackinfo->tracks[tracknum].splitframes = 225; + trackinfo->tracks[tracknum].pgdatasize = trackinfo->tracks[tracknum].datasize; + trackinfo->tracks[tracknum].pgtype = trackinfo->tracks[tracknum].trktype; + } + else + { + // "type 3 split" where the first track and last of the high-density area are data tracks and in between is audio tracks + // TODO: These 75 frames are actually included at the end of the previous track so should be written + // It's currently not possible to format it as expected without hacky code because the 150 pregap for the last track + // is sandwiched between these 75 frames and the actual track data. + // The 75 frames seems to normally be 0s so this should be ok for now until a use case is found. + trackinfo->tracks[tracknum-1].frames -= 75; + trackinfo->tracks[tracknum].pregap += 75; + } + } + } + else + { + int curextra = 150; // 00:02:00 + if (tracknum + 1 >= toc.numtrks && toc.tracks[tracknum].trktype != cdrom_file::CD_TRACK_AUDIO) + curextra += 75; // 00:01:00, special case when last track is data + + trackinfo->tracks[tracknum-1].padframes = curextra; + + trackinfo->tracks[tracknum].pregap += curextra; + trackinfo->tracks[tracknum].splitframes = curextra; + trackinfo->tracks[tracknum].pgdatasize = trackinfo->tracks[tracknum].datasize; + trackinfo->tracks[tracknum].pgtype = trackinfo->tracks[tracknum].trktype; + } + } + } + // iterate over tracks and copy all data + uint64_t totaloutputoffs = 0; uint64_t outputoffs = 0; uint32_t discoffs = 0; std::vector buffer; + for (int tracknum = 0; tracknum < toc.numtrks; tracknum++) { - std::string trackbin_name = basename; - - if (mode == MODE_GDI) + if (track_filenames[tracknum] != trackbin_name) { - trackbin_name += util::string_format("%02d", tracknum+1); - if (toc.tracks[tracknum].trktype == cdrom_file::CD_TRACK_AUDIO) - trackbin_name += ".raw"; - else - trackbin_name += ".bin"; + totaloutputoffs += outputoffs; + outputoffs = 0; + + if (mode != MODE_GDI) + discoffs = 0; output_bin_file.reset(); + trackbin_name = track_filenames[tracknum]; + filerr = util::core_file::open(trackbin_name, OPEN_FLAG_WRITE | OPEN_FLAG_CREATE, output_bin_file); if (filerr) report_error(1, "Unable to open file (%s): %s", trackbin_name, filerr.message()); - outputoffs = 0; + output_bin_filenames.push_back(trackbin_name); } - // output the metadata about the track to the TOC file - const cdrom_file::track_info &trackinfo = toc.tracks[tracknum]; - if (mode == MODE_GDI) - { - output_track_metadata(mode, *output_toc_file, tracknum, trackinfo, std::string(core_filename_extract_base(trackbin_name)), discoffs, outputoffs); - } - else + if (cdrom->is_gdrom() && mode == MODE_CUEBIN) { - output_track_metadata(mode, *output_toc_file, tracknum, trackinfo, std::string(core_filename_extract_base(*output_bin_file_str)), discoffs, outputoffs); + if (tracknum == 0) + output_toc_file->printf("REM SINGLE-DENSITY AREA\n"); + else if (toc.tracks[tracknum].physframeofs == 45000) + output_toc_file->printf("REM HIGH-DENSITY AREA\n"); } + // output the metadata about the track to the TOC file + const cdrom_file::track_info &trackinfo = toc.tracks[tracknum]; + output_track_metadata(mode, *output_toc_file, tracknum, trackinfo, std::string(core_filename_extract_base(trackbin_name)), discoffs, outputoffs); + // If this is bin/cue output and the CHD contains subdata, warn the user and don't include // the subdata size in the buffer calculation. uint32_t output_frame_size = trackinfo.datasize + ((trackinfo.subtype != cdrom_file::CD_SUB_NONE) ? trackinfo.subsize : 0); @@ -2744,31 +2865,44 @@ static void do_extract_cd(parameters_map ¶ms) // now read and output the actual data uint32_t bufferoffs = 0; - uint32_t actualframes = trackinfo.frames - trackinfo.padframes; + uint32_t actualframes = trackinfo.frames - trackinfo.padframes + trackinfo.splitframes; for (uint32_t frame = 0; frame < actualframes; frame++) { - progress(false, "Extracting, %.1f%% complete... \r", 100.0 * double(outputoffs) / double(total_bytes)); + progress(false, "Extracting, %.1f%% complete... \r", 100.0 * double(totaloutputoffs + outputoffs) / double(total_bytes)); + + int trk, frameofs; + if (tracknum > 0 && frame < trackinfo.splitframes) + { + // pull data from previous track, the reverse of how splitframes is used when making the GD-ROM CHDs + trk = tracknum - 1; + frameofs = toc.tracks[trk].frames - trackinfo.splitframes + frame; + } + else + { + trk = tracknum; + frameofs = frame - trackinfo.splitframes; + } // read the data - cdrom->read_data(cdrom->get_track_start_phys(tracknum) + frame, &buffer[bufferoffs], trackinfo.trktype, true); + cdrom->read_data(cdrom->get_track_start_phys(trk) + frameofs, &buffer[bufferoffs], toc.tracks[trk].trktype, true); // for CDRWin and GDI audio tracks must be reversed // in the case of GDI and CHD version < 5 we assuming source CHD image is GDROM so audio tracks is already reversed - if (((mode == MODE_GDI && input_chd.version() > 4) || (mode == MODE_CUEBIN)) && (trackinfo.trktype == cdrom_file::CD_TRACK_AUDIO)) - for (int swapindex = 0; swapindex < trackinfo.datasize; swapindex += 2) + if (((mode == MODE_GDI && input_chd.version() > 4) || (mode == MODE_CUEBIN)) && (toc.tracks[trk].trktype == cdrom_file::CD_TRACK_AUDIO)) + for (int swapindex = 0; swapindex < toc.tracks[trk].datasize; swapindex += 2) { uint8_t swaptemp = buffer[bufferoffs + swapindex]; buffer[bufferoffs + swapindex] = buffer[bufferoffs + swapindex + 1]; buffer[bufferoffs + swapindex + 1] = swaptemp; } - bufferoffs += trackinfo.datasize; + bufferoffs += toc.tracks[trk].datasize; discoffs++; // read the subcode data - if (trackinfo.subtype != cdrom_file::CD_SUB_NONE && (mode == MODE_NORMAL)) + if (toc.tracks[trk].subtype != cdrom_file::CD_SUB_NONE && (mode == MODE_NORMAL)) { - cdrom->read_subcode(cdrom->get_track_start_phys(tracknum) + frame, &buffer[bufferoffs], true); - bufferoffs += trackinfo.subsize; + cdrom->read_subcode(cdrom->get_track_start_phys(trk) + frameofs, &buffer[bufferoffs], true); + bufferoffs += toc.tracks[trk].subsize; } // write it out if we need to @@ -2783,28 +2917,6 @@ static void do_extract_cd(parameters_map ¶ms) } } - if (cdrom->is_gdrom() && mode == MODE_CUEBIN && trackinfo.padframes > 0) - { - uint32_t padframes = trackinfo.padframes; - - // don't write the pad frames between the end of the single density area and start of the high density area - if (tracknum+1 < toc.numtrks && toc.tracks[tracknum+1].physframeofs == 45000) - padframes = 0; - - bufferoffs = 0; - std::fill(buffer.begin(), buffer.end(), 0); - - while (bufferoffs < padframes) - { - auto const [writerr, byteswritten] = write(*output_bin_file, &buffer[0], output_frame_size); - if (writerr) - report_error(1, "Error writing pad data to file (%s): %s\n", *output_file_str->second, "Write error"); - bufferoffs++; - } - - outputoffs += output_frame_size * padframes; - } - discoffs += trackinfo.padframes; } @@ -2818,7 +2930,8 @@ static void do_extract_cd(parameters_map ¶ms) // delete the output files output_bin_file.reset(); output_toc_file.reset(); - osd_file::remove(*output_bin_file_str); + for (auto const &output_bin_filename : output_bin_filenames) + osd_file::remove(output_bin_filename); osd_file::remove(*output_file_str->second); throw; }