From 38a9d0ec489925eb25b8a0c837b5230da4eb8a68 Mon Sep 17 00:00:00 2001 From: "B. Watson" Date: Mon, 11 May 2020 21:43:44 -0400 Subject: add cuerecover --- Makefile | 26 ++-- README | 14 +- check_version | 8 + cuerecover.1 | 158 +++++++++++++++++++ cuerecover.c | 311 ++++++++++++++++++++++++++++++++++++++ cuerecover.html | 462 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ cuerecover.rst | 139 +++++++++++++++++ 7 files changed, 1106 insertions(+), 12 deletions(-) create mode 100644 cuerecover.1 create mode 100644 cuerecover.c create mode 100644 cuerecover.html create mode 100644 cuerecover.rst diff --git a/Makefile b/Makefile index 0cb352c..9284898 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,10 @@ RST2HTML=rst2html4.py PROJ=miragextract VERSION=0.1.0 -DOCS=README FAQ ChangeLog LICENSE $(PROJ).html +BINS=$(PROJ) cuerecover +MANS=$(PROJ).1 cuerecover.1 +HTMLS=$(PROJ).html cuerecover.html +DOCS=README FAQ ChangeLog LICENSE $(PROJ).html cuerecover.html SNDFILE_CFLAGS:=$(shell pkg-config --cflags sndfile) SNDFILE_LIBS:=$(shell pkg-config --libs sndfile) @@ -47,20 +50,25 @@ SNDFILE_LIBS:=$(shell pkg-config --libs sndfile) MIRAGE_CFLAGS:=$(shell pkg-config --cflags libmirage) MIRAGE_LIBS:=$(shell pkg-config --libs libmirage) -CFLAGS=-Wall $(OPTFLAGS) $(MIRAGE_CFLAGS) $(SNDFILE_CFLAGS) -DVERSION=\"$(VERSION)\" +PROJCFLAGS=-Wall $(OPTFLAGS) -DVERSION=\"$(VERSION)\" +CFLAGS=$(PROJCFLAGS) $(MIRAGE_CFLAGS) $(SNDFILE_CFLAGS) LDFLAGS=$(MIRAGE_LIBS) $(SNDFILE_LIBS)$ $(LDEXTRA) -all: $(PROJ) +all: $(BINS) -man: $(PROJ).1 +man: $(MANS) -html: $(PROJ).html +html: $(HTMLS) -$(PROJ).1: $(PROJ).rst - $(RST2MAN) $(PROJ).rst > $(PROJ).1 +# don't need to use libmirage and libsndfile flags with this one +cuerecover: cuerecover.c + $(CC) $(PROJCFLAGS) -o $@ $< -$(PROJ).html: $(PROJ).rst - $(RST2HTML) $(PROJ).rst > $(PROJ).html +%.1: %.rst + $(RST2MAN) $< > $@ || rm -f $@ + +%.html: %.rst + $(RST2HTML) $< > $@ || rm -f $@ ifeq ($(shell whoami),root) CHOWN=chown diff --git a/README b/README index 2f4f79d..3942cbb 100644 --- a/README +++ b/README @@ -3,9 +3,14 @@ README for miragextract v0.1.0 Description ----------- -Extracts data and audio tracks from any CD image supported by libmirage. -Data tracks are written as-is, and audio tracks can be written as-is -or converted to wav, flag, or ogg/vorbis (via libsndfile). +A pair of tools for working with CD image files. + +miragextract extracts data and audio tracks from any CD image supported +by libmirage. Data tracks are written as-is, and audio tracks can be +written as-is or converted to wav, flag, or ogg/vorbis (via libsndfile). + +cuerecover attempts to create a .cue file for .bin files whose .cue +files are missing. Prerequisites ------------- @@ -24,6 +29,9 @@ If you're on a system that has separate "dev" or "devel" packages If you for some reason need to rebuild the man and html pages, you'll need docutils (I used 0.14). +If you *just* want cuerecover, you don't need libmirage nor libsndfile. +Run "make cuerecover" in this case. + Installation ------------ To build, run "make" (might be spelled "gmake" on your OS). To install, diff --git a/check_version b/check_version index e42a865..6d7f25c 100644 --- a/check_version +++ b/check_version @@ -18,4 +18,12 @@ if [ "$VER" != "$1" ]; then X=1 fi +VER="$( sed -n '/^\.\. |version| replace:: /s,.* ,,p' cuerecover.rst )" +if [ "$VER" != "$1" ]; then + echo + echo "*** You forgot to update the version in cuerecover.rst" + echo + X=1 +fi + exit $X diff --git a/cuerecover.1 b/cuerecover.1 new file mode 100644 index 0000000..d57408e --- /dev/null +++ b/cuerecover.1 @@ -0,0 +1,158 @@ +.\" Man page generated from reStructuredText. +. +.TH CUERECOVER 1 "2020-05-11" "0.1.0" "Urchlay" +.SH NAME +cuerecover \- generate .cue file for CD image .bin +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.\" RST source for cuerecover(1) man page. Convert with: +. +.\" rst2man.py cuerecover.rst > cuerecover.1 +. +.\" rst2man.py comes from the SBo development/docutils package. +. +.\" note to self: don't forget to check the generated man and html pages +. +.\" into git since we don't want to require our users to have rst2man.py. +. +.SH SYNOPSIS +.sp +cuerecover [\-s sec] \fBbin\-file\fP [\fBbin\-file\fP ...] +.SH DESCRIPTION +.sp +cuerecover attempts to generate a usable cue sheet for CD images which +are missing their .cue (or .ccd, .mds, etc) files. The generated cue +sheet is written to standard output [*], so add e.g. "> filename.cue" +to redirect it to a file. +.sp +If a single .bin file is given, it\(aqs assumed to hold all the tracks (which +might only be one). If multiple .bin files are given, each one is assumed to +represent one track of the same CD image. +.sp +For data tracks, the recovered track should be correct, provided the +bin file wasn\(aqt truncated or otherwise corrupted. +.sp +For audio tracks, silence detection is used to find the start of the track +(the INDEX 01 in the .cue file). +.sp +For audio tracks in a single .bin file, silence detection is used to +find the split points between tracks. This means that in cases where +one track segues into another, the two tracks will be combined in the +resulting cue sheet. Also, if there are long periods of silence within +a single track, this track will be split into two or more tracks. +.sp +[*] \fIOnly\fP the .cue file is written to stdout. Status and progress +messages are printed to standard error. +.SH OPTIONS +.INDENT 0.0 +.TP +.B \-\-help +Print short usage string. +.TP +.BI \-s \ +Minimum amount of silence for detecting the split point between +two audio tracks (default: 2). Argument is in seconds, and +non\-integers are allowed. 0 means to disable splitting tracks: +all the audio tracks will be combined into one in the .cue +sheet. This option is ignored when multiple .bin file arguments +are given, since they\(aqre already split into tracks. +.TP +.BI \-t \ +Silence threshold, 0 to 100. Default is 0. This is +the percentage of non\-zero bytes allowed in a sector for it +to be considered silent. Sometimes audio tracks have random +data in the pregap (before the INDEX 01), which will fool +cuerecover into thinking there\(aqs no pregap. This option can +help with those, but don\(aqt set it too high. +.TP +.B \-v +Verbose mode. Prints (on stderr) some extra messages about what +cuerecover is doing. Probably only of interest to the author. +.UNINDENT +.sp +Always include a space between an option and its argument (e.g. \fB\-s 1\fP, not \fB\-s1\fP). +.SH NOTES +.sp +When reading multiple tracks from the same file, cuerecover makes some +assumptions. These are usually valid, but should be mentioned here: +.INDENT 0.0 +.IP \(bu 2 +The tracks are all MODE1 (data), MODE2 (also data, usually not found) or +CD\-DA (regular CD audio). Extended format CDs like XA or CD+I are not +supported, though you might still get listenable audio tracks from them. +.IP \(bu 2 +If there\(aqs a data track, it will be the only data track, and it will be +track 1. This is almost always the case, since most operating systems +from the CD\-ROM era (and even modern ones) don\(aqt provide access to +data tracks that aren\(aqt the first track on the disc. +.IP \(bu 2 +If there\(aqs a data track, it\(aqs a raw image (2352 bytes per sector, includes +the sync pulse, address, CRC, ECC, etc). If the data track was stored as +\(aqcooked\(aq data (2048 bytes/sector, MODE1/2048 in the original .cue file), +it\(aqll be treated as an audio track. You can check for this by trying to +mount the .bin file as an ISO or HFS image: if it mounts, the first +track is \(aqcooked\(aq. It\(aqll also be obvious if you use the .cue sheet to +extract the image into files: ISO images don\(aqt sound musical at all! +.IP \(bu 2 +cuerecover will generate \fIa\fP cue file, which will be valid... but it may +not match the original (missing) one exactly. This is because cuerecover +has to look for silent sections of the image and use those as split points +for the tracks. If there\(aqs a 3\-second silent section between tracks 2 and +3, is that 1 second of silence at the end of track 2 + 2 seconds of silence +at the start of track 3, or vice versa, or 1.5 seconds each, or...? +.UNINDENT +.\" other sections we might want, uncomment as needed. +. +.\" FILES +. +.\" ===== +. +.\" ENVIRONMENT +. +.\" =========== +. +.SH EXIT STATUS +.sp +As usual, 0 for success, non\-zero for failure. +.\" BUGS +. +.\" ==== +. +.\" EXAMPLES +. +.\" ======== +. +.SH AUTHORS +.sp +cuerecover was written by B. Watson <\fI\%yalhcru@gmail.com\fP> and +released under the WTFPL: Do WTF you want with this. +.SH SEE ALSO +.sp +miragextract(1) +.\" Generated by docutils manpage writer. +. diff --git a/cuerecover.c b/cuerecover.c new file mode 100644 index 0000000..34542d2 --- /dev/null +++ b/cuerecover.c @@ -0,0 +1,311 @@ +/* references: +https://www.gnu.org/software/ccd2cue/manual/html_node/CUE-sheet-format.html +https://wiki.osdev.org/User:Combuster/CDRom_BS +https://en.wikipedia.org/wiki/CD-ROM */ + +#include +#include +#include +#include +#include +#include + +const char *self; + +#define RAW_SECTOR_SIZE 2352 +#define DATA_SECTOR_SIZE 2048 + +unsigned char mode1_sync[] = { + 0x00, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0x00 }; + +double silence_sec = 2.0l; +int silence_frames; /* same as above in 1/75 sec frames */ +int silence_thresh = 0; +int multi_bin = 0; +int verbose = 0; + +void die(const char *msg) { + if(!msg) msg = strerror(errno); + fprintf(stderr, "%s: %s\n", self, msg); + exit(1); +} + +void usage() { + fprintf(stderr, "cuerecover v" VERSION " by B. Watson, WTFPL\n"); + fprintf(stderr, "Usage: %s [-s sec] [-t thresh] bin-file [bin-file ...]\n", self); + fprintf(stderr, "See man page for details\n"); + exit(0); +} + +int frame_is_silent(const char *data) { + int i, nonzero = 0; + for(i=0; i> 4) & 0x0f; + return hi * 10 + lo; +} + +int get_sector_addr(char *buf) { + int mm, ss, ff; + mm = unbcd(buf[12]); + ss = unbcd(buf[13]); + ff = unbcd(buf[14]); + return mm * 75 * 60 + ss * 75 + ff; +} + +const char *mode_string(int mode) { + switch(mode) { + case 1: return "MODE1/2352"; + case 2: return "MODE2/2352"; + default: return "AUDIO"; + } +} + +struct track { + int mode; + int index0; + int index1; +}; + +void process_single(char *filename) { + char buf[RAW_SECTOR_SIZE]; + FILE *f; + int i; + int track = 0; + int sector = 0; + int mode; + int silence_start, in_silence = 0; + struct track tracklist[100]; + struct track *t = tracklist; + + if(!(f = fopen(filename, "rb"))) + die(NULL); + + /* only the first track can be data */ + if(read_sector(f, buf) && (mode = get_sector_mode(buf)) != 0) { + if(verbose) fprintf(stderr, "track %d is data, mode %d\n", track, mode); + t->index0 = sector; + t->index1 = sector; + t->mode = mode; + + /* skip over the data sectors */ + while(read_sector(f, buf) && (get_sector_mode(buf) == mode)) + sector++; + + /* now buf holds the first audio sector, which might or might not + begin with silence */ + t++; track++; + t->mode = 0; + t->index0 = sector; + t->index1 = sector; + } + + if(silence_frames) while(read_sector(f, buf)) { + if(frame_is_silent(buf)) { + if(in_silence) { + /* nothing */ + } else { + t->index0 = sector; + t->index1 = -1; + silence_start = sector; + in_silence = 1; + } + } else { + if(in_silence) { + if((sector - silence_start) >= silence_frames) { + in_silence = 0; + t->index1 = sector; + t++; + track++; + t->mode = 0; + } else { + /* nothing */ + } + } + } + sector++; + } + + fclose(f); + + t = tracklist; + printf("FILE \"%s\" BINARY\r\n", filename); + for(i=0; i<=track; t++, i++) { + if(verbose) { + fprintf(stderr, "track %d, mode %d, index0 %d, index1 %d\n", + i, t->mode, t->index0, t->index1); + } + if(t->index1 > -1) { + printf(" TRACK %02d %s\r\n", i + 1, mode_string(t->mode)); + if(t->index0 < t->index1) { + if(silence_frames && t->index1 - t->index0 > silence_frames) { + t->index0 = t->index1 - silence_frames; + if(verbose) + fprintf(stderr, " (index0 adjusted to %d)\n", t->index0); + } + printf(" INDEX 00 %s\r\n", frame2time(t->index0)); + } + printf(" INDEX 01 %s\r\n", frame2time(t->index1)); + } + } +} + +void process_multi(char **filenames) { + int track = 1; + char buf[RAW_SECTOR_SIZE]; + FILE *f; + + while(*filenames) { + int index0 = 0, index1 = -1; + int mode = -1; + int sector = 0; + + if(verbose) fprintf(stderr, "reading %s\n", *filenames); + if(!(f = fopen(*filenames, "rb"))) + die(NULL); + + printf("FILE \"%s\" BINARY\r\n", *filenames); + + while(read_sector(f, buf)) { + mode = get_sector_mode(buf); + + /* if first sector is data, there's no index 01, we're done */ + if(mode > 0) { + index0 = 0; + index1 = -1; + break; + } + + /* for audio, keep reading silence until we hit sound */ + if(mode == 0 && index1 == -1 && !frame_is_silent(buf)) { + index1 = (sector / 75) * 75; /* round to integer number of seconds */ + break; + } + + sector++; + } + fclose(f); + + printf(" TRACK %02d %s\r\n", track, mode_string(mode)); + if(index1 < 1) { + printf(" INDEX 01 %s\r\n", frame2time(index0)); + } else { + printf(" INDEX 00 %s\r\n", frame2time(index0)); + printf(" INDEX 01 %s\r\n", frame2time(index1)); + } + + track++; + filenames++; + } +} + +char **parse_args(int argc, char **argv) { + if(argc == 1) usage(); + if(argc > 1 && strcmp(argv[1], "--help") == 0) usage(); + + while(++argv, --argc) { + if(argv[0][0] == '-') { + char *nextarg = argv[1]; + switch(argv[0][1]) { + case 'v': verbose++; break; + case 's': + if(nextarg && ((*nextarg >= '0' && *nextarg <= '9') || (*nextarg == '.'))) { + silence_sec = atof(nextarg); + argv++, argc--; + } else { + die("-s requires numeric seconds argument"); + } + break; + case 't': + if(nextarg && + (*nextarg >= '0' && *nextarg <= '9') && + (silence_thresh = atoi(nextarg) <= 100)) + { + silence_thresh = (RAW_SECTOR_SIZE / 100.0) * atoi(nextarg); + argv++, argc--; + } else { + die("-t requires numeric percentage argument, 0-100"); + } + break; + default: + fprintf(stderr, "unrecognized option '%s', try --help\n", *argv); + exit(1); + break; + } + } else { + break; + } + } + + silence_frames = (int)(75.0l * silence_sec); + multi_bin = (argc > 1); + return argv; +} + +void set_exe_name(const char *argv0) { + const char *p; + self = argv0; + for(p = self; *p; p++) + if(p[0] == '/' && p[1]) self = p + 1; +} + +int main(int argc, char **argv) { + set_exe_name(argv[0]); + + argv = parse_args(argc, argv); + + if(verbose) fprintf(stderr, "writing %s CUE file to stdout\n", + (multi_bin ? "multi-bin" : "single-bin")); + + if(multi_bin) + process_multi(argv); + else + process_single(*argv); + + return 0; +} diff --git a/cuerecover.html b/cuerecover.html new file mode 100644 index 0000000..3bb5510 --- /dev/null +++ b/cuerecover.html @@ -0,0 +1,462 @@ + + + + + + +cuerecover + + + + +
+

