aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile26
-rw-r--r--README14
-rw-r--r--check_version8
-rw-r--r--cuerecover.1158
-rw-r--r--cuerecover.c311
-rw-r--r--cuerecover.html462
-rw-r--r--cuerecover.rst139
7 files changed, 1106 insertions, 12 deletions
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 \ <sec>
+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 \ <thresh>
+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 <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <limits.h>
+#include <string.h>
+#include <errno.h>
+
+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<RAW_SECTOR_SIZE; i++)
+ if(data[i]) nonzero++;
+
+ if(nonzero <= silence_thresh)
+ return 1;
+ return 0;
+}
+
+const char *frame2time(int frame) {
+ static char buf[10];
+ int min, sec;
+
+ min = frame / (75 * 60);
+ frame %= (75 * 60);
+ sec = frame / 75;
+ frame %= 75;
+
+ sprintf(buf, "%02d:%02d:%02d", min, sec, frame);
+
+ return buf;
+}
+
+int read_sector(FILE *f, char *buf) {
+ /*
+ if(verbose)
+ fprintf(stderr, "read_sector at pos %d\n", ftell(f));
+ */
+ return fread(buf, RAW_SECTOR_SIZE, 1, f);
+}
+
+int has_mode1_sync(char *buf) {
+ return (memcmp(buf, mode1_sync, sizeof(mode1_sync)) == 0);
+}
+
+int get_sector_mode(char *buf) {
+ if(!has_mode1_sync(buf))
+ return 0; /* audio, or something we don't grok */
+
+ return buf[15];
+}
+
+int unbcd(int bcd) {
+ int hi, lo;
+
+ lo = bcd & 0x0f;
+ hi = (bcd >> 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 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+<meta name="generator" content="Docutils 0.14: http://docutils.sourceforge.net/" />
+<title>cuerecover</title>
+<meta name="date" content="2020-05-09" />
+<style type="text/css">
+
+/*
+:Author: David Goodger (goodger@python.org)
+:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
+:Copyright: This stylesheet has been placed in the public domain.
+
+Default cascading style sheet for the HTML output of Docutils.
+
+See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
+customize this style sheet.
+*/
+
+/* used to remove borders from tables and images */
+.borderless, table.borderless td, table.borderless th {
+ border: 0 }
+
+table.borderless td, table.borderless th {
+ /* Override padding for "table.docutils td" with "! important".
+ The right padding separates the table cells. */
+ padding: 0 0.5em 0 0 ! important }
+
+.first {
+ /* Override more specific margin styles with "! important". */
+ margin-top: 0 ! important }
+
+.last, .with-subtitle {
+ margin-bottom: 0 ! important }
+
+.hidden {
+ display: none }
+
+.subscript {
+ vertical-align: sub;
+ font-size: smaller }
+
+.superscript {
+ vertical-align: super;
+ font-size: smaller }
+
+a.toc-backref {
+ text-decoration: none ;
+ color: black }
+
+blockquote.epigraph {
+ margin: 2em 5em ; }
+
+dl.docutils dd {
+ margin-bottom: 0.5em }
+
+object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
+ overflow: hidden;
+}
+
+/* Uncomment (and remove this text!) to get bold-faced definition list terms
+dl.docutils dt {
+ font-weight: bold }
+*/
+
+div.abstract {
+ margin: 2em 5em }
+
+div.abstract p.topic-title {
+ font-weight: bold ;
+ text-align: center }
+
+div.admonition, div.attention, div.caution, div.danger, div.error,
+div.hint, div.important, div.note, div.tip, div.warning {
+ margin: 2em ;
+ border: medium outset ;
+ padding: 1em }
+
+div.admonition p.admonition-title, div.hint p.admonition-title,
+div.important p.admonition-title, div.note p.admonition-title,
+div.tip p.admonition-title {
+ font-weight: bold ;
+ font-family: sans-serif }
+
+div.attention p.admonition-title, div.caution p.admonition-title,
+div.danger p.admonition-title, div.error p.admonition-title,
+div.warning p.admonition-title, .code .error {
+ color: red ;
+ font-weight: bold ;
+ font-family: sans-serif }
+
+/* Uncomment (and remove this text!) to get reduced vertical space in
+ compound paragraphs.
+div.compound .compound-first, div.compound .compound-middle {
+ margin-bottom: 0.5em }
+
+div.compound .compound-last, div.compound .compound-middle {
+ margin-top: 0.5em }
+*/
+
+div.dedication {
+ margin: 2em 5em ;
+ text-align: center ;
+ font-style: italic }
+
+div.dedication p.topic-title {
+ font-weight: bold ;
+ font-style: normal }
+
+div.figure {
+ margin-left: 2em ;
+ margin-right: 2em }
+
+div.footer, div.header {
+ clear: both;
+ font-size: smaller }
+
+div.line-block {
+ display: block ;
+ margin-top: 1em ;
+ margin-bottom: 1em }
+
+div.line-block div.line-block {
+ margin-top: 0 ;
+ margin-bottom: 0 ;
+ margin-left: 1.5em }
+
+div.sidebar {
+ margin: 0 0 0.5em 1em ;
+ border: medium outset ;
+ padding: 1em ;
+ background-color: #ffffee ;
+ width: 40% ;
+ float: right ;
+ clear: right }
+
+div.sidebar p.rubric {
+ font-family: sans-serif ;
+ font-size: medium }
+
+div.system-messages {
+ margin: 5em }
+
+div.system-messages h1 {
+ color: red }
+
+div.system-message {
+ border: medium outset ;
+ padding: 1em }
+
+div.system-message p.system-message-title {
+ color: red ;
+ font-weight: bold }
+
+div.topic {
+ margin: 2em }
+
+h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
+h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
+ margin-top: 0.4em }
+
+h1.title {
+ text-align: center }
+
+h2.subtitle {
+ text-align: center }
+
+hr.docutils {
+ width: 75% }
+
+img.align-left, .figure.align-left, object.align-left, table.align-left {
+ clear: left ;
+ float: left ;
+ margin-right: 1em }
+
+img.align-right, .figure.align-right, object.align-right, table.align-right {
+ clear: right ;
+ float: right ;
+ margin-left: 1em }
+
+img.align-center, .figure.align-center, object.align-center {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+table.align-center {
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.align-left {
+ text-align: left }
+
+.align-center {
+ clear: both ;
+ text-align: center }
+
+.align-right {
+ text-align: right }
+
+/* reset inner alignment in figures */
+div.align-right {
+ text-align: inherit }
+
+/* div.align-center * { */
+/* text-align: left } */
+
+.align-top {
+ vertical-align: top }
+
+.align-middle {
+ vertical-align: middle }
+
+.align-bottom {
+ vertical-align: bottom }
+
+ol.simple, ul.simple {
+ margin-bottom: 1em }
+
+ol.arabic {
+ list-style: decimal }
+
+ol.loweralpha {
+ list-style: lower-alpha }
+
+ol.upperalpha {
+ list-style: upper-alpha }
+
+ol.lowerroman {
+ list-style: lower-roman }
+
+ol.upperroman {
+ list-style: upper-roman }
+
+p.attribution {
+ text-align: right ;
+ margin-left: 50% }
+
+p.caption {
+ font-style: italic }
+
+p.credits {
+ font-style: italic ;
+ font-size: smaller }
+
+p.label {
+ white-space: nowrap }
+
+p.rubric {
+ font-weight: bold ;
+ font-size: larger ;
+ color: maroon ;
+ text-align: center }
+
+p.sidebar-title {
+ font-family: sans-serif ;
+ font-weight: bold ;
+ font-size: larger }
+
+p.sidebar-subtitle {
+ font-family: sans-serif ;
+ font-weight: bold }
+
+p.topic-title {
+ font-weight: bold }
+
+pre.address {
+ margin-bottom: 0 ;
+ margin-top: 0 ;
+ font: inherit }
+
+pre.literal-block, pre.doctest-block, pre.math, pre.code {
+ margin-left: 2em ;
+ margin-right: 2em }
+
+pre.code .ln { color: grey; } /* line numbers */
+pre.code, code { background-color: #eeeeee }
+pre.code .comment, code .comment { color: #5C6576 }
+pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
+pre.code .literal.string, code .literal.string { color: #0C5404 }
+pre.code .name.builtin, code .name.builtin { color: #352B84 }
+pre.code .deleted, code .deleted { background-color: #DEB0A1}
+pre.code .inserted, code .inserted { background-color: #A3D289}
+
+span.classifier {
+ font-family: sans-serif ;
+ font-style: oblique }
+
+span.classifier-delimiter {
+ font-family: sans-serif ;
+ font-weight: bold }
+
+span.interpreted {
+ font-family: sans-serif }
+
+span.option {
+ white-space: nowrap }
+
+span.pre {
+ white-space: pre }
+
+span.problematic {
+ color: red }
+
+span.section-subtitle {
+ /* font-size relative to parent (h1..h6 element) */
+ font-size: 80% }
+
+table.citation {
+ border-left: solid 1px gray;
+ margin-left: 1px }
+
+table.docinfo {
+ margin: 2em 4em }
+
+table.docutils {
+ margin-top: 0.5em ;
+ margin-bottom: 0.5em }
+
+table.footnote {
+ border-left: solid 1px black;
+ margin-left: 1px }
+
+table.docutils td, table.docutils th,
+table.docinfo td, table.docinfo th {
+ padding-left: 0.5em ;
+ padding-right: 0.5em ;
+ vertical-align: top }
+
+table.docutils th.field-name, table.docinfo th.docinfo-name {
+ font-weight: bold ;
+ text-align: left ;
+ white-space: nowrap ;
+ padding-left: 0 }
+
+/* "booktabs" style (no vertical lines) */
+table.docutils.booktabs {
+ border: 0px;
+ border-top: 2px solid;
+ border-bottom: 2px solid;
+ border-collapse: collapse;
+}
+table.docutils.booktabs * {
+ border: 0px;
+}
+table.docutils.booktabs th {
+ border-bottom: thin solid;
+ text-align: left;
+}
+
+h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
+h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
+ font-size: 100% }
+
+ul.auto-toc {
+ list-style-type: none }
+
+</style>
+</head>
+<body>
+<div class="document" id="cuerecover">
+<h1 class="title">cuerecover</h1>
+<h2 class="subtitle" id="generate-cue-file-for-cd-image-bin">generate .cue file for CD image .bin</h2>
+<table class="docinfo" frame="void" rules="none">
+<col class="docinfo-name" />
+<col class="docinfo-content" />
+<tbody valign="top">
+<tr class="manual-section field"><th class="docinfo-name">Manual section:</th><td class="field-body">1</td>
+</tr>
+<tr class="manual-group field"><th class="docinfo-name">Manual group:</th><td class="field-body">Urchlay</td>
+</tr>
+<tr><th class="docinfo-name">Date:</th>
+<td>2020-05-09</td></tr>
+<tr><th class="docinfo-name">Version:</th>
+<td>0.1.0</td></tr>
+</tbody>
+</table>
+<!-- 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. -->
+<div class="section" id="synopsis">
+<h1>SYNOPSIS</h1>
+<p>cuerecover [-s sec] <strong>bin-file</strong> [<strong>bin-file</strong> ...]</p>
+</div>
+<div class="section" id="description">
+<h1>DESCRIPTION</h1>
+<p>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.</p>
+<p>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.</p>
+<p>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.</p>
+<p>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.</p>
+</div>
+<div class="section" id="options">
+<h1>OPTIONS</h1>
+<table class="docutils option-list" frame="void" rules="none">
+<col class="option" />
+<col class="description" />
+<tbody valign="top">
+<tr><td class="option-group">
+<kbd><span class="option">--help</span></kbd></td>
+<td>Print short usage string.</td></tr>
+<tr><td class="option-group">
+<kbd><span class="option">-a</span></kbd></td>
+<td>Assume all tracks are audio. Disable data track detection. Trying
+to listen to a data track as audio will usually give you a
+headache.</td></tr>
+<tr><td class="option-group">
+<kbd><span class="option">-d</span></kbd></td>
+<td>Only include data track(s) in the .cue file.</td></tr>
+<tr><td class="option-group">
+<kbd><span class="option">-s</span></kbd></td>
+<td>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. 2. This option is ignored when multiple .bin file arguments
+are given, since they're already split into tracks.</td></tr>
+</tbody>
+</table>
+<p>Always include a space between an option and its argument (e.g. <strong>-s 1</strong>, not <strong>-s1</strong>).</p>
+<!-- NOTES -->
+<!-- ===== -->
+<!-- other sections we might want, uncomment as needed. -->
+<!-- FILES -->
+<!-- ===== -->
+<!-- ENVIRONMENT -->
+<!-- =========== -->
+</div>
+<div class="section" id="exit-status">
+<h1>EXIT STATUS</h1>
+<p>As usual, 0 for success, non-zero for failure.</p>
+<!-- BUGS -->
+<!-- ==== -->
+<!-- EXAMPLES -->
+<!-- ======== -->
+</div>
+<div class="section" id="authors">
+<h1>AUTHORS</h1>
+<p>cuerecover was written by B. Watson &lt;<a class="reference external" href="mailto:yalhcru&#64;gmail.com">yalhcru&#64;gmail.com</a>&gt; and
+released under the WTFPL: Do WTF you want with this.</p>
+</div>
+<div class="section" id="see-also">
+<h1>SEE ALSO</h1>
+<p>miragextract(1)</p>
+</div>
+</div>
+</body>
+</html>
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 <sec> 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 <thresh> 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 <yalhcru@gmail.com> and
+released under the WTFPL: Do WTF you want with this.
+
+SEE ALSO
+========
+
+miragextract(1)