/* $Id: cdrom.c,v 1.4 1999/03/10 07:58:02 boyns Exp $ */

/*
 * Copyright (C) 1993-99 Mark R. Boyns <boyns@doit.org>
 *
 * This file is part of rplay.
 *
 * rplay 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.
 *
 * rplay 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 rplay; see the file COPYING.  If not, write to the
 * Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
 */



/* A lot of the CDDA code is based on read_cdda 1.01 by Jim Mintha
   (mintha@geog.ubc.ca) which seems to have borrowed code from Workman
   sources written by Steven Grimm (koreth@hyperion.com). */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#ifdef HAVE_CDROM

#include <sys/types.h>
#include <sys/errno.h>
#include "rplayd.h"
#include "cdrom.h"

CDROM_TABLE cdrom_table[MAX_CDROMS] =
{
#if defined (sun) && defined (SVR4)	/* Solaris 2.x */
    {"cdrom:", "/vol/dev/aliases/cdrom0", 0, 0},
    {"cdrom0:", "/vol/dev/aliases/cdrom0", 0, 0},
    {"cdrom1:", "/vol/dev/aliases/cdrom1", 0, 0},
    {"cdrom2:", "/vol/dev/aliases/cdrom2", 0, 0},
    {"cdrom3:", "/vol/dev/aliases/cdrom3", 0, 0},
#else				/* not solaris */
#if defined (linux)
    {"cdrom:", "/dev/cdrom", 0, 0},
    {"cdrom0:", "/dev/cdrom0", 0, 0},
    {"cdrom1:", "/dev/cdrom1", 0, 0},
    {"cdrom2:", "/dev/cdrom2", 0, 0},
    {"cdrom3:", "/dev/cdrom3", 0, 0},
#endif				/* linux */
#endif
};

typedef struct
{
    char *device;
    int starting_track;
    int ending_track;
    int num_blocks;
    int fd;
    char *buffer;
    int buffer_length;
    int starting_block;
    int current_block;
    int ending_block;
}
CDROM_INFO;

/* Internal prototypes. */
#ifdef HAVE_CDDA
#ifdef __STDC__
static void cdda_cdrom_reader(CDROM_TABLE * cdt, int starting_track, int ending_track, int output_fd);
#else
static void cdda_cdrom_reader( /* CDROM_TABLE *cdt, int starting_track, int ending_track, int output_fd */ );
#endif
#endif /* HAVE_CDDA */

#ifdef linux
#ifdef __STDC__
static void linux_cdrom_reader(CDROM_TABLE * cdt, int starting_track, int ending_track, int output_fd);
#else
static void linux_cdrom_reader( /* CDROM_TABLE *cdt, int starting_track, int ending_track, int output_fd */ );
#endif
#endif /* linux */

/* starting_track and ending_track can be 0 which means first and last */
#ifdef __STDC__
void
cdrom_reader(int index, int starting_track, int ending_track, int output_fd)
#else
void
cdrom_reader(index, starting_track, ending_track, output_fd)
    int index;
    int starting_track;
    int ending_track;
    int output_fd;
#endif
{
    CDROM_TABLE *cdt;

    cdt = &cdrom_table[index];

    report(REPORT_DEBUG, "cdrom_reader: %s%s tracks %d..%d\n",
	   cdt->name, cdt->device, starting_track, ending_track);

    rplay_audio_close();	/* prevent device busy problems */

#ifdef HAVE_CDDA
    cdda_cdrom_reader(cdt, starting_track, ending_track, output_fd);
#else
#ifdef linux
    linux_cdrom_reader(cdt, starting_track, ending_track, output_fd);
#endif /* linux */
#endif /* HAVE_CDDA */

    /* Add more cdrom readers here. */

    exit(0);
}


/******************************************************************************/

#ifdef HAVE_CDDA

/* A nicer interface to reading from the CDDA interface using the
   CDROM_INFO structure and the cdda_open, cdda_read, and cdda_close
   routines. */

#define CDDA_BLOCK_SIZE 2368
#define CDDA_BLOCK_IGNORE 16

