Blob Blame History Raw
/* cue2toc.c - conversion routines
 * Copyright (C) 2004 Matthias Czapla <dermatsch@gmx.de>
 *
 * 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 <config.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <unistd.h>
#include <errno.h>
#include <stdarg.h>
#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 <dermatsch@gmx.de>\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;
	}
}