Mercurial > repos > blastem
comparison 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 |
comparison
equal
deleted
inserted
replaced
2113:0013362c320c | 2114:2449c88cea36 |
---|---|
1 #include <ctype.h> | |
2 #include <string.h> | |
3 | |
4 #include "system.h" | |
5 #include "util.h" | |
6 | |
7 static char* cmd_start(char *cur) | |
8 { | |
9 while (*cur && isblank(*cur)) | |
10 { | |
11 cur++; | |
12 } | |
13 return cur; | |
14 } | |
15 | |
16 static char* cmd_start_sameline(char *cur) | |
17 { | |
18 while (*cur && isblank(*cur) && *cur != '\n') | |
19 { | |
20 cur++; | |
21 } | |
22 return cur; | |
23 } | |
24 | |
25 static char* word_end(char *cur) | |
26 { | |
27 while (*cur && !isblank(*cur)) | |
28 { | |
29 cur++; | |
30 } | |
31 return cur; | |
32 } | |
33 | |
34 static char* next_line(char *cur) | |
35 { | |
36 while (*cur && *cur != '\n') | |
37 { | |
38 cur++; | |
39 } | |
40 if (*cur) { | |
41 return cur + 1; | |
42 } | |
43 return NULL; | |
44 } | |
45 | |
46 static char* next_blank(char *cur) | |
47 { | |
48 while (*cur && !isblank(*cur)) | |
49 { | |
50 cur++; | |
51 } | |
52 return cur; | |
53 } | |
54 | |
55 static uint32_t timecode_to_lba(char *timecode) | |
56 { | |
57 char *end; | |
58 int seconds = 0, frames = 0; | |
59 int minutes = strtol(timecode, &end, 10); | |
60 if (end) { | |
61 timecode = end + 1; | |
62 seconds = strtol(timecode, &end, 10); | |
63 if (end) { | |
64 timecode = end + 1; | |
65 frames = strtol(timecode, NULL, 10); | |
66 } | |
67 } | |
68 seconds += minutes * 60; | |
69 return seconds * 75 + frames; | |
70 | |
71 } | |
72 | |
73 enum { | |
74 FAKE_DATA = 1, | |
75 FAKE_AUDIO, | |
76 }; | |
77 | |
78 static uint8_t bin_seek(system_media *media, uint32_t sector) | |
79 { | |
80 media->cur_sector = sector; | |
81 uint32_t lba = sector; | |
82 uint32_t track; | |
83 for (track = 0; track < media->num_tracks; track++) | |
84 { | |
85 if (lba < media->tracks[track].fake_pregap) { | |
86 media->in_fake_pregap = media->tracks[track].type == TRACK_DATA ? FAKE_DATA : FAKE_AUDIO; | |
87 break; | |
88 } | |
89 lba -= media->tracks[track].fake_pregap; | |
90 if (lba < media->tracks[track].start_lba) { | |
91 if (media->tracks[track].fake_pregap) { | |
92 media->in_fake_pregap = media->tracks[track].type == TRACK_DATA ? FAKE_DATA : FAKE_AUDIO; | |
93 } else { | |
94 media->in_fake_pregap = 0; | |
95 } | |
96 break; | |
97 } | |
98 if (lba < media->tracks[track].end_lba) { | |
99 media->in_fake_pregap = 0; | |
100 break; | |
101 } | |
102 } | |
103 if (track < media->num_tracks) { | |
104 fprintf(stderr, "bin_seek to sector %u, adjusted lba %u, track %u\n", sector, lba, track); | |
105 media->cur_track = track; | |
106 if (!media->in_fake_pregap) { | |
107 if (track) { | |
108 lba -= media->tracks[track - 1].end_lba; | |
109 } | |
110 fseek(media->tracks[track].f, media->tracks[track].file_offset + lba * media->tracks[track].sector_bytes, SEEK_SET); | |
111 } | |
112 } | |
113 return track; | |
114 } | |
115 | |
116 static uint8_t fake_read(uint32_t sector, uint32_t offset) | |
117 { | |
118 if (!offset || (offset >= 16)) { | |
119 return 0; | |
120 //TODO: error detection and correction bytes | |
121 } else if (offset < 12) { | |
122 return 0xFF; | |
123 } else if (offset == 12) { | |
124 uint32_t minute = (sector / 75) / 60; | |
125 return (minute % 10) | ((minute / 10 ) << 4); | |
126 } else if (offset == 13) { | |
127 uint32_t seconds = (sector / 75) % 60; | |
128 return (seconds % 10) | ((seconds / 10 ) << 4); | |
129 } else if (offset == 14) { | |
130 uint32_t frames = sector % 75; | |
131 return (frames % 10) | ((frames / 10 ) << 4); | |
132 } else { | |
133 return 1; | |
134 } | |
135 } | |
136 | |
137 static uint8_t bin_read(system_media *media, uint32_t offset) | |
138 { | |
139 if (media->in_fake_pregap == FAKE_DATA) { | |
140 return fake_read(media->cur_sector, offset); | |
141 } else if (media->in_fake_pregap == FAKE_AUDIO) { | |
142 return 0; | |
143 } else if ((media->tracks[media->cur_track].sector_bytes < 2352 && offset < 16) || offset > (media->tracks[media->cur_track].sector_bytes + 16)) { | |
144 return fake_read(media->cur_sector, offset); | |
145 } else { | |
146 if (media->tracks[media->cur_track].need_swap) { | |
147 if (offset & 1) { | |
148 return media->byte_storage; | |
149 } | |
150 media->byte_storage = fgetc(media->tracks[media->cur_track].f); | |
151 } | |
152 return fgetc(media->tracks[media->cur_track].f); | |
153 } | |
154 } | |
155 | |
156 uint8_t parse_cue(system_media *media) | |
157 { | |
158 char *line = media->buffer; | |
159 media->num_tracks = 0; | |
160 do { | |
161 char *cmd = cmd_start(line); | |
162 if (cmd) { | |
163 if (startswith(cmd, "TRACK ")) { | |
164 media->num_tracks++; | |
165 } | |
166 line = next_line(cmd); | |
167 } else { | |
168 line = NULL; | |
169 } | |
170 } while (line); | |
171 track_info *tracks = calloc(sizeof(track_info), media->num_tracks); | |
172 media->tracks = tracks; | |
173 line = media->buffer; | |
174 int track = -1; | |
175 uint8_t audio_byte_swap = 0; | |
176 FILE *f = NULL; | |
177 int track_of_file = -1; | |
178 uint8_t has_index_0 = 0; | |
179 do { | |
180 char *cmd = cmd_start(line); | |
181 if (*cmd) { | |
182 if (startswith(cmd, "TRACK ")) { | |
183 track++; | |
184 track_of_file++; | |
185 has_index_0 = 0; | |
186 cmd += 6; | |
187 char *end; | |
188 int file_track = strtol(cmd, &end, 10); | |
189 if (file_track != (track + 1)) { | |
190 warning("Expected track %d, but found track %d in CUE sheet\n", track + 1, file_track); | |
191 } | |
192 tracks[track].f = f; | |
193 | |
194 | |
195 cmd = cmd_start(end); | |
196 if (*cmd) { | |
197 if (startswith(cmd, "AUDIO")) { | |
198 tracks[track].type = TRACK_AUDIO; | |
199 tracks[track].need_swap = audio_byte_swap; | |
200 tracks[track].sector_bytes = 2352; | |
201 } else { | |
202 tracks[track].type = TRACK_DATA; | |
203 tracks[track].need_swap = 0; | |
204 tracks[track].sector_bytes = 0; | |
205 char *slash = strchr(cmd, '/'); | |
206 if (slash) { | |
207 tracks[track].sector_bytes = atoi(slash+1); | |
208 } | |
209 if (!tracks[track].sector_bytes) { | |
210 warning("Missing sector size for data track %d in cue", track + 1); | |
211 tracks[track].sector_bytes = 2352; | |
212 } | |
213 } | |
214 | |
215 } | |
216 } else if (startswith(cmd, "FILE ")) { | |
217 cmd += 5; | |
218 cmd = strchr(cmd, '"'); | |
219 if (cmd) { | |
220 cmd++; | |
221 char *end = strchr(cmd, '"'); | |
222 if (end) { | |
223 char *fname; | |
224 //TODO: zipped BIN/CUE support | |
225 if (is_absolute_path(cmd)) { | |
226 fname = malloc(end-cmd + 1); | |
227 memcpy(fname, cmd, end-cmd); | |
228 fname[end-cmd] = 0; | |
229 } else { | |
230 size_t dirlen = strlen(media->dir); | |
231 fname = malloc(dirlen + 1 + (end-cmd) + 1); | |
232 memcpy(fname, media->dir, dirlen); | |
233 fname[dirlen] = PATH_SEP[0]; | |
234 memcpy(fname + dirlen + 1, cmd, end-cmd); | |
235 fname[dirlen + 1 + (end-cmd)] = 0; | |
236 } | |
237 f = fopen(fname, "rb"); | |
238 if (!f) { | |
239 fatal_error("Failed to open %s specified by FILE command in CUE sheet %s.%s\n", fname, media->name, media->extension); | |
240 } | |
241 free(fname); | |
242 track_of_file = -1; | |
243 for (end++; *end && *end != '\n' && *end != '\r'; end++) | |
244 { | |
245 if (!isspace(*end)) { | |
246 if (startswith(end, "BINARY")) { | |
247 audio_byte_swap = 0; | |
248 } else if (startswith(end, "MOTOROLA")) { | |
249 audio_byte_swap = 1; | |
250 } else { | |
251 warning("Unsupported FILE type in CUE sheet. Only BINARY and MOTOROLA are supported\n"); | |
252 } | |
253 break; | |
254 } | |
255 } | |
256 } | |
257 } | |
258 } else if (track >= 0) { | |
259 if (startswith(cmd, "PREGAP ")) { | |
260 tracks[track].fake_pregap = timecode_to_lba(cmd + 7); | |
261 } else if (startswith(cmd, "INDEX ")) { | |
262 char *after; | |
263 int index = strtol(cmd + 6, &after, 10); | |
264 uint8_t has_start_lba = 0; | |
265 uint32_t start_lba; | |
266 if (!index) { | |
267 tracks[track].pregap_lba = start_lba = timecode_to_lba(after); | |
268 has_index_0 = 1; | |
269 has_start_lba = 1; | |
270 } else if (index == 1) { | |
271 tracks[track].start_lba = timecode_to_lba(after); | |
272 if (!has_index_0) { | |
273 start_lba = tracks[track].start_lba; | |
274 has_start_lba = 1; | |
275 } | |
276 } | |
277 if (has_start_lba) { | |
278 if (track > 0) { | |
279 tracks[track-1].end_lba = start_lba; | |
280 } | |
281 if (track_of_file > 0) { | |
282 tracks[track].file_offset = tracks[track-1].file_offset + tracks[track-1].end_lba * tracks[track-1].sector_bytes; | |
283 if (track_of_file > 1) { | |
284 tracks[track].file_offset -= tracks[track-2].end_lba * tracks[track-1].sector_bytes; | |
285 } | |
286 } else { | |
287 tracks[track].file_offset = 0; | |
288 } | |
289 } | |
290 } | |
291 } | |
292 if (cmd && *cmd) { | |
293 line = next_line(cmd); | |
294 } else { | |
295 line = NULL; | |
296 } | |
297 } else { | |
298 line = NULL; | |
299 } | |
300 } while (line); | |
301 if (media->num_tracks > 0 && media->tracks[0].f) { | |
302 //end of last track is implicitly defined by file size | |
303 if (tracks[media->num_tracks-1].f) { | |
304 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; | |
305 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; | |
306 } | |
307 //replace cue sheet with first sector | |
308 free(media->buffer); | |
309 media->buffer = calloc(2048, 1); | |
310 if (tracks[0].type == TRACK_DATA && tracks[0].sector_bytes == 2352) { | |
311 // if the first track is a data track, don't trust the CUE sheet and look at the MM:SS:FF from first sector | |
312 uint8_t msf[3]; | |
313 fseek(tracks[0].f, 12, SEEK_SET); | |
314 if (sizeof(msf) == fread(msf, 1, sizeof(msf), tracks[0].f)) { | |
315 tracks[0].fake_pregap = msf[2] + (msf[0] * 60 + msf[1]) * 75; | |
316 } | |
317 } else if (!tracks[0].start_lba && !tracks[0].fake_pregap) { | |
318 tracks[0].fake_pregap = 2 * 75; | |
319 } | |
320 | |
321 fseek(tracks[0].f, tracks[0].sector_bytes == 2352 ? 16 : 0, SEEK_SET); | |
322 media->size = fread(media->buffer, 1, 2048, tracks[0].f); | |
323 media->seek = bin_seek; | |
324 media->read = bin_read; | |
325 } | |
326 uint8_t valid = media->num_tracks > 0 && media->tracks[0].f != NULL; | |
327 media->type = valid ? MEDIA_CDROM : MEDIA_CART; | |
328 return valid; | |
329 } | |
330 | |
331 uint8_t parse_toc(system_media *media) | |
332 { | |
333 char *line = media->buffer; | |
334 media->num_tracks = 0; | |
335 do { | |
336 char *cmd = cmd_start(line); | |
337 if (cmd) { | |
338 if (startswith(cmd, "TRACK ")) { | |
339 media->num_tracks++; | |
340 } | |
341 line = next_line(cmd); | |
342 } else { | |
343 line = NULL; | |
344 } | |
345 } while (line); | |
346 track_info *tracks = calloc(sizeof(track_info), media->num_tracks); | |
347 media->tracks = tracks; | |
348 line = media->buffer; | |
349 char *last_file_name = NULL; | |
350 FILE *f = NULL; | |
351 int track = -1; | |
352 do { | |
353 char *cmd = cmd_start(line); | |
354 if (*cmd) { | |
355 if (startswith(cmd, "TRACK ")) { | |
356 track++; | |
357 cmd = cmd_start(cmd + 6); | |
358 if (startswith(cmd, "AUDIO")) { | |
359 tracks[track].type = TRACK_AUDIO; | |
360 tracks[track].sector_bytes = 2352; | |
361 tracks[track].need_swap = 1; | |
362 } else { | |
363 tracks[track].type = TRACK_DATA; | |
364 tracks[track].need_swap = 0; | |
365 if (startswith(cmd, "MODE1_RAW") || startswith(cmd, "MODE2_RAW")) { | |
366 tracks[track].sector_bytes = 2352; | |
367 } else if (startswith(cmd, "MODE2_FORM2")) { | |
368 tracks[track].sector_bytes = 2324; | |
369 } else if (startswith(cmd, "MODE1") || startswith(cmd, "MODE2_FORM1")) { | |
370 tracks[track].sector_bytes = 2048; | |
371 } else if (startswith(cmd, "MODE2")) { | |
372 tracks[track].sector_bytes = 2336; | |
373 } | |
374 } | |
375 cmd = word_end(cmd); | |
376 if (*cmd && *cmd != '\n') { | |
377 cmd = cmd_start_sameline(cmd); | |
378 if (*cmd && *cmd != '\n') { | |
379 //TODO: record whether subcode is in raw format or not | |
380 if (startswith(cmd, "RW_RAW")) { | |
381 tracks[track].sector_bytes += 96; | |
382 } else if (startswith(cmd, "RW")) { | |
383 tracks[track].sector_bytes += 96; | |
384 } | |
385 } | |
386 } | |
387 if (track) { | |
388 tracks[track].start_lba = tracks[track].pregap_lba = tracks[track].end_lba = tracks[track-1].end_lba; | |
389 } | |
390 } else if (track >= 0) { | |
391 uint8_t is_datafile = startswith(cmd, "DATAFILE"); | |
392 if (is_datafile || startswith(cmd, "FILE")) { | |
393 | |
394 if (tracks[track].f) { | |
395 warning("TOC file has more than one file for track %d, only one is supported\n", track + 1); | |
396 } else { | |
397 cmd += 8; | |
398 char *fname_start = strchr(cmd, '"'); | |
399 if (fname_start) { | |
400 ++fname_start; | |
401 char *fname_end = strchr(fname_start, '"'); | |
402 if (fname_end) { | |
403 if (!last_file_name || strncmp(last_file_name, fname_start, fname_end-fname_start)) { | |
404 free(last_file_name); | |
405 last_file_name = calloc(1, 1 + fname_end-fname_start); | |
406 memcpy(last_file_name, fname_start, fname_end-fname_start); | |
407 char *fname; | |
408 //TODO: zipped BIN/TOC support | |
409 if (is_absolute_path(last_file_name)) { | |
410 fname = last_file_name; | |
411 } else { | |
412 size_t dirlen = strlen(media->dir); | |
413 fname = malloc(dirlen + 1 + (fname_end-fname_start) + 1); | |
414 memcpy(fname, media->dir, dirlen); | |
415 fname[dirlen] = PATH_SEP[0]; | |
416 memcpy(fname + dirlen + 1, fname_start, fname_end-fname_start); | |
417 fname[dirlen + 1 + (fname_end-fname_start)] = 0; | |
418 } | |
419 f = fopen(fname, "rb"); | |
420 if (!f) { | |
421 fatal_error("Failed to open %s specified by DATAFILE command in TOC file %s.%s\n", fname, media->name, media->extension); | |
422 } | |
423 if (fname != last_file_name) { | |
424 free(fname); | |
425 } | |
426 } | |
427 tracks[track].f = f; | |
428 cmd = fname_end + 1; | |
429 cmd = cmd_start_sameline(cmd); | |
430 if (*cmd == '#') { | |
431 char *end; | |
432 tracks[track].file_offset = strtol(cmd + 1, &end, 10); | |
433 cmd = cmd_start_sameline(end); | |
434 } | |
435 if (!is_datafile) { | |
436 if (isdigit(*cmd)) { | |
437 uint32_t start = timecode_to_lba(cmd); | |
438 tracks[track].file_offset += start * tracks[track].sector_bytes; | |
439 cmd = next_blank(cmd); | |
440 } | |
441 } | |
442 if (isdigit(*cmd)) { | |
443 uint32_t length = timecode_to_lba(cmd); | |
444 tracks[track].end_lba += length; | |
445 } else { | |
446 long fsize = file_size(f); | |
447 tracks[track].end_lba += fsize - tracks[track].file_offset; | |
448 } | |
449 } | |
450 } | |
451 } | |
452 } else if (startswith(cmd, "SILENCE")) { | |
453 cmd = cmd_start_sameline(cmd + 7); | |
454 tracks[track].fake_pregap += timecode_to_lba(cmd); | |
455 } else if (startswith(cmd, "START")) { | |
456 cmd = cmd_start_sameline(cmd + 5); | |
457 tracks[track].start_lba = tracks[track].pregap_lba + timecode_to_lba(cmd); | |
458 } | |
459 } | |
460 if (cmd && *cmd) { | |
461 line = next_line(cmd); | |
462 } else { | |
463 line = NULL; | |
464 } | |
465 } else { | |
466 line = NULL; | |
467 } | |
468 } while (line); | |
469 if (media->num_tracks > 0 && media->tracks[0].f) { | |
470 //replace cue sheet with first sector | |
471 free(media->buffer); | |
472 media->buffer = calloc(2048, 1); | |
473 if (tracks[0].type == TRACK_DATA && tracks[0].sector_bytes == 2352) { | |
474 // if the first track is a data track, don't trust the TOC file and look at the MM:SS:FF from first sector | |
475 uint8_t msf[3]; | |
476 fseek(tracks[0].f, 12, SEEK_SET); | |
477 if (sizeof(msf) == fread(msf, 1, sizeof(msf), tracks[0].f)) { | |
478 tracks[0].fake_pregap = msf[2] + (msf[0] * 60 + msf[1]) * 75; | |
479 } | |
480 } else if (!tracks[0].start_lba && !tracks[0].fake_pregap) { | |
481 tracks[0].fake_pregap = 2 * 75; | |
482 } | |
483 | |
484 fseek(tracks[0].f, tracks[0].sector_bytes == 2352 ? 16 : 0, SEEK_SET); | |
485 media->size = fread(media->buffer, 1, 2048, tracks[0].f); | |
486 media->seek = bin_seek; | |
487 media->read = bin_read; | |
488 } | |
489 uint8_t valid = media->num_tracks > 0 && media->tracks[0].f != NULL; | |
490 media->type = valid ? MEDIA_CDROM : MEDIA_CART; | |
491 return valid; | |
492 } | |
493 | |
494 uint32_t make_iso_media(system_media *media, const char *filename) | |
495 { | |
496 FILE *f = fopen(filename, "rb"); | |
497 if (!f) { | |
498 return 0; | |
499 } | |
500 media->buffer = calloc(2048, 1); | |
501 media->size = fread(media->buffer, 1, 2048, f); | |
502 media->num_tracks = 1; | |
503 media->tracks = calloc(sizeof(track_info), 1); | |
504 media->tracks[0] = (track_info){ | |
505 .f = f, | |
506 .file_offset = 0, | |
507 .fake_pregap = 2 * 75, | |
508 .start_lba = 0, | |
509 .end_lba = file_size(f), | |
510 .sector_bytes = 2048, | |
511 .need_swap = 0, | |
512 .type = TRACK_DATA | |
513 }; | |
514 media->type = MEDIA_CDROM; | |
515 media->seek = bin_seek; | |
516 media->read = bin_read; | |
517 return media->size; | |
518 } |