static int cdda_open(CDROM_INFO * info);
static int cdda_read(CDROM_INFO * info);
static int cdda_close(CDROM_INFO * info);
static int internal_cdda_init(CDROM_INFO * info);
static int internal_cdda_read(CDROM_INFO * info);
static int internal_cdda_toc(CDROM_INFO * info);

#ifdef __STDC__
static void
cdda_cdrom_reader(CDROM_TABLE * cdt, int starting_track, int ending_track, int output_fd)
#else
static void
cdda_cdrom_reader(cdt, starting_track, ending_track, output_fd)
    CDROM_TABLE *cdt;
    int starting_track;
    int ending_track;
    int output_fd;
#endif
{
    CDROM_INFO info;
    int n, i;
    char *p;

    info.device = cdt->device;
    info.starting_track = starting_track;
    info.ending_track = ending_track;
    info.num_blocks = 30;
    info.fd = -1;
    info.buffer = NULL;

    if (cdda_open(&info) < 0)
    {
	return;
    }

    for (;;)
    {
	n = cdda_read(&info);
	if (n <= 0)
	{
	    break;
	}

	p = info.buffer;
	for (i = 0; i < info.num_blocks; i++)
	{
	    write(output_fd, p, CDDA_BLOCK_SIZE - CDDA_BLOCK_IGNORE);
	    p += CDDA_BLOCK_SIZE;
	}
    }

    cdda_close(&info);
}

#ifdef __STDC__
static int
cdda_open(CDROM_INFO * info)
#else
static int
cdda_open(info)
    CDROM_INFO *info;
#endif
{
    if (info->fd < 0)
    {
	if (internal_cdda_init(info) < 0)
	{
	    return -1;
	}
    }

    return internal_cdda_toc(info);
}

#ifdef __STDC__
static int
cdda_read(CDROM_INFO * info)
#else
static int
cdda_read(info)
    CDROM_INFO *info;
#endif
{
    int n;

    n = internal_cdda_read(info);
    if (n < 0)
    {
	return -1;
    }

    return n;
}

#ifdef __STDC__
static int
cdda_close(CDROM_INFO * info)
#else
static int
cdda_close(info)
    CDROM_INFO *info;
#endif
{
    close(info->fd);
    info->fd = -1;
    if (info->buffer)
    {
	free((char *) info->buffer);
    }
    info->buffer = NULL;
    return 0;
}

/* End of nicer CDDA interface. */

#include <sys/cdio.h>
#include <sys/ioctl.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <sys/scsi/impl/uscsi.h>
#include <sys/time.h>
#include <ustat.h>

#define CDDABLKSIZE 2368
#define SAMPLES_PER_BLK 588

#ifdef __STDC__
static int
internal_cdda_init(CDROM_INFO * info)
#else
static int
internal_cdda_init(info)
    CDROM_INFO *info;
#endif
{
    struct cdrom_cdda cdda;

    info->fd = open(info->device, 0);
    if (info->fd < 0)
    {
	perror(info->device);
	return -1;
    }

    info->buffer = (char *) malloc(info->num_blocks * CDDABLKSIZE + CDDABLKSIZE);
    if (info->buffer == NULL)
    {
	fprintf(stderr, "malloc: out of memory\n");
	return -1;
    }

    info->buffer_length = info->num_blocks * CDDABLKSIZE;

    cdda.cdda_addr = 200;
    cdda.cdda_length = 1;
    cdda.cdda_data = info->buffer;
    cdda.cdda_subcode = CDROM_DA_SUBQ;

    if (ioctl(info->fd, CDROMCDDA, &cdda) < 0)
    {
	perror("ioctl: CDROMCDDA");
	free((char *) info->buffer);
	info->buffer = NULL;
	close(info->fd);
	return -1;
    }

    return 0;
}

#ifdef __STDC__
static int
internal_cdda_read(CDROM_INFO * info)
#else
static int
internal_cdda_read(info)
    CDROM_INFO *info;
