Cuesheet patch from Earl Chew.
[flac.git] / src / share / grabbag / cuesheet.c
1 /* grabbag - Convenience lib for various routines common to several tools
2  * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009  Josh Coalson
3  *
4  * This library is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Lesser General Public
6  * License as published by the Free Software Foundation; either
7  * version 2.1 of the License, or (at your option) any later version.
8  *
9  * This library is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * Lesser General Public License for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public
15  * License along with this library; if not, write to the Free Software
16  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
17  */
18
19 #if HAVE_CONFIG_H
20 #  include <config.h>
21 #endif
22
23 #include "share/grabbag.h"
24 #include "FLAC/assert.h"
25 #if HAVE_INTTYPES_H
26 #include <inttypes.h>
27 #endif
28 #include <stdio.h>
29 #include <stdlib.h>
30 #include <string.h>
31
32 unsigned grabbag__cuesheet_msf_to_frame(unsigned minutes, unsigned seconds, unsigned frames)
33 {
34         return ((minutes * 60) + seconds) * 75 + frames;
35 }
36
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 */
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 */
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  */
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  */
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
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
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 #if defined _MSC_VER || defined __MINGW32__ || defined __EMX__
246 #define FLAC__STRCASECMP stricmp
247 #else
248 #define FLAC__STRCASECMP strcasecmp
249 #endif
250         char buffer[4096], *line, *field;
251         unsigned forced_leadout_track_num = 0;
252         FLAC__uint64 forced_leadout_track_offset = 0;
253         int in_track_num = -1, in_index_num = -1;
254         FLAC__bool disc_has_catalog = false, track_has_flags = false, track_has_isrc = false, has_forced_leadout = false;
255         FLAC__StreamMetadata_CueSheet *cs = &cuesheet->data.cue_sheet;
256
257         FLAC__ASSERT(!is_cdda || sample_rate == 44100);
258         /* double protection */
259         if(is_cdda && sample_rate != 44100) {
260                 *error_message = "CD-DA cuesheet only allowed with 44.1kHz sample rate";
261                 return false;
262         }
263
264         cs->lead_in = is_cdda? 2 * 44100 /* The default lead-in size for CD-DA */ : 0;
265         cs->is_cd = is_cdda;
266
267         while(0 != fgets(buffer, sizeof(buffer), file)) {
268                 (*last_line_read)++;
269                 line = buffer;
270
271                 {
272                         size_t linelen = strlen(line);
273                         if((linelen == sizeof(buffer)-1) && line[linelen-1] != '\n') {
274                                 *error_message = "line too long";
275                                 return false;
276                         }
277                 }
278
279                 if(0 != (field = local__get_field_(&line, /*allow_quotes=*/false))) {
280                         if(0 == FLAC__STRCASECMP(field, "CATALOG")) {
281                                 if(disc_has_catalog) {
282                                         *error_message = "found multiple CATALOG commands";
283                                         return false;
284                                 }
285                                 if(0 == (field = local__get_field_(&line, /*allow_quotes=*/true))) {
286                                         *error_message = "CATALOG is missing catalog number";
287                                         return false;
288                                 }
289                                 if(strlen(field) >= sizeof(cs->media_catalog_number)) {
290                                         *error_message = "CATALOG number is too long";
291                                         return false;
292                                 }
293                                 if(is_cdda && (strlen(field) != 13 || strspn(field, "0123456789") != 13)) {
294                                         *error_message = "CD-DA CATALOG number must be 13 decimal digits";
295                                         return false;
296                                 }
297                                 strcpy(cs->media_catalog_number, field);
298                                 disc_has_catalog = true;
299                         }
300                         else if(0 == FLAC__STRCASECMP(field, "FLAGS")) {
301                                 if(track_has_flags) {
302                                         *error_message = "found multiple FLAGS commands";
303                                         return false;
304                                 }
305                                 if(in_track_num < 0 || in_index_num >= 0) {
306                                         *error_message = "FLAGS command must come after TRACK but before INDEX";
307                                         return false;
308                                 }
309                                 while(0 != (field = local__get_field_(&line, /*allow_quotes=*/false))) {
310                                         if(0 == FLAC__STRCASECMP(field, "PRE"))
311                                                 cs->tracks[cs->num_tracks-1].pre_emphasis = 1;
312                                 }
313                                 track_has_flags = true;
314                         }
315                         else if(0 == FLAC__STRCASECMP(field, "INDEX")) {
316                                 FLAC__int64 xx;
317                                 FLAC__StreamMetadata_CueSheet_Track *track = &cs->tracks[cs->num_tracks-1];
318                                 if(in_track_num < 0) {
319                                         *error_message = "found INDEX before any TRACK";
320                                         return false;
321                                 }
322                                 if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) {
323                                         *error_message = "INDEX is missing index number";
324                                         return false;
325                                 }
326                                 in_index_num = local__parse_int_(field);
327                                 if(in_index_num < 0) {
328                                         *error_message = "INDEX has invalid index number";
329                                         return false;
330                                 }
331                                 FLAC__ASSERT(cs->num_tracks > 0);
332                                 if(track->num_indices == 0) {
333                                         /* it's the first index point of the track */
334                                         if(in_index_num > 1) {
335                                                 *error_message = "first INDEX number of a TRACK must be 0 or 1";
336                                                 return false;
337                                         }
338                                 }
339                                 else {
340                                         if(in_index_num != track->indices[track->num_indices-1].number + 1) {
341                                                 *error_message = "INDEX numbers must be sequential";
342                                                 return false;
343                                         }
344                                 }
345                                 if(is_cdda && in_index_num > 99) {
346                                         *error_message = "CD-DA INDEX number must be between 0 and 99, inclusive";
347                                         return false;
348                                 }
349                                 /*@@@ search for duplicate track number? */
350                                 if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) {
351                                         *error_message = "INDEX is missing an offset after the index number";
352                                         return false;
353                                 }
354                                 /* first parse as minute:second:frame format */
355                                 xx = local__parse_msf_(field, sample_rate);
356                                 if(xx < 0) {
357                                         /* CD-DA must use only MM:SS:FF format */
358                                         if(is_cdda) {
359                                                 *error_message = "illegal INDEX offset (not of the form MM:SS:FF)";
360                                                 return false;
361                                         }
362                                         /* as an extension for non-CD-DA we allow MM:SS.SS or raw sample number */
363                                         xx = local__parse_ms_(field, sample_rate);
364                                         if(xx < 0) {
365                                                 xx = local__parse_int64_(field);
366                                                 if(xx < 0) {
367                                                         *error_message = "illegal INDEX offset";
368                                                         return false;
369                                                 }
370                                         }
371                                 }
372                                 else if(sample_rate % 75 && xx) {
373                                         /* only sample zero is exact */
374                                         *error_message = "illegal INDEX offset (MM:SS:FF form not allowed if sample rate is not a multiple of 75)";
375                                         return false;
376                                 }
377                                 if(is_cdda && cs->num_tracks == 1 && cs->tracks[0].num_indices == 0 && xx != 0) {
378                                         *error_message = "first INDEX of first TRACK must have an offset of 00:00:00";
379                                         return false;
380                                 }
381                                 if(is_cdda && track->num_indices > 0 && (FLAC__uint64)xx <= track->indices[track->num_indices-1].offset) {
382                                         *error_message = "CD-DA INDEX offsets must increase in time";
383                                         return false;
384                                 }
385                                 /* fill in track offset if it's the first index of the track */
386                                 if(track->num_indices == 0)
387                                         track->offset = (FLAC__uint64)xx;
388                                 if(is_cdda && cs->num_tracks > 1) {
389                                         const FLAC__StreamMetadata_CueSheet_Track *prev = &cs->tracks[cs->num_tracks-2];
390                                         if((FLAC__uint64)xx <= prev->offset + prev->indices[prev->num_indices-1].offset) {
391                                                 *error_message = "CD-DA INDEX offsets must increase in time";
392                                                 return false;
393                                         }
394                                 }
395                                 if(!FLAC__metadata_object_cuesheet_track_insert_blank_index(cuesheet, cs->num_tracks-1, track->num_indices)) {
396                                         *error_message = "memory allocation error";
397                                         return false;
398                                 }
399                                 track->indices[track->num_indices-1].offset = (FLAC__uint64)xx - track->offset;
400                                 track->indices[track->num_indices-1].number = in_index_num;
401                         }
402                         else if(0 == FLAC__STRCASECMP(field, "ISRC")) {
403                                 char *l, *r;
404                                 if(track_has_isrc) {
405                                         *error_message = "found multiple ISRC commands";
406                                         return false;
407                                 }
408                                 if(in_track_num < 0 || in_index_num >= 0) {
409                                         *error_message = "ISRC command must come after TRACK but before INDEX";
410                                         return false;
411                                 }
412                                 if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) {
413                                         *error_message = "ISRC is missing ISRC number";
414                                         return false;
415                                 }
416                                 /* strip out dashes */
417                                 for(l = r = field; *r; r++) {
418                                         if(*r != '-')
419                                                 *l++ = *r;
420                                 }
421                                 *l = '\0';
422                                 if(strlen(field) != 12 || strspn(field, "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") < 5 || strspn(field+5, "1234567890") != 7) {
423                                         *error_message = "invalid ISRC number";
424                                         return false;
425                                 }
426                                 strcpy(cs->tracks[cs->num_tracks-1].isrc, field);
427                                 track_has_isrc = true;
428                         }
429                         else if(0 == FLAC__STRCASECMP(field, "TRACK")) {
430                                 if(cs->num_tracks > 0) {
431                                         const FLAC__StreamMetadata_CueSheet_Track *prev = &cs->tracks[cs->num_tracks-1];
432                                         if(
433                                                 prev->num_indices == 0 ||
434                                                 (
435                                                         is_cdda &&
436                                                         (
437                                                                 (prev->num_indices == 1 && prev->indices[0].number != 1) ||
438                                                                 (prev->num_indices == 2 && prev->indices[0].number != 1 && prev->indices[1].number != 1)
439                                                         )
440                                                 )
441                                         ) {
442                                                 *error_message = is_cdda?
443                                                         "previous TRACK must specify at least one INDEX 01" :
444                                                         "previous TRACK must specify at least one INDEX";
445                                                 return false;
446                                         }
447                                 }
448                                 if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) {
449                                         *error_message = "TRACK is missing track number";
450                                         return false;
451                                 }
452                                 in_track_num = local__parse_int_(field);
453                                 if(in_track_num < 0) {
454                                         *error_message = "TRACK has invalid track number";
455                                         return false;
456                                 }
457                                 if(in_track_num == 0) {
458                                         *error_message = "TRACK number must be greater than 0";
459                                         return false;
460                                 }
461                                 if(is_cdda) {
462                                         if(in_track_num > 99) {
463                                                 *error_message = "CD-DA TRACK number must be between 1 and 99, inclusive";
464                                                 return false;
465                                         }
466                                 }
467                                 else {
468                                         if(in_track_num == 255) {
469                                                 *error_message = "TRACK number 255 is reserved for the lead-out";
470                                                 return false;
471                                         }
472                                         else if(in_track_num > 255) {
473                                                 *error_message = "TRACK number must be between 1 and 254, inclusive";
474                                                 return false;
475                                         }
476                                 }
477                                 if(is_cdda && cs->num_tracks > 0 && in_track_num != cs->tracks[cs->num_tracks-1].number + 1) {
478                                         *error_message = "CD-DA TRACK numbers must be sequential";
479                                         return false;
480                                 }
481                                 /*@@@ search for duplicate track number? */
482                                 if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) {
483                                         *error_message = "TRACK is missing a track type after the track number";
484                                         return false;
485                                 }
486                                 if(!FLAC__metadata_object_cuesheet_insert_blank_track(cuesheet, cs->num_tracks)) {
487                                         *error_message = "memory allocation error";
488                                         return false;
489                                 }
490                                 cs->tracks[cs->num_tracks-1].number = in_track_num;
491                                 cs->tracks[cs->num_tracks-1].type = (0 == FLAC__STRCASECMP(field, "AUDIO"))? 0 : 1; /*@@@ should we be more strict with the value here? */
492                                 in_index_num = -1;
493                                 track_has_flags = false;
494                                 track_has_isrc = false;
495                         }
496                         else if(0 == FLAC__STRCASECMP(field, "REM")) {
497                                 if(0 != (field = local__get_field_(&line, /*allow_quotes=*/false))) {
498                                         if(0 == strcmp(field, "FLAC__lead-in")) {
499                                                 FLAC__int64 xx;
500                                                 if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) {
501                                                         *error_message = "FLAC__lead-in is missing offset";
502                                                         return false;
503                                                 }
504                                                 xx = local__parse_int64_(field);
505                                                 if(xx < 0) {
506                                                         *error_message = "illegal FLAC__lead-in offset";
507                                                         return false;
508                                                 }
509                                                 if(is_cdda && xx % 588 != 0) {
510                                                         *error_message = "illegal CD-DA FLAC__lead-in offset, must be even multiple of 588 samples";
511                                                         return false;
512                                                 }
513                                                 cs->lead_in = (FLAC__uint64)xx;
514                                         }
515                                         else if(0 == strcmp(field, "FLAC__lead-out")) {
516                                                 int track_num;
517                                                 FLAC__int64 offset;
518                                                 if(has_forced_leadout) {
519                                                         *error_message = "multiple FLAC__lead-out commands";
520                                                         return false;
521                                                 }
522                                                 if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) {
523                                                         *error_message = "FLAC__lead-out is missing track number";
524                                                         return false;
525                                                 }
526                                                 track_num = local__parse_int_(field);
527                                                 if(track_num < 0) {
528                                                         *error_message = "illegal FLAC__lead-out track number";
529                                                         return false;
530                                                 }
531                                                 forced_leadout_track_num = (unsigned)track_num;
532                                                 /*@@@ search for duplicate track number? */
533                                                 if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) {
534                                                         *error_message = "FLAC__lead-out is missing offset";
535                                                         return false;
536                                                 }
537                                                 offset = local__parse_int64_(field);
538                                                 if(offset < 0) {
539                                                         *error_message = "illegal FLAC__lead-out offset";
540                                                         return false;
541                                                 }
542                                                 forced_leadout_track_offset = (FLAC__uint64)offset;
543                                                 if(forced_leadout_track_offset != lead_out_offset) {
544                                                         *error_message = "FLAC__lead-out offset does not match end-of-stream offset";
545                                                         return false;
546                                                 }
547                                                 has_forced_leadout = true;
548                                         }
549                                 }
550                         }
551                 }
552         }
553
554         if(cs->num_tracks == 0) {
555                 *error_message = "there must be at least one TRACK command";
556                 return false;
557         }
558         else {
559                 const FLAC__StreamMetadata_CueSheet_Track *prev = &cs->tracks[cs->num_tracks-1];
560                 if(
561                         prev->num_indices == 0 ||
562                         (
563                                 is_cdda &&
564                                 (
565                                         (prev->num_indices == 1 && prev->indices[0].number != 1) ||
566                                         (prev->num_indices == 2 && prev->indices[0].number != 1 && prev->indices[1].number != 1)
567                                 )
568                         )
569                 ) {
570                         *error_message = is_cdda?
571                                 "previous TRACK must specify at least one INDEX 01" :
572                                 "previous TRACK must specify at least one INDEX";
573                         return false;
574                 }
575         }
576
577         if(!has_forced_leadout) {
578                 forced_leadout_track_num = is_cdda? 170 : 255;
579                 forced_leadout_track_offset = lead_out_offset;
580         }
581         if(!FLAC__metadata_object_cuesheet_insert_blank_track(cuesheet, cs->num_tracks)) {
582                 *error_message = "memory allocation error";
583                 return false;
584         }
585         cs->tracks[cs->num_tracks-1].number = forced_leadout_track_num;
586         cs->tracks[cs->num_tracks-1].offset = forced_leadout_track_offset;
587
588         if(!feof(file)) {
589                 *error_message = "read error";
590                 return false;
591         }
592         return true;
593 #undef FLAC__STRCASECMP
594 }
595
596 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)
597 {
598         FLAC__StreamMetadata *cuesheet;
599
600         FLAC__ASSERT(0 != file);
601         FLAC__ASSERT(0 != error_message);
602         FLAC__ASSERT(0 != last_line_read);
603
604         *last_line_read = 0;
605         cuesheet = FLAC__metadata_object_new(FLAC__METADATA_TYPE_CUESHEET);
606
607         if(0 == cuesheet) {
608                 *error_message = "memory allocation error";
609                 return 0;
610         }
611
612         if(!local__cuesheet_parse_(file, error_message, last_line_read, cuesheet, sample_rate, is_cdda, lead_out_offset)) {
613                 FLAC__metadata_object_delete(cuesheet);
614                 return 0;
615         }
616
617         return cuesheet;
618 }
619
620 void grabbag__cuesheet_emit(FILE *file, const FLAC__StreamMetadata *cuesheet, const char *file_reference)
621 {
622         const FLAC__StreamMetadata_CueSheet *cs;
623         unsigned track_num, index_num;
624
625         FLAC__ASSERT(0 != file);
626         FLAC__ASSERT(0 != cuesheet);
627         FLAC__ASSERT(cuesheet->type == FLAC__METADATA_TYPE_CUESHEET);
628
629         cs = &cuesheet->data.cue_sheet;
630
631         if(*(cs->media_catalog_number))
632                 fprintf(file, "CATALOG %s\n", cs->media_catalog_number);
633         fprintf(file, "FILE %s\n", file_reference);
634
635         for(track_num = 0; track_num < cs->num_tracks-1; track_num++) {
636                 const FLAC__StreamMetadata_CueSheet_Track *track = cs->tracks + track_num;
637
638                 fprintf(file, "  TRACK %02u %s\n", (unsigned)track->number, track->type == 0? "AUDIO" : "DATA");
639
640                 if(track->pre_emphasis)
641                         fprintf(file, "    FLAGS PRE\n");
642                 if(*(track->isrc))
643                         fprintf(file, "    ISRC %s\n", track->isrc);
644
645                 for(index_num = 0; index_num < track->num_indices; index_num++) {
646                         const FLAC__StreamMetadata_CueSheet_Index *index = track->indices + index_num;
647
648                         fprintf(file, "    INDEX %02u ", (unsigned)index->number);
649                         if(cs->is_cd) {
650                                 const unsigned logical_frame = (unsigned)((track->offset + index->offset) / (44100 / 75));
651                                 unsigned m, s, f;
652                                 grabbag__cuesheet_frame_to_msf(logical_frame, &m, &s, &f);
653                                 fprintf(file, "%02u:%02u:%02u\n", m, s, f);
654                         }
655                         else
656                                 fprintf(file, "%" PRIu64 "\n", (track->offset + index->offset));
657                 }
658         }
659
660         fprintf(file, "REM FLAC__lead-in %" PRIu64 "\n", cs->lead_in);
661         fprintf(file, "REM FLAC__lead-out %u %" PRIu64 "\n", (unsigned)cs->tracks[track_num].number, cs->tracks[track_num].offset);
662 }