/* cue2toc.c - conversion routines * Copyright (C) 2004 Matthias Czapla * * This file is part of cue2toc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include #include #include #include #include #include #include #include #include "cue2toc.h" #include "timecode.h" #define TCBUFLEN 9 /* Buffer length for timecode strings (HH:MM:SS) */ #define MAXCMDLEN 10 /* Longest command (currently SONGWRITER) */ extern const char *progname; /* Set to argv[0] by main */ extern int verbose; /* Set by main */ /* * Input is divied into tokens that are separated by whitespace, horizantal * tabulator, line feed and carriage return. Tokens can be either commands * from a fixed set or strings. If a string is to contain any of the token * delimiting characters it must be enclosed in double quotes. */ static const char token_delimiter[] = { ' ', '\t', '\n', '\r' }; /* Return true if c is one of token_delimiter */ static int isdelim(int c) { int i; int n = sizeof(token_delimiter); for (i = 0; i < n; i++) if (c == token_delimiter[i]) return 1; return 0; } /* Used as return type for get_command and index into cmds */ enum command { REM, CATALOG, CDTEXTFILE, FILECMD, PERFORMER, SONGWRITER, TITLE, TRACK, FLAGS, DCP, FOURCH, PRE, SCMS, ISRC, PREGAP, INDEX, POSTGAP, BINARY, MOTOROLA, AIFF, WAVE, MP3, UNKNOWN, END }; /* Except the last two these are the valid CUE commands */ char cmds[][MAXCMDLEN + 1] = { "REM", "CATALOG", "CDTEXTFILE", "FILE", "PERFORMER", "SONGWRITER", "TITLE", "TRACK", "FLAGS", "DCP", "4CH", "PRE", "SCMS", "ISRC", "PREGAP", "INDEX", "POSTGAP", "BINARY", "MOTOROLA", "AIFF", "WAVE", "MP3", "UNKNOWN", "END" }; /* These are for error messages */ static const char *fname = "stdin"; static long line; /* current line number */ static long tokenstart; /* line where last token started */ /* To generate meaningful error messages in check_once */ enum scope { CUESHEET, GLOBAL, ONETRACK }; /* Fatal error while processing input file */ static void err_fail(const char *s) { fprintf(stderr, "%s:%s:%ld: %s\n", progname, fname, tokenstart, s); exit(EXIT_FAILURE); } /* Fatal error */ static void err_fail2(const char *s) { fprintf(stderr, "%s: %s\n", progname, s); exit(EXIT_FAILURE); } /* EOF while expecting more */ static void err_earlyend() { fprintf(stderr, "%s:%s:%ld: Premature end of file\n", progname, fname, line); exit(EXIT_FAILURE); } /* Warning. Keep on going. */ static void err_warn(const char *s) { if (!verbose) return; fprintf(stderr, "%s:%s:%ld: Warning, %s\n", progname, fname, tokenstart, s); } /* Get next command from file */ static enum command get_command(FILE *f) { int c; char buf[MAXCMDLEN + 1]; int i = 0; /* eat whitespace */ do { c = getc(f); if (c == '\n') line++; } while (isdelim(c)); if (c == EOF) return END; tokenstart = line; /* get command, transform to upper case */ do { buf[i++] = toupper(c); c = getc(f); } while (!isdelim(c) && c!= EOF && i < MAXCMDLEN); if (!isdelim(c)) return UNKNOWN; /* command longer than MAXCMDLEN */ if (c == EOF) return END; if (c == '\n') line++; buf[i] = '\0'; if (strcmp(buf, cmds[REM]) == 0) return REM; else if (strcmp(buf, cmds[CATALOG]) == 0) return CATALOG; else if (strcmp(buf, cmds[CDTEXTFILE]) == 0) return CDTEXTFILE; else if (strcmp(buf, cmds[FILECMD]) == 0) return FILECMD; else if (strcmp(buf, cmds[PERFORMER]) == 0) return PERFORMER; else if (strcmp(buf, cmds[SONGWRITER]) == 0) return SONGWRITER; else if (strcmp(buf, cmds[TITLE]) == 0) return TITLE; else if (strcmp(buf, cmds[TRACK]) == 0) return TRACK; else if (strcmp(buf, cmds[FLAGS]) == 0) return FLAGS; else if (strcmp(buf, cmds[DCP]) == 0) return DCP; else if (strcmp(buf, cmds[FOURCH]) == 0) return FOURCH; else if (strcmp(buf, cmds[PRE]) == 0) return PRE; else if (strcmp(buf, cmds[SCMS]) == 0) return SCMS; else if (strcmp(buf, cmds[ISRC]) == 0) return ISRC; else if (strcmp(buf, cmds[PREGAP]) == 0) return PREGAP; else if (strcmp(buf, cmds[INDEX]) == 0) return INDEX; else if (strcmp(buf, cmds[POSTGAP]) == 0) return POSTGAP; else if (strcmp(buf, cmds[BINARY]) == 0) return BINARY; else if (strcmp(buf, cmds[MOTOROLA]) == 0) return MOTOROLA; else if (strcmp(buf, cmds[AIFF]) == 0) return AIFF; else if (strcmp(buf, cmds[WAVE]) == 0) return WAVE; else if (strcmp(buf, cmds[MP3]) == 0) return MP3; else return UNKNOWN; } /* Skip leading token delimiters then read at most n chars from f into s. * Put terminating Null at the end of s. This implies that s must be * really n + 1. Return number of characters written to s. The only case to * return zero is on EOF before any character was read. * Exit the program indicating failure if string is longer than n. */ static size_t get_string(FILE *f, char *s, size_t n) { int c; size_t i = 0; /* eat whitespace */ do { c = getc(f); if (c == '\n') line++; } while (isdelim(c)); if (c == EOF) return 0; tokenstart = line; if (c == '\"') { c = getc(f); if (c == '\n') line++; while (c != '\"' && c != EOF && i < n) { s[i++] = c; c = getc(f); if (c == '\n') line++; } if (i == n && c != '\"' && c != EOF) err_fail("String too long"); } else { while (!isdelim(c) && c != EOF && i < n) { s[i++] = c; c = getc(f); } if (i == n && !isdelim(c) && c != EOF) err_fail("String too long"); } if (i == 0) err_fail("Empty string"); if (c == '\n') line++; s[i] = '\0'; return i; } /* Return track mode */ static enum track_mode get_track_mode(FILE *f) { char buf[] = "MODE1/2048"; char *pbuf = buf; if (get_string(f, buf, sizeof(buf) - 1) < 1) err_fail("Illegal track mode"); /* transform to upper case */ while (*pbuf) { *pbuf = toupper(*pbuf); pbuf++; } if (strcmp(buf, "AUDIO") == 0) return AUDIO; else if (strcmp(buf, "MODE1/2048") == 0) return MODE1; else if (strcmp(buf, "MODE1/2352") == 0) return MODE1_RAW; else if (strcmp(buf, "MODE2/2336") == 0) return MODE2; else if (strcmp(buf, "MODE2/2352") == 0) return MODE2_RAW; else err_fail("Unsupported track mode"); return AUDIO; } static void check_once(enum command cmd, char *s, enum scope sc); /* Read at most CDTEXTLEN chars into s */ static void get_cdtext(FILE *f, enum command cmd, char *s, enum scope sc) { check_once(cmd, s, sc); if (get_string(f, s, CDTEXTLEN) < 1) err_earlyend(); } /* All strings have their first character initialized to '\0' so if s[0] is not Null the cmd has already been seen in input. In this case print a message end exit program indicating failure. The only purpose of the arguments cmd and sc is to print meaningful error messages. */ static void check_once(enum command cmd, char *s, enum scope sc) { if (s[0] == '\0') return; fprintf(stderr, "%s:%s:%ld: %s allowed only once", progname, fname, line, cmds[cmd]); switch (sc) { case CUESHEET: fprintf(stderr, "\n"); break; case GLOBAL: fprintf(stderr, " in global section\n"); break; case ONETRACK: fprintf(stderr, " per track\n"); break; } exit(EXIT_FAILURE); } /* If this is a data track and does not start at position zero exit the program. The TOC format has no way to specify a data track using only a portion past the first byte of a binary file. */ static void check_cutting_binary(struct trackspec *tr) { if (tr->mode == AUDIO) return; if (tr->pregap_data_from_file) { if (tr->pregap < tr->start) err_fail("TOC format does not allow cutting binary " "files. Try burning CUE file directly.\n"); } else if (tr->start > 0) err_fail("TOC format does not allow cutting binary " "files. Try burning CUE file directly.\n"); } /* Allocate, initialize and return new track */ static struct trackspec* new_track(void) { struct trackspec *track; int i; if ((track = (struct trackspec*) malloc(sizeof(struct trackspec))) == NULL) err_fail("Memory allocation error in new_track()"); track->copy = track->pre_emphasis = track->four_channel_audio = track->pregap_data_from_file = 0; track->isrc[0] = track->title[0] = track->performer[0] = track->songwriter[0] = track->filename[0] = '\0'; track->pregap = track->start = track->postgap = -1; for (i = 0; i < NUM_OF_INDEXES; i++) track->indexes[i] = -1; track->next = NULL; return track; } /* Read the cuefile and return a pointer to the cuesheet */ struct cuesheet* read_cue(const char *cuefile, const char *wavefile) { FILE *f; enum command cmd; struct cuesheet *cs = NULL; struct trackspec *track = NULL; size_t n; int c; char file[FILENAMELEN + 1]; enum command filetype = UNKNOWN; char timecode_buffer[TCBUFLEN]; char devnull[FILENAMELEN + 1]; /* just for eating CDTEXTFILE arg */ if (NULL == cuefile) { f = stdin; } else if (NULL == (f = fopen(cuefile, "r"))) { fprintf(stderr, "%s: Could not open file \"%s\" for " "reading: %s\n", progname, cuefile, strerror(errno)); exit(EXIT_FAILURE); } if (cuefile) fname = cuefile; if ((cs = (struct cuesheet*) malloc(sizeof(struct cuesheet))) == NULL) err_fail("Memory allocation error in read_cue()"); cs->catalog[0] = '\0'; cs->type = 0; cs->title[0] = '\0'; cs->performer[0] = '\0'; cs->songwriter[0] = '\0'; cs->tracklist = NULL; file[0] = '\0'; line = 1; /* global section */ while ((cmd = get_command(f)) != TRACK) { switch (cmd) { case UNKNOWN: err_fail("Unknown command"); case END: err_earlyend(); case REM: c = getc(f); while (c != '\n' && c != EOF) c = getc(f); break; case CDTEXTFILE: err_warn("ignoring CDTEXTFILE..."); if (get_string(f, devnull, FILENAMELEN) == 0) err_warn("Syntactically incorrect " "CDTEXTFILE command. But who " "cares..."); break; case CATALOG: check_once(CATALOG, cs->catalog, CUESHEET); n = get_string(f, cs->catalog, 13); if (n != 13) err_fail("Catalog number must be 13 " "characters long"); break; case TITLE: get_cdtext(f, TITLE, cs->title, GLOBAL); break; case PERFORMER: get_cdtext(f, PERFORMER, cs->performer, GLOBAL); break; case SONGWRITER: get_cdtext(f, SONGWRITER, cs->songwriter, GLOBAL); break; case FILECMD: check_once(FILECMD, file, GLOBAL); if (get_string(f, file, FILENAMELEN) < 1) err_earlyend(); switch (cmd = get_command(f)) { case MOTOROLA: err_warn("big endian binary file"); case BINARY: filetype = BINARY; break; case AIFF: case MP3: err_warn("AIFF and MP3 not supported by " "cdrdao"); case WAVE: if (wavefile) { strncpy(file, wavefile, FILENAMELEN); file[FILENAMELEN] = '\0'; } filetype = WAVE; break; default: err_fail("Unsupported file type"); } break; default: err_fail("Command not allowed in global section"); break; } } /* leaving global section, entering track specifications */ if (file[0] == '\0') err_fail("TRACK without previous FILE"); while (cmd != END) { switch(cmd) { case UNKNOWN: err_fail("Unknown command"); case REM: c = getc(f); while (c != '\n' && c != EOF) c = getc(f); break; case TRACK: if (track == NULL) /* first track */ cs->tracklist = track = new_track(); else { check_cutting_binary(track); track = track->next = new_track(); } /* the CUE format is "TRACK nn MODE" but we are not interested in the track number */ while (isdelim(c = getc(f))) if (c == '\n') line++; while (!isdelim(c = getc(f))) ; if (c == '\n') line++; track->mode = get_track_mode(f); /* audio tracks with binary files seem quite common */ /* if (track->mode == AUDIO && filetype == BINARY || track->mode != AUDIO && filetype == WAVE) err_fail("File and track type mismatch"); */ strcpy(track->filename, file); break; case TITLE: get_cdtext(f, TITLE, track->title, ONETRACK); break; case PERFORMER: get_cdtext(f, PERFORMER, track->performer, ONETRACK); break; case SONGWRITER: get_cdtext(f, SONGWRITER, track->songwriter, ONETRACK); break; case ISRC: check_once(ISRC, track->isrc, ONETRACK); if (get_string(f, track->isrc, 12) != 12) err_fail("ISRC must be 12 characters long"); break; case FLAGS: if (track->copy || track->pre_emphasis || track->four_channel_audio) err_fail("FLAGS allowed only once per track"); /* get the flags */ cmd = get_command(f); while (cmd == DCP || cmd == FOURCH || cmd == PRE || cmd == SCMS) { switch (cmd) { case DCP: track->copy = 1; break; case FOURCH: track->four_channel_audio = 1; break; case PRE: track->pre_emphasis = 1; break; case SCMS: err_warn("serial copy management " "system flag not supported " "by cdrdao"); break; default: err_fail("Should not get here"); } cmd = get_command(f); } /* current non-FLAG command is already in cmd, so avoid get_command() call below */ continue; break; case PREGAP: if (track->pregap != -1) err_fail("PREGAP allowed only once per track"); if (get_string(f, timecode_buffer, TCBUFLEN - 1) < 1) err_earlyend(); track->pregap = tc2fr(timecode_buffer); if (track->pregap == -1) err_fail("Timecode out of range"); track->pregap_data_from_file = 0; break; case POSTGAP: if (track->postgap != -1) err_fail("POSTGAP allowed only once per track"); if (get_string(f, timecode_buffer, TCBUFLEN - 1) < 1) err_earlyend(); track->postgap = tc2fr(timecode_buffer); if (track->postgap == -1) err_fail("Timecode out of range"); break; case INDEX: if (get_string(f, timecode_buffer, 2) < 1) err_earlyend(); n = atoi(timecode_buffer); if (n < 0 || n > 99) err_fail("Index out of range"); /* Index 0 is track pregap and Index 1 is start of track. Index 2 to 99 are the true subindexes and only allowed if the preceding one was there before */ switch (n) { case 0: if (track->start != -1) err_fail("Indexes must be sequential"); if (track->pregap != -1) err_fail("PREGAP allowed only once " "per track"); if (get_string(f, timecode_buffer, TCBUFLEN - 1) < 1) err_earlyend(); /* This is only a temporary value until index 01 is read */ track->pregap = tc2fr(timecode_buffer); if (track->pregap == -1) err_fail("Timecode out of range"); track->pregap_data_from_file = 1; break; case 1: if (track->start != -1) err_fail("Each index allowed only " "once per track"); if (get_string(f, timecode_buffer, TCBUFLEN - 1) < 1) err_fail("Missing timecode"); track->start = tc2fr(timecode_buffer); if (track->start == -1) err_fail("Timecode out of range"); /* Fix the pregap value */ if (track->pregap_data_from_file) track->pregap = track->start - track->pregap; break; case 2: if (track->start == -1) err_fail("Indexes must be sequential"); if (track->indexes[n - 2] != -1) err_fail("Each index allowed only " "once per track"); if (get_string(f, timecode_buffer, TCBUFLEN - 1) < 1) err_fail("Missing timecode"); track->indexes[n - 2] = tc2fr(timecode_buffer); if (track->indexes[n - 2] == -1) err_fail("Timecode out of range"); break; default: /* the other 97 indexes */ /* check if previous index is there */ if (track->indexes[n - 3] == -1) err_fail("Indexes must be sequential"); if (track->indexes[n - 2] != -1) err_fail("Each index allowed only " "once per track"); if (get_string(f, timecode_buffer, TCBUFLEN - 1) < 1) err_fail("Missing timecode"); track->indexes[n - 2] = tc2fr(timecode_buffer); if (track->indexes[n - 2] == -1) err_fail("Timecode out of range"); break; } break; case FILECMD: if (get_string(f, file, FILENAMELEN) < 1) err_earlyend(); switch (cmd = get_command(f)) { case MOTOROLA: err_warn("big endian binary file"); case BINARY: filetype = BINARY; break; case AIFF: case MP3: err_warn("AIFF and MP3 not supported by " "cdrdao"); case WAVE: if (wavefile) { strncpy(file, wavefile, FILENAMELEN); file[FILENAMELEN] = '\0'; } filetype = WAVE; break; default: err_fail("Unsupported file type"); } break; default: err_fail("Command not allowed in track spec"); break; } cmd = get_command(f); } check_cutting_binary(track); return cs; } /* Deduce the disc session type from the track modes */ static enum session_type determine_session_type(struct trackspec *list) { struct trackspec *track = list; /* set to true if track of corresponding type is found */ int audio = 0; int mode1 = 0; int mode2 = 0; while (track != NULL) { switch (track->mode) { case AUDIO: audio = 1; break; case MODE1: case MODE1_RAW: mode1 = 1; break; case MODE2: case MODE2_RAW: mode2 = 1; break; default: /* should never get here */ err_fail2("Dont know how this could happen, but here " "is a track with an unknown mode :|"); } track = track->next; } /* CD_DA only audio * CD_ROM only mode1 with or without audio * CD_ROM_XA only mode2 with or without audio */ if (audio && !mode1 && !mode2) return CD_DA; else if ((audio && mode1 && !mode2) || (!audio && mode1 && !mode2)) return CD_ROM; else if ((audio && !mode1 && mode2) || (!audio && !mode1 && mode2)) return CD_ROM_XA; else return INVALID; } /* Return true if cuesheet contains any CD-Text data */ static int contains_cdtext(struct cuesheet *cs) { struct trackspec *track = cs->tracklist; if (cs->title[0] != '\0' || cs->performer[0] != '\0' || cs->songwriter[0] != '\0') return 1; while (track) { if (track->title[0] != '\0' || track->performer[0] != '\0' || track->songwriter[0] != '\0') return 1; track = track->next; } return 0; } /* fprintf() with indentation. The argument indent is the number of spaces to print per level. E.g. with indent=4 and level=3 there are 12 spaces printed. Every eight spaces are replaced by a single tabulator. The return value is the return value of fprintf(). */ static int ifprintf(FILE *f, int indent, int level, const char *format, ...) { va_list ap; int fprintf_return = 0; int tabs = indent * level / 8; int spaces = indent * level % 8; int i; for (i = 0; i < tabs; i++) fputc('\t', f); for (i = 0; i < spaces; i++) fputc(' ', f); va_start(ap, format); fprintf_return = vfprintf(f, format, ap); va_end(ap); return fprintf_return; } /* Write a track to the file f. The arguments i and l are the indentation amount and level (see ifprintf above). Do not write CD-Text data if cdtext is zero. */ static void write_track(struct trackspec *tr, FILE *f, int i, int l, int cdtext) { char timecode_buffer[TCBUFLEN]; long start = 0, len = 0; int j = 0; fprintf(f, "\n"); ifprintf(f, i, l++, "TRACK "); switch(tr->mode) { case AUDIO: fprintf(f, "AUDIO\n"); break; case MODE1: fprintf(f, "MODE1\n"); break; case MODE1_RAW: fprintf(f, "MODE1_RAW\n"); break; case MODE2: fprintf(f, "MODE2\n"); break; case MODE2_RAW: fprintf(f, "MODE2_RAW\n"); break; default: err_fail2("Unknown track mode"); /* cant get here */ } /* Flags and ISRC */ if (tr->copy) ifprintf(f, i, l, "COPY\n"); if (tr->pre_emphasis) ifprintf(f, i, l, "PRE_EMPHASIS\n"); if (tr->four_channel_audio) ifprintf(f, i, l, "FOUR_CHANNEL_AUDIO\n"); if (tr->isrc[0] != '\0') ifprintf(f, i, l, "ISRC \"%s\"\n", tr->isrc); /* CD-Text data */ if (cdtext && (tr->title[0] != '\0' || tr->performer[0] != '\0' || tr->songwriter[0] != '\0')) { ifprintf(f, i, l++, "CD_TEXT {\n"); ifprintf(f, i, l++, "LANGUAGE 0 {\n"); if (tr->title[0] != '\0') ifprintf(f, i, l, "TITLE \"%s\"\n", tr->title); if (tr->performer[0] != '\0') ifprintf(f, i, l, "PERFORMER \"%s\"\n", tr->performer); if (tr->songwriter[0] != '\0') ifprintf(f, i, l, "SONGWRITER \"%s\"\n", tr->songwriter); ifprintf(f, i, --l, "}\n"); /* LANGUAGE 0 { */ ifprintf(f, i, --l, "}\n"); /* CD_TEXT { */ } /* Pregap with zero data */ if (tr->pregap != -1 && !tr->pregap_data_from_file) { if (fr2tc(timecode_buffer, tr->pregap) == -1) err_fail2("Pregap out of range"); ifprintf(f, i, l, "PREGAP %s\n", timecode_buffer); } /* Specify the file */ start = 0; if (tr->mode == AUDIO) { ifprintf(f, i, l, "AUDIOFILE \"%s\" ", tr->filename); if (tr->start != -1) { if (tr->pregap_data_from_file) { start = tr->start - tr->pregap; } else start = tr->start; } if (fr2tc(timecode_buffer, start) == -1) err_fail2("Track start out of range"); fprintf(f, "%s", timecode_buffer); } else ifprintf(f, i, l, "DATAFILE \"%s\"", tr->filename); /* If next track has the same filename and specified a start value use the difference between start of this and start of the next track as the length of the current track */ if (tr->next && strcmp(tr->filename, tr->next->filename) == 0 && tr->next->start != -1) { if (tr->next->pregap_data_from_file) len = tr->next->start - tr->next->pregap - start; else len = tr->next->start - start; if (fr2tc(timecode_buffer, len) == -1) err_fail2("Track length out of range"); fprintf(f, " %s\n", timecode_buffer); } else fprintf(f, "\n"); /* Pregap with data from file */ if (tr->pregap_data_from_file) { if (fr2tc(timecode_buffer, tr->pregap) == -1) err_fail2("Pregap out of range"); ifprintf(f, i, l, "START %s\n", timecode_buffer); } /* Postgap */ if (tr->postgap != -1) { if (fr2tc(timecode_buffer, tr->postgap) == -1) err_fail2("Postgap out of range"); if (tr->mode == AUDIO) ifprintf(f, i, l, "SILENCE %s\n", timecode_buffer); else ifprintf(f, i, l, "ZERO %s\n", timecode_buffer); } /* Indexes */ while (tr->indexes[j] != -1 && i < NUM_OF_INDEXES) { if (fr2tc(timecode_buffer, tr->indexes[j++]) == -1) err_fail2("Index out of range"); ifprintf(f, i, l, "INDEX %s\n", timecode_buffer); } } /* Write the cuesheet cs to the file named in tocfile. If tocfile is NULL write to stdout. Do not write CD-Text data if cdt is zero. */ void write_toc(const char *tocfile, struct cuesheet *cs, int cdt) { FILE *f = stdout; int i = 4; /* number of chars for indentation */ int l = 0; /* current leven of indentation */ int cdtext = contains_cdtext(cs) && cdt; struct trackspec *track = cs->tracklist; if (tocfile != NULL) if ((f = fopen(tocfile, "w")) == NULL) { fprintf(stderr, "%s: Could not open file \"%s\" for " "writing: %s\n", progname, tocfile, strerror(errno)); exit(EXIT_FAILURE); } if ((cs->type = determine_session_type(cs->tracklist)) == INVALID) err_fail2("Invalid combination of track modes"); ifprintf(f, i, l, "// Generated by cue2toc 0.2\n"); ifprintf(f, i, l, "// Report bugs to \n"); if (cs->catalog[0] != '\0') ifprintf(f, i, l, "CATALOG \"%s\"\n", cs->catalog); switch (cs->type) { case CD_DA: ifprintf(f, i, l, "CD_DA\n"); break; case CD_ROM: ifprintf(f, i, l, "CD_ROM\n"); break; case CD_ROM_XA: ifprintf(f, i, l, "CD_ROM_XA\n"); break; default: err_fail2("Should never get here"); } if (cdtext) { ifprintf(f, i, l++, "CD_TEXT {\n"); ifprintf(f, i, l++, "LANGUAGE_MAP {\n"); ifprintf(f, i, l, "0 : EN\n"); ifprintf(f, i, --l, "}\n"); ifprintf(f, i, l++, "LANGUAGE 0 {\n"); if (cs->title[0] != '\0') ifprintf(f, i, l, "TITLE \"%s\"\n", cs->title); if (cs->performer[0] != '\0') ifprintf(f, i, l, "PERFORMER \"%s\"\n", cs->performer); if (cs->songwriter[0] != '\0') ifprintf(f, i, l, "SONGWRITER \"%s\"\n", cs->songwriter); ifprintf(f, i, --l, "}\n"); ifprintf(f, i, --l, "}\n"); } while (track) { write_track(track, f, i, l, cdtext); track = track->next; } }