#endif
{
    struct cdrom_cdda cdda;
    int blk;
    unsigned char *q;

    if (info->current_block >= info->ending_block)
    {
	return 0;
    }

    cdda.cdda_addr = info->current_block;
    if (info->current_block + info->num_blocks > info->ending_block)
    {
	cdda.cdda_length = info->ending_block - info->current_block;
    }
    else
    {
	cdda.cdda_length = info->num_blocks;
    }

    cdda.cdda_data = info->buffer;
    cdda.cdda_subcode = CDROM_DA_SUBQ;

    if (ioctl(info->fd, CDROMCDDA, &cdda) < 0)
    {
	if (errno == ENXIO)
	{
	    perror("ioctl: CDROMCDDA: CD ejected");
	    return -1;
	}

	if (info->current_block + info->num_blocks > info->ending_block)
	{
	    return 0;
	}

	if (ioctl(info->fd, CDROMCDDA, &cdda) < 0)
	{
	    if (ioctl(info->fd, CDROMCDDA, &cdda) < 0)
	    {
		if (ioctl(info->fd, CDROMCDDA, &cdda) < 0)
		{
		    perror("ioctl: CDROMCDDA");
		    return -1;
		}
	    }
	}
    }

    info->current_block += cdda.cdda_length;

    return cdda.cdda_length * CDDABLKSIZE;
}

#ifdef __STDC__
static int
internal_cdda_toc(CDROM_INFO * info)
#else
static int
internal_cdda_toc(info)
    CDROM_INFO *info;
#endif
{
    struct cdrom_tochdr hdr;
    struct cdrom_tocentry entry;
    int i;

    if (ioctl(info->fd, CDROMREADTOCHDR, &hdr) < 0)
    {
	perror("ioctl: CDROMREADTOCHDR");
	return -1;
    }

    info->starting_block = 0;
    info->ending_block = 0;

    if (info->starting_track == 0)
    {
	info->starting_track = hdr.cdth_trk0;
    }
    if (info->ending_track == 0)
    {
	info->ending_track = hdr.cdth_trk1;
    }

    for (i = 1; i <= hdr.cdth_trk1; i++)
    {
	entry.cdte_track = i;
	entry.cdte_format = CDROM_MSF;

	if (ioctl(info->fd, CDROMREADTOCENTRY, &entry) < 0)
	{
	    perror("ioctl: CDROMREADTOCENTRY");
	    return -1;
	}

	if (i == info->starting_track)
	{
	    info->starting_block = entry.cdte_addr.msf.minute * 60 * 75 +
		entry.cdte_addr.msf.second * 75 + entry.cdte_addr.msf.frame;
	}

	if (i == info->ending_track + 1)
	{
	    info->ending_block = entry.cdte_addr.msf.minute * 60 * 75 +
		entry.cdte_addr.msf.second * 75 + entry.cdte_addr.msf.frame;
	}
    }

    entry.cdte_track = CDROM_LEADOUT;
    entry.cdte_format = CDROM_MSF;

    if (ioctl(info->fd, CDROMREADTOCENTRY, &entry) < 0)
    {
	perror("ioctl: CDROMREADTOCENTRY");
	return -1;
    }

    if (info->ending_block == 0)
    {
	info->ending_block = entry.cdte_addr.msf.minute * 60 * 75 +
	    entry.cdte_addr.msf.second * 75 + entry.cdte_addr.msf.frame;
    }

    /* Move back two seconds - don't know why but works */
    info->starting_block -= 150;
    info->ending_block -= 150;

    info->current_block = info->starting_block;

    return 0;
}

#else

/******************************************************************************/

#if defined (linux)

#include <linux/cdrom.h>

#ifdef __STDC__
static int
linux_read_toc(CDROM_INFO * info)
#else
static int
linux_read_toc(info)
    CDROM_INFO *info;
