Mercurial > repos > blastem
view cdimage.c @ 2321:2eda5f81f91e
More fully baked ROM db support for SMS
author | Michael Pavone <pavone@retrodev.com> |
---|---|
date | Thu, 15 Jun 2023 09:36:11 -0700 |
parents | 9d68799f945b |
children | 9f0c67e5c50a |
line wrap: on
line source
#include <ctype.h> #include <string.h> #include <stdlib.h> #include "system.h" #include "util.h" #include "wave.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, minutes = 0; int frames = strtol(timecode, &end, 10); if (end && *end == ':') { timecode = end + 1; seconds = frames; frames = strtol(timecode, &end, 10); if (end && *end == ':') { minutes = seconds; seconds = frames; 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) { media->cur_track = track; if (!media->in_fake_pregap) { if (track) { lba -= media->tracks[track - 1].end_lba; } if (media->tracks[track].flac) { flac_seek(media->tracks[track].flac, (media->tracks[track].file_offset + lba * media->tracks[track].sector_bytes) / 4); } else { if (media->tracks[track].has_subcodes) { if (!media->tmp_buffer) { media->tmp_buffer = calloc(1, 96); } fseek(media->tracks[track].f, media->tracks[track].file_offset + (lba + 1) * media->tracks[track].sector_bytes - 96, SEEK_SET); int bytes = fread(media->tmp_buffer, 1, 96, media->tracks[track].f); if (bytes != 96) { fprintf(stderr, "Only read %d subcode bytes\n", bytes); } } 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 == 11 || (offset >= 16)) { return 0; //TODO: error detection and correction bytes } else if (offset < 11) { 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].flac) { if (offset & 3) { return media->byte_storage[(offset & 3) - 1]; } else { int16_t samples[2]; flac_get_sample(media->tracks[media->cur_track].flac, samples, 2); media->byte_storage[0] = samples[0] >> 8; media->byte_storage[1] = samples[1]; media->byte_storage[2] = samples[1] >> 8; return samples[0]; } } else { if (media->tracks[media->cur_track].need_swap) { if (offset & 1) { return media->byte_storage[0]; } media->byte_storage[0] = fgetc(media->tracks[media->cur_track].f); } return fgetc(media->tracks[media->cur_track].f); } } static uint8_t bin_subcode_read(system_media *media, uint32_t offset) { if (media->in_fake_pregap || !media->tracks[media->cur_track].has_subcodes) { //TODO: Fake PQ subcodes return 0; } //TODO: Translate "cooked" subcodes back to raw format return media->tmp_buffer[offset]; } 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; flac_file *flac = NULL; int track_of_file = -1; uint8_t has_index_0 = 0; uint32_t extra_offset = 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; tracks[track].flac = flac; 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; } flac = NULL; 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); } track_of_file = -1; for (end++; *end && *end != '\n' && *end != '\r'; end++) { if (!isspace(*end)) { extra_offset = 0; if (startswith(end, "BINARY")) { audio_byte_swap = 0; } else if (startswith(end, "MOTOROLA")) { audio_byte_swap = 1; } else if (startswith(end, "WAVE")) { audio_byte_swap = 0; wave_header wave; if (wave_read_header(f, &wave)) { if (wave.audio_format != 1 || wave.num_channels != 2 || wave.sample_rate != 44100 || wave.bits_per_sample != 16) { warning("BlastEm only suports WAVE tracks in 16-bit stereo PCM format at 44100 hz, file %s does not match\n", fname); } extra_offset = wave.format_header.size + sizeof(wave.data_header) + sizeof(wave.chunk); } else { fseek(f, 0, SEEK_SET); flac = flac_file_from_file(f); if (!flac) { fatal_error("WAVE file %s in cue sheet %s.%s is neither a valid WAVE nor a valid FLAC file\n", fname, media->name, media->extension); } if (flac->sample_rate != 44100 || flac->bits_per_sample != 16 || flac->channels != 2) { warning("FLAC files in a CUE sheet should match CD audio specs, %s does not\n", fname); } } } else { warning("Unsupported FILE type in CUE sheet. Only BINARY and MOTOROLA are supported\n"); } break; } } free(fname); } } } 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; if (!tracks[track].fake_pregap) { tracks[track].pregap_lba = 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 = extra_offset; } } } } 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 in a file is implictly based on the size f = tracks[0].f; uint32_t offset = 0; for (int track = 0; track < media->num_tracks; track++) { if (track == media->num_tracks - 1 && tracks[track].f) { uint32_t start_lba =tracks[track].fake_pregap ? tracks[track].start_lba : tracks[track].pregap_lba; uint32_t fsize; if (tracks[track].flac) { fsize = tracks[track].flac->total_samples * 4; } else { fsize = file_size(tracks[track].f); } tracks[track].end_lba = start_lba + (fsize - tracks[track].file_offset)/ tracks[track].sector_bytes; } else if (tracks[track].f != f) { uint32_t start_lba =tracks[track-1].fake_pregap ? tracks[track-1].start_lba : tracks[track-1].pregap_lba; uint32_t fsize; if (tracks[track-1].flac) { fsize = tracks[track-1].flac->total_samples * 4; } else { fsize = file_size(tracks[track-1].f); } tracks[track-1].end_lba = start_lba + (fsize - tracks[track-1].file_offset)/ tracks[track-1].sector_bytes; offset = tracks[track-1].end_lba; } if (!tracks[track].fake_pregap) { tracks[track].pregap_lba += offset; } tracks[track].start_lba += offset; tracks[track].end_lba += offset; } //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 && !tracks[0].flac) { // 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; media->read_subcodes = bin_subcode_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; tracks[track].has_subcodes = SUBCODES_RAW; } else if (startswith(cmd, "RW")) { tracks[track].sector_bytes += 96; tracks[track].has_subcodes = SUBCODES_COOKED; } } } 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 += is_datafile ? 8 : 4; 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 = cmd_start_sameline(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; media->read_subcodes = bin_subcode_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, .has_subcodes = SUBCODES_NONE, .need_swap = 0, .type = TRACK_DATA }; media->type = MEDIA_CDROM; media->seek = bin_seek; media->read = bin_read; media->read_subcodes = bin_subcode_read; return media->size; } void cdimage_serialize(system_media *media, serialize_buffer *buf) { if (media->type != MEDIA_CDROM) { return; } save_int32(buf, media->cur_track); save_int32(buf, media->cur_sector); if (media->cur_track < media->num_tracks && media->tracks[media->cur_track].f) { save_int32(buf, ftell(media->tracks[media->cur_track].f)); } else { save_int32(buf, 0); } save_int8(buf, media->in_fake_pregap); save_int8(buf, media->byte_storage[0]); if (media->tmp_buffer) { save_buffer8(buf, media->tmp_buffer, 96); } save_int8(buf, media->byte_storage[1]); save_int8(buf, media->byte_storage[2]); } void cdimage_deserialize(deserialize_buffer *buf, void *vmedia) { system_media *media = vmedia; if (media->type != MEDIA_CDROM) { return; } media->cur_track = load_int32(buf); media->cur_sector = load_int32(buf); uint32_t seekpos = load_int32(buf); if (media->cur_track < media->num_tracks && media->tracks[media->cur_track].f) { fseek(media->tracks[media->cur_track].f, seekpos, SEEK_SET); } media->in_fake_pregap = load_int8(buf); media->byte_storage[0] = load_int8(buf); if (media->tmp_buffer) { load_buffer8(buf, media->tmp_buffer, 96); } if (buf->size - buf->cur_pos >= 2) { media->byte_storage[1] = load_int8(buf); media->byte_storage[2] = load_int8(buf); } }