cuerecover

+

generate .cue file for CD image .bin

+ +++ + + + + + + + + + +
Manual section:1
Manual group:Urchlay
Date:2020-05-09
Version:0.1.0
+ + + + + +
+

SYNOPSIS

+

cuerecover [-s sec] bin-file [bin-file ...]

+
+
+

DESCRIPTION

+

cuerecover attempts to generate a usable cue sheet for CD images which +are missing their .cue (or .ccd, .mds, etc) files. The generated cue +sheet is written to standard output.

+

If a single .bin file is given, it's assumed to hold all the tracks (which +might only be one). If multiple .bin files are given, each one is assumed to +represent one track of the same CD image.

+

For data tracks that hold ISO9660, UFS, or Macintosh HFS filesystems, +the recovered track should be correct, provided the .bin file wasn't +truncated or otherwise corrupted.

+

For audio tracks, silence detection is used to find the split points +between tracks. This means that in cases where one track segues into +another, the two tracks will be combined in the resulting cue sheet. Also, +if there are long periods of silence within a single track, this track +will be split into two or more tracks.

+
+
+

OPTIONS

+ +++ + + + + + + + + + +
+--helpPrint short usage string.
+-aAssume all tracks are audio. Disable data track detection. Trying +to listen to a data track as audio will usually give you a +headache.
+-dOnly include data track(s) in the .cue file.
+-sMinimum amount of silence for detecting the split point between +two audio tracks (default: 2). Argument is in seconds, and +non-integers are allowed. 0 means to disable splitting tracks: +all the audio tracks will be combined into one in the .cue +sheet. 2. This option is ignored when multiple .bin file arguments +are given, since they're already split into tracks.
+

