/* 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 #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; char *outfile = NULL; 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 looks_like_filesystem(const char *filename, FILE *f) { int have_apple_magic = 0, have_iso_magic = 0; char buf[2048]; if(fseek(f, 0L, SEEK_SET) < 0) die(NULL); if(!fread(buf, 2048, 1, f)) { fprintf(stderr, "%s: file %s is too short to be a CD image file\n", self, filename); return 0; } if(buf[0] == 'E' && buf[1] == 'C' && buf[2] == 'M') { fprintf(stderr, "%s: file %s is ECM, use 'ecm2bin' to convert to raw bin\n", self, filename); exit(1); } if(buf[0] == 'E' && buf[1] == 'R' && buf[3] == 0 && (buf[2] == 0x02 || buf[2] == 0x08)) { have_apple_magic++; } if(fseek(f, 16 * 2048L, SEEK_SET) < 0) die(NULL); if(!fread(buf, 2048, 1, f)) { fprintf(stderr, "%s: file %s is too short to be a CD image file\n", self, filename); return 0; } if(strncmp("\x01" "CD001", buf, 6) == 0) { have_iso_magic++; } if(fseek(f, 0L, SEEK_SET) < 0) die(NULL); if(have_apple_magic && have_iso_magic) { fprintf(stderr, "%s: file %s looks like hybrid ISO/HFS image\n", self, filename); } else if(have_apple_magic) { fprintf(stderr, "%s: file %s looks like Mac HFS image\n", self, filename); } else if(have_iso_magic) { fprintf(stderr, "%s: file %s looks like ISO9660 image\n", self, filename); } return have_apple_magic || have_iso_magic; } 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); if(looks_like_filesystem(filename, f)) { fprintf(stderr, "%s: %s looks like 'cooked' data!\n", self, filename); /* // don't know all the details on how this works yet int skipped_sectors = 0; fprintf(stderr, "%s: %s looks like 'cooked' data, ", self, filename); if((skipped_sectors = skip_fs(filename, f))) { fprintf(stderr, "skipped %d 2048-byte sectors\n", skipped_sectors); } else { fprintf(stderr, "but not ISO9660, don't know how to skip it\n"); } */ } /* 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++; if(track > 99) { fprintf(stderr, "%s: too many tracks (>99), skipping rest of file\n", self); break; } 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) { char *filename; int track = 0; char buf[RAW_SECTOR_SIZE]; FILE *f; while((filename = *filenames++)) { int index0 = 0, index1 = -1; int mode = -1; int sector = 0; track++; if(verbose) fprintf(stderr, "reading %s\n", filename); if(!(f = fopen(filename, "rb"))) die(NULL); printf("FILE \"%s\" BINARY\r\n", filename); if(looks_like_filesystem(filename, f)) { fprintf(stderr, "%s: %s looks like 'cooked' data, assuming MODE1/2048\n", self, filename); printf(" TRACK %02d MODE1/2048\r\n", track); printf(" INDEX 01 00:00:00\r\n"); continue; } 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)); } } } 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 'o': if(nextarg) outfile = nextarg; else die("-o requires an output filename"); argv++, argc--; 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 redirect_output(void) { struct stat ss; if(lstat(outfile, &ss) >= 0) die("output file exists, not overwriting"); if(!(freopen(outfile, "w", stdout))) die(NULL); } 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 %s\n", (multi_bin ? "multi-bin" : "single-bin"), (outfile ? outfile : "")); if(outfile) redirect_output(); if(multi_bin) process_multi(argv); else process_single(*argv); return 0; }