1 /* grabbag - Convenience lib for various routines common to several tools
2 * Copyright (C) 2002-2009 Josh Coalson
3 * Copyright (C) 2011-2016 Xiph.Org Foundation
4 *
5 * This library is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU Lesser General Public
7 * License as published by the Free Software Foundation; either
8 * version 2.1 of the License, or (at your option) any later version.
9 *
10 * This library is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * Lesser General Public License for more details.
14 *
15 * You should have received a copy of the GNU Lesser General Public
16 * License along with this library; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18 */
19
20 #ifdef HAVE_CONFIG_H
21 # include <config.h>
22 #endif
23
24 #include <stdio.h>
25 #include <stdlib.h>
26 #include <string.h>
27 #include "FLAC/assert.h"
28 #include "share/compat.h"
29 #include "share/grabbag.h"
30 #include "share/safe_str.h"
31
grabbag__cuesheet_msf_to_frame(uint32_t minutes,uint32_t seconds,uint32_t frames)32 uint32_t grabbag__cuesheet_msf_to_frame(uint32_t minutes, uint32_t seconds, uint32_t frames)
33 {
34 return ((minutes * 60) + seconds) * 75 + frames;
35 }
36
grabbag__cuesheet_frame_to_msf(uint32_t frame,uint32_t * minutes,uint32_t * seconds,uint32_t * frames)37 void grabbag__cuesheet_frame_to_msf(uint32_t frame, uint32_t *minutes, uint32_t *seconds, uint32_t *frames)
38 {
39 *frames = frame % 75;
40 frame /= 75;
41 *seconds = frame % 60;
42 frame /= 60;
43 *minutes = frame;
44 }
45
46 /* since we only care about values >= 0 or error, returns < 0 for any illegal string, else value */
local__parse_int_(const char * s)47 static int local__parse_int_(const char *s)
48 {
49 int ret = 0;
50 char c;
51
52 if(*s == '\0')
53 return -1;
54
55 while('\0' != (c = *s++))
56 if(c >= '0' && c <= '9')
57 ret = ret * 10 + (c - '0');
58 else
59 return -1;
60
61 return ret;
62 }
63
64 /* since we only care about values >= 0 or error, returns < 0 for any illegal string, else value */
local__parse_int64_(const char * s)65 static FLAC__int64 local__parse_int64_(const char *s)
66 {
67 FLAC__int64 ret = 0;
68 char c;
69
70 if(*s == '\0')
71 return -1;
72
73 while('\0' != (c = *s++))
74 if(c >= '0' && c <= '9')
75 ret = ret * 10 + (c - '0');
76 else
77 return -1;
78
79 return ret;
80 }
81
82 /* accept minute:second:frame syntax of '[0-9]+:[0-9][0-9]?:[0-9][0-9]?', but max second of 59 and max frame of 74, e.g. 0:0:0, 123:45:67
83 * return sample number or <0 for error
84 * WATCHOUT: if sample rate is not evenly divisible by 75, the resulting sample number will be approximate
85 */
local__parse_msf_(const char * s,uint32_t sample_rate)86 static FLAC__int64 local__parse_msf_(const char *s, uint32_t sample_rate)
87 {
88 FLAC__int64 ret, field;
89 char c;
90
91 c = *s++;
92 if(c >= '0' && c <= '9')
93 field = (c - '0');
94 else
95 return -1;
96 while(':' != (c = *s++)) {
97 if(c >= '0' && c <= '9')
98 field = field * 10 + (c - '0');
99 else
100 return -1;
101 }
102
103 ret = field * 60 * sample_rate;
104
105 c = *s++;
106 if(c >= '0' && c <= '9')
107 field = (c - '0');
108 else
109 return -1;
110 if(':' != (c = *s++)) {
111 if(c >= '0' && c <= '9') {
112 field = field * 10 + (c - '0');
113 c = *s++;
114 if(c != ':')
115 return -1;
116 }
117 else
118 return -1;
119 }
120
121 if(field >= 60)
122 return -1;
123
124 ret += field * sample_rate;
125
126 c = *s++;
127 if(c >= '0' && c <= '9')
128 field = (c - '0');
129 else
130 return -1;
131 if('\0' != (c = *s++)) {
132 if(c >= '0' && c <= '9') {
133 field = field * 10 + (c - '0');
134 c = *s++;
135 }
136 else
137 return -1;
138 }
139
140 if(c != '\0')
141 return -1;
142
143 if(field >= 75)
144 return -1;
145
146 ret += field * (sample_rate / 75);
147
148 return ret;
149 }
150
151 /* accept minute:second syntax of '[0-9]+:[0-9][0-9]?{,.[0-9]+}', but second < 60, e.g. 0:0.0, 3:5, 15:31.731
152 * return sample number or <0 for error
153 * WATCHOUT: depending on the sample rate, the resulting sample number may be approximate with fractional seconds
154 */
local__parse_ms_(const char * s,uint32_t sample_rate)155 static FLAC__int64 local__parse_ms_(const char *s, uint32_t sample_rate)
156 {
157 FLAC__int64 ret, field;
158 double x;
159 char c, *end;
160
161 c = *s++;
162 if(c >= '0' && c <= '9')
163 field = (c - '0');
164 else
165 return -1;
166 while(':' != (c = *s++)) {
167 if(c >= '0' && c <= '9')
168 field = field * 10 + (c - '0');
169 else
170 return -1;
171 }
172
173 ret = field * 60 * sample_rate;
174
175 s++; /* skip the ':' */
176 if(strspn(s, "0123456789.") != strlen(s))
177 return -1;
178 x = strtod(s, &end);
179 if(*end || end == s)
180 return -1;
181 if(x < 0.0 || x >= 60.0)
182 return -1;
183
184 ret += (FLAC__int64)(x * sample_rate);
185
186 return ret;
187 }
188
local__get_field_(char ** s,FLAC__bool allow_quotes)189 static char *local__get_field_(char **s, FLAC__bool allow_quotes)
190 {
191 FLAC__bool has_quote = false;
192 char *p;
193
194 FLAC__ASSERT(0 != s);
195
196 if(0 == *s)
197 return 0;
198
199 /* skip leading whitespace */
200 while(**s && 0 != strchr(" \t\r\n", **s))
201 (*s)++;
202
203 if(**s == 0) {
204 *s = 0;
205 return 0;
206 }
207
208 if(allow_quotes && (**s == '"')) {
209 has_quote = true;
210 (*s)++;
211 if(**s == 0) {
212 *s = 0;
213 return 0;
214 }
215 }
216
217 p = *s;
218
219 if(has_quote) {
220 *s = strchr(*s, '\"');
221 /* if there is no matching end quote, it's an error */
222 if(0 == *s)
223 p = *s = 0;
224 else {
225 **s = '\0';
226 (*s)++;
227 }
228 }
229 else {
230 while(**s && 0 == strchr(" \t\r\n", **s))
231 (*s)++;
232 if(**s) {
233 **s = '\0';
234 (*s)++;
235 }
236 else
237 *s = 0;
238 }
239
240 return p;
241 }
242
local__cuesheet_parse_(FILE * file,const char ** error_message,uint32_t * last_line_read,FLAC__StreamMetadata * cuesheet,uint32_t sample_rate,FLAC__bool is_cdda,FLAC__uint64 lead_out_offset)243 static FLAC__bool local__cuesheet_parse_(FILE *file, const char **error_message, uint32_t *last_line_read, FLAC__StreamMetadata *cuesheet, uint32_t sample_rate, FLAC__bool is_cdda, FLAC__uint64 lead_out_offset)
244 {
245 char buffer[4096], *line, *field;
246 uint32_t forced_leadout_track_num = 0;
247 FLAC__uint64 forced_leadout_track_offset = 0;
248 int in_track_num = -1, in_index_num = -1;
249 FLAC__bool disc_has_catalog = false, track_has_flags = false, track_has_isrc = false, has_forced_leadout = false;
250 FLAC__StreamMetadata_CueSheet *cs = &cuesheet->data.cue_sheet;
251
252 FLAC__ASSERT(!is_cdda || sample_rate == 44100);
253 /* double protection */
254 if(is_cdda && sample_rate != 44100) {
255 *error_message = "CD-DA cuesheet only allowed with 44.1kHz sample rate";
256 return false;
257 }
258
259 cs->lead_in = is_cdda? 2 * 44100 /* The default lead-in size for CD-DA */ : 0;
260 cs->is_cd = is_cdda;
261
262 while(0 != fgets(buffer, sizeof(buffer), file)) {
263 (*last_line_read)++;
264 line = buffer;
265
266 {
267 size_t linelen = strlen(line);
268 if((linelen == sizeof(buffer)-1) && line[linelen-1] != '\n') {
269 *error_message = "line too long";
270 return false;
271 }
272 }
273
274 if(0 != (field = local__get_field_(&line, /*allow_quotes=*/false))) {
275 if(0 == FLAC__STRCASECMP(field, "CATALOG")) {
276 if(disc_has_catalog) {
277 *error_message = "found multiple CATALOG commands";
278 return false;
279 }
280 if(0 == (field = local__get_field_(&line, /*allow_quotes=*/true))) {
281 *error_message = "CATALOG is missing catalog number";
282 return false;
283 }
284 if(strlen(field) >= sizeof(cs->media_catalog_number)) {
285 *error_message = "CATALOG number is too long";
286 return false;
287 }
288 if(is_cdda && (strlen(field) != 13 || strspn(field, "0123456789") != 13)) {
289 *error_message = "CD-DA CATALOG number must be 13 decimal digits";
290 return false;
291 }
292 safe_strncpy(cs->media_catalog_number, field, sizeof(cs->media_catalog_number));
293 disc_has_catalog = true;
294 }
295 else if(0 == FLAC__STRCASECMP(field, "FLAGS")) {
296 if(track_has_flags) {
297 *error_message = "found multiple FLAGS commands";
298 return false;
299 }
300 if(in_track_num < 0 || in_index_num >= 0) {
301 *error_message = "FLAGS command must come after TRACK but before INDEX";
302 return false;
303 }
304 while(0 != (field = local__get_field_(&line, /*allow_quotes=*/false))) {
305 if(0 == FLAC__STRCASECMP(field, "PRE"))
306 cs->tracks[cs->num_tracks-1].pre_emphasis = 1;
307 }
308 track_has_flags = true;
309 }
310 else if(0 == FLAC__STRCASECMP(field, "INDEX")) {
311 FLAC__int64 xx;
312 FLAC__StreamMetadata_CueSheet_Track *track = &cs->tracks[cs->num_tracks-1];
313 if(in_track_num < 0) {
314 *error_message = "found INDEX before any TRACK";
315 return false;
316 }
317 if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) {
318 *error_message = "INDEX is missing index number";
319 return false;
320 }
321 in_index_num = local__parse_int_(field);
322 if(in_index_num < 0) {
323 *error_message = "INDEX has invalid index number";
324 return false;
325 }
326 FLAC__ASSERT(cs->num_tracks > 0);
327 if(track->num_indices == 0) {
328 /* it's the first index point of the track */
329 if(in_index_num > 1) {
330 *error_message = "first INDEX number of a TRACK must be 0 or 1";
331 return false;
332 }
333 }
334 else {
335 if(in_index_num != track->indices[track->num_indices-1].number + 1) {
336 *error_message = "INDEX numbers must be sequential";
337 return false;
338 }
339 }
340 if(is_cdda && in_index_num > 99) {
341 *error_message = "CD-DA INDEX number must be between 0 and 99, inclusive";
342 return false;
343 }
344 /*@@@ search for duplicate track number? */
345 if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) {
346 *error_message = "INDEX is missing an offset after the index number";
347 return false;
348 }
349 /* first parse as minute:second:frame format */
350 xx = local__parse_msf_(field, sample_rate);
351 if(xx < 0) {
352 /* CD-DA must use only MM:SS:FF format */
353 if(is_cdda) {
354 *error_message = "illegal INDEX offset (not of the form MM:SS:FF)";
355 return false;
356 }
357 /* as an extension for non-CD-DA we allow MM:SS.SS or raw sample number */
358 xx = local__parse_ms_(field, sample_rate);
359 if(xx < 0) {
360 xx = local__parse_int64_(field);
361 if(xx < 0) {
362 *error_message = "illegal INDEX offset";
363 return false;
364 }
365 }
366 }
367 else if(sample_rate % 75 && xx) {
368 /* only sample zero is exact */
369 *error_message = "illegal INDEX offset (MM:SS:FF form not allowed if sample rate is not a multiple of 75)";
370 return false;
371 }
372 if(is_cdda && cs->num_tracks == 1 && cs->tracks[0].num_indices == 0 && xx != 0) {
373 *error_message = "first INDEX of first TRACK must have an offset of 00:00:00";
374 return false;
375 }
376 if(is_cdda && track->num_indices > 0 && (FLAC__uint64)xx <= track->indices[track->num_indices-1].offset) {
377 *error_message = "CD-DA INDEX offsets must increase in time";
378 return false;
379 }
380 /* fill in track offset if it's the first index of the track */
381 if(track->num_indices == 0)
382 track->offset = (FLAC__uint64)xx;
383 if(is_cdda && cs->num_tracks > 1) {
384 const FLAC__StreamMetadata_CueSheet_Track *prev = &cs->tracks[cs->num_tracks-2];
385 if((FLAC__uint64)xx <= prev->offset + prev->indices[prev->num_indices-1].offset) {
386 *error_message = "CD-DA INDEX offsets must increase in time";
387 return false;
388 }
389 }
390 if(!FLAC__metadata_object_cuesheet_track_insert_blank_index(cuesheet, cs->num_tracks-1, track->num_indices)) {
391 *error_message = "memory allocation error";
392 return false;
393 }
394 track->indices[track->num_indices-1].offset = (FLAC__uint64)xx - track->offset;
395 track->indices[track->num_indices-1].number = in_index_num;
396 }
397 else if(0 == FLAC__STRCASECMP(field, "ISRC")) {
398 char *l, *r;
399 if(track_has_isrc) {
400 *error_message = "found multiple ISRC commands";
401 return false;
402 }
403 if(in_track_num < 0 || in_index_num >= 0) {
404 *error_message = "ISRC command must come after TRACK but before INDEX";
405 return false;
406 }
407 if(0 == (field = local__get_field_(&line, /*allow_quotes=*/true))) {
408 *error_message = "ISRC is missing ISRC number";
409 return false;
410 }
411 /* strip out dashes */
412 for(l = r = field; *r; r++) {
413 if(*r != '-')
414 *l++ = *r;
415 }
416 *l = '\0';
417 if(strlen(field) != 12 || strspn(field, "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") < 5 || strspn(field+5, "1234567890") != 7) {
418 *error_message = "invalid ISRC number";
419 return false;
420 }
421 safe_strncpy(cs->tracks[cs->num_tracks-1].isrc, field, sizeof(cs->tracks[cs->num_tracks-1].isrc));
422 track_has_isrc = true;
423 }
424 else if(0 == FLAC__STRCASECMP(field, "TRACK")) {
425 if(cs->num_tracks > 0) {
426 const FLAC__StreamMetadata_CueSheet_Track *prev = &cs->tracks[cs->num_tracks-1];
427 if(
428 prev->num_indices == 0 ||
429 (
430 is_cdda &&
431 (
432 (prev->num_indices == 1 && prev->indices[0].number != 1) ||
433 (prev->num_indices == 2 && prev->indices[0].number != 1 && prev->indices[1].number != 1)
434 )
435 )
436 ) {
437 *error_message = is_cdda?
438 "previous TRACK must specify at least one INDEX 01" :
439 "previous TRACK must specify at least one INDEX";
440 return false;
441 }
442 }
443 if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) {
444 *error_message = "TRACK is missing track number";
445 return false;
446 }
447 in_track_num = local__parse_int_(field);
448 if(in_track_num < 0) {
449 *error_message = "TRACK has invalid track number";
450 return false;
451 }
452 if(in_track_num == 0) {
453 *error_message = "TRACK number must be greater than 0";
454 return false;
455 }
456 if(is_cdda) {
457 if(in_track_num > 99) {
458 *error_message = "CD-DA TRACK number must be between 1 and 99, inclusive";
459 return false;
460 }
461 }
462 else {
463 if(in_track_num == 255) {
464 *error_message = "TRACK number 255 is reserved for the lead-out";
465 return false;
466 }
467 else if(in_track_num > 255) {
468 *error_message = "TRACK number must be between 1 and 254, inclusive";
469 return false;
470 }
471 }
472 if(is_cdda && cs->num_tracks > 0 && in_track_num != cs->tracks[cs->num_tracks-1].number + 1) {
473 *error_message = "CD-DA TRACK numbers must be sequential";
474 return false;
475 }
476 /*@@@ search for duplicate track number? */
477 if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) {
478 *error_message = "TRACK is missing a track type after the track number";
479 return false;
480 }
481 if(!FLAC__metadata_object_cuesheet_insert_blank_track(cuesheet, cs->num_tracks)) {
482 *error_message = "memory allocation error";
483 return false;
484 }
485 cs->tracks[cs->num_tracks-1].number = in_track_num;
486 cs->tracks[cs->num_tracks-1].type = (0 == FLAC__STRCASECMP(field, "AUDIO"))? 0 : 1; /*@@@ should we be more strict with the value here? */
487 in_index_num = -1;
488 track_has_flags = false;
489 track_has_isrc = false;
490 }
491 else if(0 == FLAC__STRCASECMP(field, "REM")) {
492 if(0 != (field = local__get_field_(&line, /*allow_quotes=*/false))) {
493 if(0 == strcmp(field, "FLAC__lead-in")) {
494 FLAC__int64 xx;
495 if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) {
496 *error_message = "FLAC__lead-in is missing offset";
497 return false;
498 }
499 xx = local__parse_int64_(field);
500 if(xx < 0) {
501 *error_message = "illegal FLAC__lead-in offset";
502 return false;
503 }
504 if(is_cdda && xx % 588 != 0) {
505 *error_message = "illegal CD-DA FLAC__lead-in offset, must be even multiple of 588 samples";
506 return false;
507 }
508 cs->lead_in = (FLAC__uint64)xx;
509 }
510 else if(0 == strcmp(field, "FLAC__lead-out")) {
511 int track_num;
512 FLAC__int64 offset;
513 if(has_forced_leadout) {
514 *error_message = "multiple FLAC__lead-out commands";
515 return false;
516 }
517 if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) {
518 *error_message = "FLAC__lead-out is missing track number";
519 return false;
520 }
521 track_num = local__parse_int_(field);
522 if(track_num < 0) {
523 *error_message = "illegal FLAC__lead-out track number";
524 return false;
525 }
526 forced_leadout_track_num = (uint32_t)track_num;
527 /*@@@ search for duplicate track number? */
528 if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) {
529 *error_message = "FLAC__lead-out is missing offset";
530 return false;
531 }
532 offset = local__parse_int64_(field);
533 if(offset < 0) {
534 *error_message = "illegal FLAC__lead-out offset";
535 return false;
536 }
537 forced_leadout_track_offset = (FLAC__uint64)offset;
538 if(forced_leadout_track_offset != lead_out_offset) {
539 *error_message = "FLAC__lead-out offset does not match end-of-stream offset";
540 return false;
541 }
542 has_forced_leadout = true;
543 }
544 }
545 }
546 }
547 }
548
549 if(cs->num_tracks == 0) {
550 *error_message = "there must be at least one TRACK command";
551 return false;
552 }
553 else {
554 const FLAC__StreamMetadata_CueSheet_Track *prev = &cs->tracks[cs->num_tracks-1];
555 if(
556 prev->num_indices == 0 ||
557 (
558 is_cdda &&
559 (
560 (prev->num_indices == 1 && prev->indices[0].number != 1) ||
561 (prev->num_indices == 2 && prev->indices[0].number != 1 && prev->indices[1].number != 1)
562 )
563 )
564 ) {
565 *error_message = is_cdda?
566 "previous TRACK must specify at least one INDEX 01" :
567 "previous TRACK must specify at least one INDEX";
568 return false;
569 }
570 }
571
572 if(!has_forced_leadout) {
573 forced_leadout_track_num = is_cdda? 170 : 255;
574 forced_leadout_track_offset = lead_out_offset;
575 }
576 if(!FLAC__metadata_object_cuesheet_insert_blank_track(cuesheet, cs->num_tracks)) {
577 *error_message = "memory allocation error";
578 return false;
579 }
580 cs->tracks[cs->num_tracks-1].number = forced_leadout_track_num;
581 cs->tracks[cs->num_tracks-1].offset = forced_leadout_track_offset;
582
583 if(!feof(file)) {
584 *error_message = "read error";
585 return false;
586 }
587 return true;
588 }
589
grabbag__cuesheet_parse(FILE * file,const char ** error_message,uint32_t * last_line_read,uint32_t sample_rate,FLAC__bool is_cdda,FLAC__uint64 lead_out_offset)590 FLAC__StreamMetadata *grabbag__cuesheet_parse(FILE *file, const char **error_message, uint32_t *last_line_read, uint32_t sample_rate, FLAC__bool is_cdda, FLAC__uint64 lead_out_offset)
591 {
592 FLAC__StreamMetadata *cuesheet;
593
594 FLAC__ASSERT(0 != file);
595 FLAC__ASSERT(0 != error_message);
596 FLAC__ASSERT(0 != last_line_read);
597
598 *last_line_read = 0;
599 cuesheet = FLAC__metadata_object_new(FLAC__METADATA_TYPE_CUESHEET);
600
601 if(0 == cuesheet) {
602 *error_message = "memory allocation error";
603 return 0;
604 }
605
606 if(!local__cuesheet_parse_(file, error_message, last_line_read, cuesheet, sample_rate, is_cdda, lead_out_offset)) {
607 FLAC__metadata_object_delete(cuesheet);
608 return 0;
609 }
610
611 return cuesheet;
612 }
613
grabbag__cuesheet_emit(FILE * file,const FLAC__StreamMetadata * cuesheet,const char * file_reference)614 void grabbag__cuesheet_emit(FILE *file, const FLAC__StreamMetadata *cuesheet, const char *file_reference)
615 {
616 const FLAC__StreamMetadata_CueSheet *cs;
617 uint32_t track_num, index_num;
618
619 FLAC__ASSERT(0 != file);
620 FLAC__ASSERT(0 != cuesheet);
621 FLAC__ASSERT(cuesheet->type == FLAC__METADATA_TYPE_CUESHEET);
622
623 cs = &cuesheet->data.cue_sheet;
624
625 if(*(cs->media_catalog_number))
626 fprintf(file, "CATALOG %s\n", cs->media_catalog_number);
627 fprintf(file, "FILE %s\n", file_reference);
628
629 for(track_num = 0; track_num < cs->num_tracks-1; track_num++) {
630 const FLAC__StreamMetadata_CueSheet_Track *track = cs->tracks + track_num;
631
632 fprintf(file, " TRACK %02u %s\n", (uint32_t)track->number, track->type == 0? "AUDIO" : "DATA");
633
634 if(track->pre_emphasis)
635 fprintf(file, " FLAGS PRE\n");
636 if(*(track->isrc))
637 fprintf(file, " ISRC %s\n", track->isrc);
638
639 for(index_num = 0; index_num < track->num_indices; index_num++) {
640 const FLAC__StreamMetadata_CueSheet_Index *indx = track->indices + index_num;
641
642 fprintf(file, " INDEX %02u ", (uint32_t)indx->number);
643 if(cs->is_cd) {
644 const uint32_t logical_frame = (uint32_t)((track->offset + indx->offset) / (44100 / 75));
645 uint32_t m, s, f;
646 grabbag__cuesheet_frame_to_msf(logical_frame, &m, &s, &f);
647 fprintf(file, "%02u:%02u:%02u\n", m, s, f);
648 }
649 else
650 fprintf(file, "%" PRIu64 "\n", (track->offset + indx->offset));
651 }
652 }
653
654 fprintf(file, "REM FLAC__lead-in %" PRIu64 "\n", cs->lead_in);
655 fprintf(file, "REM FLAC__lead-out %u %" PRIu64 "\n", (uint32_t)cs->tracks[track_num].number, cs->tracks[track_num].offset);
656 }
657