summaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
-rw-r--r--docs/source/tools/chdman.rst1
-rw-r--r--src/tools/chdman.cpp381
2 files changed, 248 insertions, 134 deletions
diff --git a/docs/source/tools/chdman.rst b/docs/source/tools/chdman.rst
index 04612642764..cbf16fcba75 100644
--- a/docs/source/tools/chdman.rst
+++ b/docs/source/tools/chdman.rst
@@ -319,6 +319,7 @@ Common options supported:
Additional options:
* ``--outputbin <file>`` / ``-ob <file>``
+* ``--splitbin`` / ``-sb``
extractdvd
~~~~~~~~~~
diff --git a/src/tools/chdman.cpp b/src/tools/chdman.cpp
index 4ef41387117..639aec22a12 100644
--- a/src/tools/chdman.cpp
+++ b/src/tools/chdman.cpp
@@ -30,8 +30,9 @@
#include <iostream>
#include <limits>
#include <memory>
-#include <optional>
#include <new>
+#include <optional>
+#include <regex>
#include <string_view>
#include <tuple>
#include <unordered_map>
@@ -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, " <filename>: parent file name for input CHD" },
{ OPTION_OUTPUT, "o", true, " <filename>: output file name" },
{ OPTION_OUTPUT_BIN, "ob", true, " <filename>: 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, " <filename>: parent file name for output CHD" },
{ OPTION_INPUT_START_BYTE, "isb", true, " <offset>: 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 &params)
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<std::string> track_filenames;
util::core_file::ptr output_bin_file;
util::core_file::ptr output_toc_file;
+ std::vector<std::string> 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 &params)
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<uint8_t> 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 &params)
// 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 &params)
}
}
- 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 &params)
// 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;
}