diff cdimage.c @ 2114:2449c88cea36

Enhance support for CUE files and add initial support for cdrdao TOC files
author Michael Pavone <pavone@retrodev.com>
date Wed, 16 Feb 2022 21:22:12 -0800
parents cue.c@0db3af42dd72
children cd057d6fe030
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cdimage.c	Wed Feb 16 21:22:12 2022 -0800
@@ -0,0 +1,518 @@
+#include <ctype.h>
+#include <string.h>
+
+#include "system.h"
+#include "util.h"
+
+static char* cmd_start(char *cur)
+{
+	while (*cur && isblank(*cur))
+	{
+		cur++;
+	}
+	return cur;
+}
+
+static char* cmd_start_sameline(char *cur)
+{
+	while (*cur && isblank(*cur) && *cur != '\n')
+	{
+		cur++;
+	}
+	return cur;
+}
+
+static char* word_end(char *cur)
+{
+	while (*cur && !isblank(*cur))
+	{
+		cur++;
+	}
+	return cur;
+}
+
+static char* next_line(char *cur)
+{
+	while (*cur && *cur != '\n')
+	{
+		cur++;
+	}
+	if (*cur) {
+		return cur + 1;
+	}
+	return NULL;
+}
+
+static char* next_blank(char *cur)
+{
+	while (*cur && !isblank(*cur))
+	{
+		cur++;
+	}
+	return cur;
+}
+
+static uint32_t timecode_to_lba(char *timecode)
+{
+	char *end;
+	int seconds = 0, frames = 0;
+	int minutes = strtol(timecode, &end, 10);
+	if (end) {
+		timecode = end + 1;
+		seconds = strtol(timecode, &end, 10);
+		if (end) {
+			timecode = end + 1;
+			frames = strtol(timecode, NULL, 10);
+		}
+	}
+	seconds += minutes * 60;
+	return seconds * 75 + frames;
+
+}
+
+enum {
+	FAKE_DATA = 1,
+	FAKE_AUDIO,
+};
+
+static uint8_t bin_seek(system_media *media, uint32_t sector)
+{
+	media->cur_sector = sector;
+	uint32_t lba = sector;
+	uint32_t track;
+	for (track = 0; track < media->num_tracks; track++)
+	{
+		if (lba < media->tracks[track].fake_pregap) {
+			media->in_fake_pregap = media->tracks[track].type == TRACK_DATA ? FAKE_DATA : FAKE_AUDIO;
+			break;
+		}
+		lba -= media->tracks[track].fake_pregap;
+		if (lba < media->tracks[track].start_lba) {
+			if (media->tracks[track].fake_pregap) {
+				media->in_fake_pregap = media->tracks[track].type == TRACK_DATA ? FAKE_DATA : FAKE_AUDIO;
+			} else {
+				media->in_fake_pregap = 0;
+			}
+			break;
+		}
+		if (lba < media->tracks[track].end_lba) {
+			media->in_fake_pregap = 0;
+			break;
+		}
+	}
+	if (track < media->num_tracks) {
+		fprintf(stderr, "bin_seek to sector %u, adjusted lba %u, track %u\n", sector, lba, track);
+		media->cur_track = track;
+		if (!media->in_fake_pregap) {
+			if (track) {
+				lba -= media->tracks[track - 1].end_lba;
+			}
+			fseek(media->tracks[track].f, media->tracks[track].file_offset + lba * media->tracks[track].sector_bytes, SEEK_SET);
+		}
+	}
+	return track;
+}
+
+static uint8_t fake_read(uint32_t sector, uint32_t offset)
+{
+	if (!offset || (offset >= 16)) {
+		return 0;
+		//TODO: error detection and correction bytes
+	} else if (offset < 12) {
+		return 0xFF;
+	} else if (offset == 12) {
+		uint32_t minute = (sector / 75) / 60;
+		return (minute % 10) | ((minute / 10 ) << 4);
+	} else if (offset == 13) {
+		uint32_t seconds = (sector / 75) % 60;
+		return (seconds % 10) | ((seconds / 10 ) << 4);
+	} else if (offset == 14) {
+		uint32_t frames = sector % 75;
+		return (frames % 10) | ((frames / 10 ) << 4);
+	} else {
+		return 1;
+	}
+}
+
+static uint8_t bin_read(system_media *media, uint32_t offset)
+{
+	if (media->in_fake_pregap == FAKE_DATA) {
+		return fake_read(media->cur_sector, offset);
+	} else if (media->in_fake_pregap == FAKE_AUDIO) {
+		return 0;
+	} else if ((media->tracks[media->cur_track].sector_bytes < 2352 && offset < 16) || offset > (media->tracks[media->cur_track].sector_bytes + 16)) {
+		return fake_read(media->cur_sector, offset);
+	} else {
+		if (media->tracks[media->cur_track].need_swap) {
+			if (offset & 1) {
+				return media->byte_storage;
+			}
+			media->byte_storage = fgetc(media->tracks[media->cur_track].f);
+		}
+		return fgetc(media->tracks[media->cur_track].f);
+	}
+}
+
+uint8_t parse_cue(system_media *media)
+{
+	char *line = media->buffer;
+	media->num_tracks = 0;
+	do {
+		char *cmd = cmd_start(line);
+		if (cmd) {
+			if (startswith(cmd, "TRACK ")) {
+				media->num_tracks++;
+			}
+			line = next_line(cmd);
+		} else {
+			line = NULL;
+		}
+	} while (line);
+	track_info *tracks = calloc(sizeof(track_info), media->num_tracks);
+	media->tracks = tracks;
+	line = media->buffer;
+	int track = -1;
+	uint8_t audio_byte_swap = 0;
+	FILE *f = NULL;
+	int track_of_file = -1;
+	uint8_t has_index_0 = 0;
+	do {
+		char *cmd = cmd_start(line);
+		if (*cmd) {
+			if (startswith(cmd, "TRACK ")) {
+				track++;
+				track_of_file++;
+				has_index_0 = 0;
+				cmd += 6;
+				char *end;
+				int file_track = strtol(cmd, &end, 10);
+				if (file_track != (track + 1)) {
+					warning("Expected track %d, but found track %d in CUE sheet\n", track + 1, file_track);
+				}
+				tracks[track].f = f;
+
+
+				cmd = cmd_start(end);
+				if (*cmd) {
+					if (startswith(cmd, "AUDIO")) {
+						tracks[track].type = TRACK_AUDIO;
+						tracks[track].need_swap = audio_byte_swap;
+						tracks[track].sector_bytes = 2352;
+					} else {
+						tracks[track].type = TRACK_DATA;
+						tracks[track].need_swap = 0;
+						tracks[track].sector_bytes = 0;
+						char *slash = strchr(cmd, '/');
+						if (slash) {
+							tracks[track].sector_bytes = atoi(slash+1);
+						}
+						if (!tracks[track].sector_bytes) {
+							warning("Missing sector size for data track %d in cue", track + 1);
+							tracks[track].sector_bytes = 2352;
+						}
+					}
+
+				}
+			} else if (startswith(cmd, "FILE ")) {
+				cmd += 5;
+				cmd = strchr(cmd, '"');
+				if (cmd) {
+					cmd++;
+					char *end = strchr(cmd, '"');
+					if (end) {
+						char *fname;
+						//TODO: zipped BIN/CUE support
+						if (is_absolute_path(cmd)) {
+							fname = malloc(end-cmd + 1);
+							memcpy(fname, cmd, end-cmd);
+							fname[end-cmd] = 0;
+						} else {
+							size_t dirlen = strlen(media->dir);
+							fname = malloc(dirlen + 1 + (end-cmd) + 1);
+							memcpy(fname, media->dir, dirlen);
+							fname[dirlen] = PATH_SEP[0];
+							memcpy(fname + dirlen + 1, cmd, end-cmd);
+							fname[dirlen + 1 + (end-cmd)] = 0;
+						}
+						f = fopen(fname, "rb");
+						if (!f) {
+							fatal_error("Failed to open %s specified by FILE command in CUE sheet %s.%s\n", fname, media->name, media->extension);
+						}
+						free(fname);
+						track_of_file = -1;
+						for (end++; *end && *end != '\n' && *end != '\r'; end++)
+						{
+							if (!isspace(*end)) {
+								if (startswith(end, "BINARY")) {
+									audio_byte_swap = 0;
+								} else if (startswith(end, "MOTOROLA")) {
+									audio_byte_swap = 1;
+								} else {
+									warning("Unsupported FILE type in CUE sheet. Only BINARY and MOTOROLA are supported\n");
+								}
+								break;
+							}
+						}
+					}
+				}
+			} else if (track >= 0) {
+				if (startswith(cmd, "PREGAP ")) {
+					tracks[track].fake_pregap = timecode_to_lba(cmd + 7);
+				} else if (startswith(cmd, "INDEX ")) {
+					char *after;
+					int index = strtol(cmd + 6, &after, 10);
+					uint8_t has_start_lba = 0;
+					uint32_t start_lba;
+					if (!index) {
+						tracks[track].pregap_lba = start_lba = timecode_to_lba(after);
+						has_index_0 = 1;
+						has_start_lba = 1;
+					} else if (index == 1) {
+						tracks[track].start_lba = timecode_to_lba(after);
+						if (!has_index_0) {
+							start_lba = tracks[track].start_lba;
+							has_start_lba = 1;
+						}
+					}
+					if (has_start_lba) {
+						if (track > 0) {
+							tracks[track-1].end_lba = start_lba;
+						}
+						if (track_of_file > 0) {
+							tracks[track].file_offset = tracks[track-1].file_offset + tracks[track-1].end_lba * tracks[track-1].sector_bytes;
+							if (track_of_file > 1) {
+								tracks[track].file_offset -= tracks[track-2].end_lba * tracks[track-1].sector_bytes;
+							}
+						} else {
+							tracks[track].file_offset = 0;
+						}
+					}
+				}
+			}
+			if (cmd && *cmd) {
+				line = next_line(cmd);
+			} else {
+				line = NULL;
+			}
+		} else {
+			line = NULL;
+		}
+	} while (line);
+	if (media->num_tracks > 0 && media->tracks[0].f) {
+		//end of last track is implicitly defined by file size
+		if (tracks[media->num_tracks-1].f) {
+			uint32_t start_lba =tracks[media->num_tracks-1].pregap_lba ? tracks[media->num_tracks-1].pregap_lba : tracks[media->num_tracks-1].start_lba;
+			tracks[media->num_tracks-1].end_lba = start_lba + (file_size(tracks[media->num_tracks-1].f) - tracks[media->num_tracks-1].file_offset)/ tracks[media->num_tracks-1].sector_bytes;
+		}
+		//replace cue sheet with first sector
+		free(media->buffer);
+		media->buffer = calloc(2048, 1);
+		if (tracks[0].type == TRACK_DATA && tracks[0].sector_bytes == 2352) {
+			// if the first track is a data track, don't trust the CUE sheet and look at the MM:SS:FF from first sector
+			uint8_t msf[3];
+			fseek(tracks[0].f, 12, SEEK_SET);
+			if (sizeof(msf) == fread(msf, 1, sizeof(msf), tracks[0].f)) {
+				tracks[0].fake_pregap = msf[2] + (msf[0] * 60 + msf[1]) * 75;
+			}
+		} else if (!tracks[0].start_lba && !tracks[0].fake_pregap) {
+			tracks[0].fake_pregap = 2 * 75;
+		}
+
+		fseek(tracks[0].f, tracks[0].sector_bytes == 2352 ? 16 : 0, SEEK_SET);
+		media->size = fread(media->buffer, 1, 2048, tracks[0].f);
+		media->seek = bin_seek;
+		media->read = bin_read;
+	}
+	uint8_t valid = media->num_tracks > 0 && media->tracks[0].f != NULL;
+	media->type = valid ? MEDIA_CDROM : MEDIA_CART;
+	return valid;
+}
+
+uint8_t parse_toc(system_media *media)
+{
+	char *line = media->buffer;
+	media->num_tracks = 0;
+	do {
+		char *cmd = cmd_start(line);
+		if (cmd) {
+			if (startswith(cmd, "TRACK ")) {
+				media->num_tracks++;
+			}
+			line = next_line(cmd);
+		} else {
+			line = NULL;
+		}
+	} while (line);
+	track_info *tracks = calloc(sizeof(track_info), media->num_tracks);
+	media->tracks = tracks;
+	line = media->buffer;
+	char *last_file_name = NULL;
+	FILE *f = NULL;
+	int track = -1;
+	do {
+		char *cmd = cmd_start(line);
+		if (*cmd) {
+			if (startswith(cmd, "TRACK ")) {
+				track++;
+				cmd = cmd_start(cmd + 6);
+				if (startswith(cmd, "AUDIO")) {
+					tracks[track].type = TRACK_AUDIO;
+					tracks[track].sector_bytes = 2352;
+					tracks[track].need_swap = 1;
+				} else {
+					tracks[track].type = TRACK_DATA;
+					tracks[track].need_swap = 0;
+					if (startswith(cmd, "MODE1_RAW") || startswith(cmd, "MODE2_RAW")) {
+						tracks[track].sector_bytes = 2352;
+					} else if (startswith(cmd, "MODE2_FORM2")) {
+						tracks[track].sector_bytes = 2324;
+					} else if (startswith(cmd, "MODE1") || startswith(cmd, "MODE2_FORM1")) {
+						tracks[track].sector_bytes = 2048;
+					} else if (startswith(cmd, "MODE2")) {
+						tracks[track].sector_bytes = 2336;
+					}
+				}
+				cmd = word_end(cmd);
+				if (*cmd && *cmd != '\n') {
+					cmd = cmd_start_sameline(cmd);
+					if (*cmd && *cmd != '\n') {
+						//TODO: record whether subcode is in raw format or not
+						if (startswith(cmd, "RW_RAW")) {
+							tracks[track].sector_bytes += 96;
+						} else if (startswith(cmd, "RW")) {
+							tracks[track].sector_bytes += 96;
+						}
+					}
+				}
+				if (track) {
+					tracks[track].start_lba = tracks[track].pregap_lba = tracks[track].end_lba = tracks[track-1].end_lba;
+				}
+			} else if (track >= 0) {
+				uint8_t is_datafile = startswith(cmd, "DATAFILE");
+				if (is_datafile || startswith(cmd, "FILE")) {
+
+					if (tracks[track].f) {
+						warning("TOC file has more than one file for track %d, only one is supported\n", track + 1);
+					} else {
+						cmd += 8;
+						char *fname_start = strchr(cmd, '"');
+						if (fname_start) {
+							++fname_start;
+							char *fname_end = strchr(fname_start, '"');
+							if (fname_end) {
+								if (!last_file_name || strncmp(last_file_name, fname_start, fname_end-fname_start)) {
+									free(last_file_name);
+									last_file_name = calloc(1, 1 + fname_end-fname_start);
+									memcpy(last_file_name, fname_start, fname_end-fname_start);
+									char *fname;
+									//TODO: zipped BIN/TOC support
+									if (is_absolute_path(last_file_name)) {
+										fname = last_file_name;
+									} else {
+										size_t dirlen = strlen(media->dir);
+										fname = malloc(dirlen + 1 + (fname_end-fname_start) + 1);
+										memcpy(fname, media->dir, dirlen);
+										fname[dirlen] = PATH_SEP[0];
+										memcpy(fname + dirlen + 1, fname_start, fname_end-fname_start);
+										fname[dirlen + 1 + (fname_end-fname_start)] = 0;
+									}
+									f = fopen(fname, "rb");
+									if (!f) {
+										fatal_error("Failed to open %s specified by DATAFILE command in TOC file %s.%s\n", fname, media->name, media->extension);
+									}
+									if (fname != last_file_name) {
+										free(fname);
+									}
+								}
+								tracks[track].f = f;
+								cmd = fname_end + 1;
+								cmd = cmd_start_sameline(cmd);
+								if (*cmd == '#') {
+									char *end;
+									tracks[track].file_offset = strtol(cmd + 1, &end, 10);
+									cmd = cmd_start_sameline(end);
+								}
+								if (!is_datafile) {
+									if (isdigit(*cmd)) {
+										uint32_t start = timecode_to_lba(cmd);
+										tracks[track].file_offset += start * tracks[track].sector_bytes;
+										cmd = next_blank(cmd);
+									}
+								}
+								if (isdigit(*cmd)) {
+									uint32_t length = timecode_to_lba(cmd);
+									tracks[track].end_lba += length;
+								} else {
+									long fsize = file_size(f);
+									tracks[track].end_lba += fsize - tracks[track].file_offset;
+								}
+							}
+						}
+					}
+				} else if (startswith(cmd, "SILENCE")) {
+					cmd = cmd_start_sameline(cmd + 7);
+					tracks[track].fake_pregap += timecode_to_lba(cmd);
+				} else if (startswith(cmd, "START")) {
+					cmd = cmd_start_sameline(cmd + 5);
+					tracks[track].start_lba = tracks[track].pregap_lba + timecode_to_lba(cmd);
+				}
+			}
+			if (cmd && *cmd) {
+				line = next_line(cmd);
+			} else {
+				line = NULL;
+			}
+		} else {
+			line = NULL;
+		}
+	} while (line);
+	if (media->num_tracks > 0 && media->tracks[0].f) {
+		//replace cue sheet with first sector
+		free(media->buffer);
+		media->buffer = calloc(2048, 1);
+		if (tracks[0].type == TRACK_DATA && tracks[0].sector_bytes == 2352) {
+			// if the first track is a data track, don't trust the TOC file and look at the MM:SS:FF from first sector
+			uint8_t msf[3];
+			fseek(tracks[0].f, 12, SEEK_SET);
+			if (sizeof(msf) == fread(msf, 1, sizeof(msf), tracks[0].f)) {
+				tracks[0].fake_pregap = msf[2] + (msf[0] * 60 + msf[1]) * 75;
+			}
+		} else if (!tracks[0].start_lba && !tracks[0].fake_pregap) {
+			tracks[0].fake_pregap = 2 * 75;
+		}
+
+		fseek(tracks[0].f, tracks[0].sector_bytes == 2352 ? 16 : 0, SEEK_SET);
+		media->size = fread(media->buffer, 1, 2048, tracks[0].f);
+		media->seek = bin_seek;
+		media->read = bin_read;
+	}
+	uint8_t valid = media->num_tracks > 0 && media->tracks[0].f != NULL;
+	media->type = valid ? MEDIA_CDROM : MEDIA_CART;
+	return valid;
+}
+
+uint32_t make_iso_media(system_media *media, const char *filename)
+{
+	FILE *f = fopen(filename, "rb");
+	if (!f) {
+		return 0;
+	}
+	media->buffer = calloc(2048, 1);
+	media->size = fread(media->buffer, 1, 2048, f);
+	media->num_tracks = 1;
+	media->tracks = calloc(sizeof(track_info), 1);
+	media->tracks[0] = (track_info){
+		.f = f,
+		.file_offset = 0,
+		.fake_pregap = 2 * 75,
+		.start_lba = 0,
+		.end_lba = file_size(f),
+		.sector_bytes = 2048,
+		.need_swap = 0,
+		.type = TRACK_DATA
+	};
+	media->type = MEDIA_CDROM;
+	media->seek = bin_seek;
+	media->read = bin_read;
+	return media->size;
+}