#endif
{
    struct cdrom_tochdr hdr;
    struct cdrom_tocentry entry;
    int i;

    if (ioctl(info->fd, CDROMREADTOCHDR, &hdr) < 0)
    {
	perror("ioctl: CDROMREADTOCHDR");
	return -1;
    }

    info->starting_block = 0;
    info->ending_block = 0;

    if (info->starting_track == 0)
    {
	info->starting_track = hdr.cdth_trk0;
    }
    if (info->ending_track == 0)
    {
	info->ending_track = hdr.cdth_trk1;
    }

    for (i = 1; i <= hdr.cdth_trk1; i++)
    {
	entry.cdte_track = i;
	entry.cdte_format = CDROM_MSF;

	if (ioctl(info->fd, CDROMREADTOCENTRY, &entry) < 0)
	{
	    perror("ioctl: CDROMREADTOCENTRY");
	    return -1;
	}

	if (i == info->starting_track)
	{
	    info->starting_block = entry.cdte_addr.msf.minute * 60 * 75 +
		entry.cdte_addr.msf.second * 75 + entry.cdte_addr.msf.frame;
	}

	if (i == info->ending_track + 1)
	{
	    info->ending_block = entry.cdte_addr.msf.minute * 60 * 75 +
		entry.cdte_addr.msf.second * 75 + entry.cdte_addr.msf.frame;
	}
    }

    entry.cdte_track = CDROM_LEADOUT;
    entry.cdte_format = CDROM_MSF;

    if (ioctl(info->fd, CDROMREADTOCENTRY, &entry) < 0)
    {
	perror("ioctl: CDROMREADTOCENTRY");
	return -1;
    }

    if (info->ending_block == 0)
    {
	info->ending_block = entry.cdte_addr.msf.minute * 60 * 75 +
	    entry.cdte_addr.msf.second * 75 + entry.cdte_addr.msf.frame;
    }

    /* Move back two seconds - don't know why but works */
    info->starting_block -= 150;
    info->ending_block -= 150;

    info->current_block = info->starting_block;

    return 0;
}

#ifdef __STDC__
static void
linux_cdrom_reader(CDROM_TABLE * cdt, int starting_track, int ending_track, int output_fd)
#else
static void
linux_cdrom_reader(cdt, starting_track, ending_track, output_fd)
    CDROM_TABLE *cdt;
    int starting_track;
    int ending_track;
    int output_fd;
#endif
{
    CDROM_INFO info;
    int n, i;
    char *p;

    info.device = cdt->device;
    info.starting_track = starting_track;
    info.ending_track = ending_track;
    info.num_blocks = 30;
    info.fd = -1;
    info.buffer = NULL;

    info.fd = open(info.device, 0);
    if (info.fd < 0)
    {
	perror(info.device);
	return;
    }

    info.buffer = (char *) malloc(info.num_blocks * CD_FRAMESIZE_RAW + CD_FRAMESIZE_RAW);
    if (info.buffer == NULL)
    {
	fprintf(stderr, "malloc: out of memory\n");
	return;
    }

    linux_read_toc(&info);

/*     printf ("start track=%d block=%d\n", info.starting_track, info.starting_block); */
/*     printf ("end track=%d block=%d\n", info.ending_track, info.ending_block); */

    while (info.current_block < info.ending_block)
    {
	int frames;
	int max_retries = 5;

	if (info.current_block + info.num_blocks > info.ending_block)
	{
	    frames = info.ending_block - info.current_block;
	}
	else
	{
	    frames = info.num_blocks;
	}

	for (i = 0; i < max_retries; i++)
	{
	    struct cdrom_read_audio audio;

	    audio.addr_format = CDROM_LBA;
	    audio.addr.lba = info.current_block;
	    audio.nframes = frames;
	    audio.buf = info.buffer;

	    if (ioctl(info.fd, CDROMREADAUDIO, &audio) >= 0)
	    {
		break;
	    }
	}
	if (i == max_retries)
	{
	    perror(info.device);
	    break;
	}

	if (write(output_fd, info.buffer, CD_FRAMESIZE_RAW * frames) < 0)
	{
	    break;
	}

	info.current_block += frames;
    }

    free(info.buffer);
    close(info.fd);
}

#endif /* linux */
#endif /* HAVE_CDDA */
#endif /* HAVE_CDROM */
