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(unsigned minutes,unsigned seconds,unsigned frames)32 unsigned grabbag__cuesheet_msf_to_frame(unsigned minutes, unsigned seconds, unsigned frames)
33 {
34 	return ((minutes * 60) + seconds) * 75 + frames;
35 }
36 
grabbag__cuesheet_frame_to_msf(unsigned frame,unsigned * minutes,unsigned * seconds,unsigned * frames)37 void grabbag__cuesheet_frame_to_msf(unsigned frame, unsigned *minutes, unsigned *seconds, unsigned *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,unsigned sample_rate)86 static FLAC__int64 local__parse_msf_(const char *s, unsigned 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,unsigned sample_rate)155 static FLAC__int64 local__parse_ms_(const char *s, unsigned 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,unsigned * last_line_read,FLAC__StreamMetadata * cuesheet,unsigned sample_rate,FLAC__bool is_cdda,FLAC__uint64 lead_out_offset)243 static FLAC__bool local__cuesheet_parse_(FILE *file, const char **error_message, unsigned *last_line_read, FLAC__StreamMetadata *cuesheet, unsigned sample_rate, FLAC__bool is_cdda, FLAC__uint64 lead_out_offset)
244 {
245 	char buffer[4096], *line, *field;
246 	unsigned 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 = (unsigned)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,unsigned * last_line_read,unsigned sample_rate,FLAC__bool is_cdda,FLAC__uint64 lead_out_offset)590 FLAC__StreamMetadata *grabbag__cuesheet_parse(FILE *file, const char **error_message, unsigned *last_line_read, unsigned 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 	unsigned 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", (unsigned)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 ", (unsigned)indx->number);
643 			if(cs->is_cd) {
644 				const unsigned logical_frame = (unsigned)((track->offset + indx->offset) / (44100 / 75));
645 				unsigned 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", (unsigned)cs->tracks[track_num].number, cs->tracks[track_num].offset);
656 }
657