Always include a space between an option and its argument (e.g. -s 1, not -s1).

+ + + + + + + +
+
+

EXIT STATUS

+

As usual, 0 for success, non-zero for failure.

+ + + + +
+
+

AUTHORS

+

cuerecover was written by B. Watson <yalhcru@gmail.com> and +released under the WTFPL: Do WTF you want with this.

+
+
+

SEE ALSO

+

miragextract(1)

+
+
+ + diff --git a/cuerecover.rst b/cuerecover.rst new file mode 100644 index 0000000..d4d932b --- /dev/null +++ b/cuerecover.rst @@ -0,0 +1,139 @@ +.. RST source for cuerecover(1) man page. Convert with: +.. rst2man.py cuerecover.rst > cuerecover.1 +.. rst2man.py comes from the SBo development/docutils package. + +.. note to self: don't forget to check the generated man and html pages +.. into git since we don't want to require our users to have rst2man.py. + +.. |version| replace:: 0.1.0 +.. |date| date:: + +========== +cuerecover +========== + +------------------------------------ +generate .cue file for CD image .bin +------------------------------------ + +:Manual section: 1 +:Manual group: Urchlay +:Date: |date| +:Version: |version| + +SYNOPSIS +======== + +cuerecover [-s sec] **bin-file** [**bin-file** ...] + +DESCRIPTION +=========== + +cuerecover attempts to generate a usable cue sheet for CD images which +are missing their .cue (or .ccd, .mds, etc) files. The generated cue +sheet is written to standard output [*], so add e.g. "> filename.cue" +to redirect it to a file. + +If a single .bin file is given, it's assumed to hold all the tracks (which +might only be one). If multiple .bin files are given, each one is assumed to +represent one track of the same CD image. + +For data tracks, the recovered track should be correct, provided the +bin file wasn't truncated or otherwise corrupted. + +For audio tracks, silence detection is used to find the start of the track +(the INDEX 01 in the .cue file). + +For audio tracks in a single .bin file, silence detection is used to +find the split points between tracks. This means that in cases where +one track segues into another, the two tracks will be combined in the +resulting cue sheet. Also, if there are long periods of silence within +a single track, this track will be split into two or more tracks. + +[*] *Only* the .cue file is written to stdout. Status and progress +messages are printed to standard error. + +OPTIONS +======= + +--help + Print short usage string. + +-s Minimum amount of silence for detecting the split point between + two audio tracks (default: 2). Argument is in seconds, and + non-integers are allowed. 0 means to disable splitting tracks: + all the audio tracks will be combined into one in the .cue + sheet. This option is ignored when multiple .bin file arguments + are given, since they're already split into tracks. + +-t Silence threshold, 0 to 100. Default is 0. This is + the percentage of non-zero bytes allowed in a sector for it + to be considered silent. Sometimes audio tracks have random + data in the pregap (before the INDEX 01), which will fool + cuerecover into thinking there's no pregap. This option can + help with those, but don't set it too high. + +-v Verbose mode. Prints (on stderr) some extra messages about what + cuerecover is doing. Probably only of interest to the author. + +Always include a space between an option and its argument (e.g. **-s 1**, not **-s1**). + +NOTES +===== + +When reading multiple tracks from the same file, cuerecover makes some +assumptions. These are usually valid, but should be mentioned here: + +- The tracks are all MODE1 (data), MODE2 (also data, usually video CDs) or + CD-DA (regular CD audio). Extended format CDs like XA or CD+I are not + supported, though you might still get listenable audio tracks from them. + +- If there's a data track, it will be the only data track, and it will be + track 1. This is almost always the case, since most operating systems + from the CD-ROM era (and even modern ones) don't provide access to + data tracks that aren't the first track on the disc. + +- If there's a data track, it's a raw image (2352 bytes per sector, includes + the sync pulse, address, CRC, ECC, etc). If the data track was stored as + 'cooked' data (2048 bytes/sector, MODE1/2048 in the original .cue file), + it'll be treated as an audio track. You can check for this by trying to + mount the .bin file as an ISO or HFS image: if it mounts, the first + track is 'cooked'. It'll also be obvious if you use the .cue sheet to + extract the image into files: ISO images don't sound musical at all! + +- cuerecover will generate *a* cue file, which will be valid... but it may + not match the original (missing) one exactly. This is because cuerecover + has to look for silent sections of the image and use those as split points + for the tracks. If there's a 3-second silent section between tracks 2 and + 3, is that 1 second of silence at the end of track 2 + 2 seconds of silence + at the start of track 3, or vice versa, or 1.5 seconds each, or...? + +.. other sections we might want, uncomment as needed. + +.. FILES +.. ===== + +.. ENVIRONMENT +.. =========== + +EXIT STATUS +=========== + +As usual, 0 for success, non-zero for failure. + +.. BUGS +.. ==== + +.. EXAMPLES +.. ======== + +AUTHORS +======= + +cuerecover was written by B. Watson and +released under the WTFPL: Do WTF you want with this. + +SEE ALSO +======== + +miragextract(1) -- cgit v1.2.3