view src/cdaudio/cdaudio.c @ 972:cf7021ca4e7b trunk

[svn] Add lastfm:// transport, an abstract VFS class which derives from curl to provide lastfm radio support. Written by majeru with some cleanups by me. Most last.fm metadata support isn't yet implemented, however, and will need to be done by majeru. ;)
author nenolod
date Sun, 22 Apr 2007 04:16:08 -0700
parents 914c96de3244
children daa0323924d6
line wrap: on
line source

/*  XMMS - Cross-platform multimedia player
 *  Copyright (C) 1998-2003  Peter Alm, Mikael Alm, Olle Hallnas,
 *                           Thomas Nilsson and 4Front Technologies
 *  Copyright (C) 1999-2003  Haavard Kvaalen <havardk@xmms.org>
 *
 *  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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#ifdef HAVE_CONFIG_H
#  include "config.h"
#endif

#include "cdaudio.h"

#include <glib.h>
#include <audacious/i18n.h>
#include <glib/gprintf.h>
#include <string.h>
#include <stdlib.h>

#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/stat.h>

#include <audacious/configdb.h>
#include <audacious/util.h>
#include <audacious/titlestring.h>
#include "audacious/output.h"

#ifdef CDROMSTOP
# define XMMS_STOP CDROMSTOP
#elif defined CDIOCSTOP
# define XMMS_STOP CDIOCSTOP
#else
# error "No stop ioctl"
#endif

#ifdef CDIOCPAUSE
# define XMMS_PAUSE CDIOCPAUSE
#elif defined CDROMPAUSE
# define XMMS_PAUSE CDROMPAUSE
#else
# error "No pause ioctl"
#endif

#ifdef CDIOCRESUME
# define XMMS_RESUME CDIOCRESUME
#elif defined CDROMRESUME
# define XMMS_RESUME CDROMRESUME
#else
# error "No resume ioctl"
#endif

/*
 * Distributions should not patch this, but instead use the
 * --with-cdda-device=path and --with-cdda-dir=path configure options.
 */

#ifndef CDDA_DEVICE
# ifdef HAVE_SYS_CDIO_H
#  if defined(__FreeBSD__) && !defined(CDIOCREADAUDIO)
#   define CDDA_DEVICE "/dev/acd0c"
#  elif defined __FreeBSD__
#   define CDDA_DEVICE "/dev/acd0"
#  elif defined __OpenBSD__ || defined __NetBSD__
#   define CDDA_DEVICE "/dev/cd0c"
#  else
#   define CDDA_DEVICE "/vol/dev/aliases/cdrom0"
#  endif
# else
#   define CDDA_DEVICE "/dev/cdrom"
# endif
#endif

#ifndef CDDA_DIRECTORY
# ifdef HAVE_SYS_CDIO_H
#  ifdef __FreeBSD__
#   define CDDA_DIRECTORY "/cdrom"
#  elif defined __OpenBSD__ || defined __NetBSD__
#   define CDDA_DIRECTORY "/cdrom"
#  else
#   define CDDA_DIRECTORY "/cdrom/cdrom0"
#  endif
# else
#   define CDDA_DIRECTORY "/mnt/cdrom"
# endif
#endif




static TitleInput *cdda_get_tuple(cdda_disc_toc_t * toc, int track);
static gchar *get_song_title(TitleInput *tuple);
static gboolean stop_timeout(gpointer data);

static void cdda_init(void);
static int is_our_file(char *filename);
static GList *scan_dir(char *dir);
static void play_file(InputPlayback *playback);
static void stop(InputPlayback *playback);
static void cdda_pause(InputPlayback *playback, short p);
static void seek(InputPlayback *playback, int time);
static int get_time(InputPlayback *playback);
static void get_song_info(char *filename, char **title, int *length);
static TitleInput *get_song_tuple(char *filename);
static gint get_volume(int *l, int *r);
static gint set_volume(int l, int r);
static void cleanup(void);
void cdda_fileinfo(char *filename);

InputPlugin cdda_ip = {
    NULL,
    NULL,
    NULL,                       /* Description */
    cdda_init,
    NULL,                       /* about */
    cdda_configure,
    is_our_file,
    scan_dir,
    play_file,
    stop,
    cdda_pause,
    seek,
    NULL,                       /* set_eq */
    get_time,
    get_volume,
    set_volume,
    cleanup,
    NULL,                       /* obsolete */
    NULL,                       /* add_vis_pcm */
    NULL,                       /* set_info, filled in by xmms */
    NULL,                       /* set_info_text, filled in by xmms */
    get_song_info,
    NULL,                       /*  cdda_fileinfo, *//* file_info_box */
    NULL,                        /* output plugin handle */
    get_song_tuple
};

CDDAConfig cdda_cfg;

static struct {
    struct driveinfo drive;
    cdda_disc_toc_t cd_toc;
    int track;
    int fd;
    gboolean playing;
} cdda_playing;

static struct {
    GThread *thread;
    gboolean audio_error, eof;
    int seek;

} dae_data;

static gboolean is_paused;
static int pause_time;

struct timeout {
    int id;
    char *device;
};

static GList *timeout_list;

/* Time to delay stop command in 1/10 second */
#define STOP_DELAY 20

InputPlugin *
get_iplugin_info(void)
{
    cdda_ip.description = g_strdup_printf(_("CD Audio Plugin"));
    return &cdda_ip;
}



#ifdef BEEP_CDROM_SOLARIS
/*
 * Lowlevel cdrom access, Solaris style (Solaris, Linux)
 */

static void
play_ioctl(struct cdda_msf *start, struct cdda_msf *end)
{
    struct cdrom_msf msf;

    msf.cdmsf_min0 = start->minute;
    msf.cdmsf_sec0 = start->second;
    msf.cdmsf_frame0 = start->frame;
    msf.cdmsf_min1 = end->minute;
    msf.cdmsf_sec1 = end->second;
    msf.cdmsf_frame1 = end->frame;
    ioctl(cdda_playing.fd, CDROMPLAYMSF, &msf);
}

static int
get_current_frame(void)
{
    struct cdrom_subchnl subchnl;

    subchnl.cdsc_format = CDROM_MSF;
    if (ioctl(cdda_playing.fd, CDROMSUBCHNL, &subchnl) < 0)
        return -1;

    switch (subchnl.cdsc_audiostatus) {
    case CDROM_AUDIO_COMPLETED:
    case CDROM_AUDIO_ERROR:
        return -1;
    }

    return (LBA(subchnl.cdsc_absaddr.msf));
}

#if !defined(CDROMVOLREAD)
static int volume_left = 100, volume_right = 100;
#endif

static void
drive_get_volume(int *l, int *r)
{
#if defined(CDROMVOLREAD)
    struct cdrom_volctrl vol;

    if (cdda_playing.fd != -1 && !ioctl(cdda_playing.fd, CDROMVOLREAD, &vol)) {
        *l = (100 * vol.channel0) / 255;
        *r = (100 * vol.channel1) / 255;
    }
#if 0
    else if (cdda_playing.fd != -1)
        g_message("CDROMVOLREAD failed");
#endif
#else
    *l = volume_left;
    *r = volume_right;
#endif
}

static void
drive_set_volume(int l, int r)
{
    struct cdrom_volctrl vol;

    if (cdda_playing.fd != -1) {
        vol.channel0 = vol.channel2 = (l * 255) / 100;
        vol.channel1 = vol.channel3 = (r * 255) / 100;
        ioctl(cdda_playing.fd, CDROMVOLCTRL, &vol);
    }
#if !defined(CDROMVOLREAD)
    volume_left = l;
    volume_right = r;
#endif
}

#ifdef CDROMREADAUDIO
int
read_audio_data(int fd, int pos, int num, void *buf)
{
    struct cdrom_read_audio cdra;

#if 1
    cdra.addr.lba = pos - CDDA_MSF_OFFSET;
    cdra.addr_format = CDROM_LBA;
#else
    cdra.addr.msf.minute = pos / (60 * 75);
    cdra.addr.msf.second = (pos / 75) % 60;
    cdra.addr.msf.frame = pos % 75;
    cdra.addr_format = CDROM_MSF;
#endif

    cdra.nframes = num;
    cdra.buf = buf;

    if (ioctl(fd, CDROMREADAUDIO, &cdra) < 0)
        return -errno;

    return cdra.nframes;
}
#endif                          /* CDROMREADAUDIO */

#if defined(CDROMCDDA)
int
read_audio_data(int fd, int pos, int num, void *buf)
{
    struct cdrom_cdda cdra;

    cdra.cdda_addr = pos - CDDA_MSF_OFFSET;
    cdra.cdda_length = num;
    cdra.cdda_data = buf;
    cdra.cdda_subcode = CDROM_DA_NO_SUBCODE;
    if (ioctl(fd, CDROMCDDA, &cdra) < 0)
        return -errno;

    return cdra.cdda_length;
}
#endif

static gboolean
cdda_get_toc_lowlevel(int fd, cdda_disc_toc_t * info)
{
    struct cdrom_tochdr tochdr;
    struct cdrom_tocentry tocentry;
    int i;



    if (ioctl(fd, CDROMREADTOCHDR, &tochdr))
        return FALSE;

    for (i = tochdr.cdth_trk0; i <= tochdr.cdth_trk1; i++) {
        tocentry.cdte_format = CDROM_MSF;
        tocentry.cdte_track = i;
        if (ioctl(fd, CDROMREADTOCENTRY, &tocentry))
            return FALSE;
        info->track[i].minute = tocentry.cdte_addr.msf.minute;
        info->track[i].second = tocentry.cdte_addr.msf.second;
        info->track[i].frame = tocentry.cdte_addr.msf.frame;
        info->track[i].flags.data_track =
            tocentry.cdte_ctrl == CDROM_DATA_TRACK;

    }

    /* Get the leadout track */
    tocentry.cdte_track = CDROM_LEADOUT;
    tocentry.cdte_format = CDROM_MSF;

    if (ioctl(fd, CDROMREADTOCENTRY, &tocentry))
        return FALSE;
    info->leadout.minute = tocentry.cdte_addr.msf.minute;
    info->leadout.second = tocentry.cdte_addr.msf.second;
    info->leadout.frame = tocentry.cdte_addr.msf.frame;

    info->first_track = tochdr.cdth_trk0;
    info->last_track = tochdr.cdth_trk1;

    return TRUE;
}

#endif

#ifdef BEEP_CDROM_BSD
/*
 * Lowlevel cdrom access, BSD style (FreeBSD, OpenBSD, NetBSD, Darwin)
 */

static void
play_ioctl(struct cdda_msf *start, struct cdda_msf *end)
{
    struct ioc_play_msf msf;

    msf.start_m = start->minute;
    msf.start_s = start->second;
    msf.start_f = start->frame;
    msf.end_m = end->minute;
    msf.end_s = end->second;
    msf.end_f = end->frame;
    ioctl(cdda_playing.fd, CDIOCPLAYMSF, &msf);
}

static int
get_current_frame(void)
{
    struct ioc_read_subchannel subchnl;
    struct cd_sub_channel_info subinfo;
    subchnl.address_format = CD_MSF_FORMAT;
    subchnl.data_format = CD_CURRENT_POSITION;
    subchnl.track = 0;
    subchnl.data_len = sizeof(subinfo);
    subchnl.data = &subinfo;
    if (ioctl(cdda_playing.fd, CDIOCREADSUBCHANNEL, &subchnl) < 0)
        return -1;

#ifdef BEEP_CDROM_BSD_DARWIN
    return ((subchnl.data->what.position.absaddr[1] * 60
             subchnl.data->what.position.absaddr[2]) * 75 +
            subchnl.data->what.position.absaddr[3]);
#else
    return (LBA(subchnl.data->what.position.absaddr.msf));
#endif
}

static void
drive_get_volume(int *l, int *r)
{
    struct ioc_vol vol;

    if (cdda_playing.fd != -1) {
        ioctl(cdda_playing.fd, CDIOCGETVOL, &vol);
        *l = (100 * vol.vol[0]) / 255;
        *r = (100 * vol.vol[1]) / 255;
    }
}

static void
drive_set_volume(int l, int r)
{
    struct ioc_vol vol;

    if (cdda_playing.fd != -1) {
        vol.vol[0] = vol.vol[2] = (l * 255) / 100;
        vol.vol[1] = vol.vol[3] = (r * 255) / 100;
        ioctl(cdda_playing.fd, CDIOCSETVOL, &vol);
    }
}

#if defined(__FreeBSD__) && !defined(CDIOCREADAUDIO)
int
read_audio_data(int fd, int pos, int num, void *buf)
{
    int bs = CD_FRAMESIZE_RAW;

    if (ioctl(fd, CDRIOCSETBLOCKSIZE, &bs) == -1)
	return -1;
    if (pread(fd, buf, num * bs, (pos - 150) * bs) != num * bs)
	return -1;

    return num;
}
#endif

#if defined(CDIOCREADAUDIO)
int
read_audio_data(int fd, int pos, int num, void *buf)
{
    struct ioc_read_audio cdra;

    cdra.address.lba = pos - CDDA_MSF_OFFSET;
    cdra.address_format = CD_LBA_FORMAT;
    cdra.nframes = num;
    cdra.buffer = buf;

    if (ioctl(fd, CDIOCREADAUDIO, &cdra) < 0)
        return -errno;

    return cdra.nframes;
}
#endif                          /* CDIOCREADAUDIO */

#ifdef BEEP_CDROM_BSD_NETBSD    /* NetBSD, OpenBSD */

static gboolean
cdda_get_toc_lowlevel(int fd, cdda_disc_toc_t * info)
{
    struct ioc_toc_header tochdr;
    struct ioc_read_toc_entry tocentry;
    struct cd_toc_entry tocentrydata;
    int i;

    if (ioctl(fd, CDIOREADTOCHEADER, &tochdr))
        return FALSE;

    for (i = tochdr.starting_track; i <= tochdr.ending_track; i++) {
        tocentry.address_format = CD_MSF_FORMAT;

        tocentry.starting_track = i;
        tocentry.data = &tocentrydata;
        tocentry.data_len = sizeof(tocentrydata);
        if (ioctl(fd, CDIOREADTOCENTRYS, &tocentry))
            return FALSE;
        info->track[i].minute = tocentry.data->addr.msf.minute;
        info->track[i].second = tocentry.data->addr.msf.second;
        info->track[i].frame = tocentry.data->addr.msf.frame;
        info->track[i].flags.data_track = (tocentry.data->control & 4) == 4;
    }

    /* Get the leadout track */
    tocentry.address_format = CD_MSF_FORMAT;

    tocentry.starting_track = 0xAA;
    tocentry.data = &tocentrydata;
    tocentry.data_len = sizeof(tocentrydata);
    if (ioctl(fd, CDIOREADTOCENTRYS, &tocentry))
        return FALSE;
    info->leadout.minute = tocentry.data->addr.msf.minute;
    info->leadout.second = tocentry.data->addr.msf.second;
    info->leadout.frame = tocentry.data->addr.msf.frame;

    info->first_track = tochdr.starting_track;
    info->last_track = tochdr.ending_track;

    return TRUE;
}

#elif defined(BEEP_CDROM_BSD_DARWIN)

static gboolean
cdda_get_toc_lowlevel(int fd, cdda_disc_toc_t * info)
{
    struct ioc_toc_header tochdr;
    struct ioc_read_toc_entry tocentry;
    int i;

    if (ioctl(fd, CDIOREADTOCHEADER, &tochdr))
        return FALSE;

    for (i = tochdr.starting_track; i <= tochdr.ending_track; i++) {
        tocentry.address_format = CD_MSF_FORMAT;

        tocentry.starting_track = i;
        if (ioctl(fd, CDIOREADTOCENTRYS, &tocentry))
            return FALSE;
        info->track[i].minute = tocentry.data->addr[1];
        info->track[i].second = tocentry.data->addr[2];
        info->track[i].frame = tocentry.data->addr[3];
        info->track[i].flags.data_track = (tocentry.data->control & 4) == 4;
    }

    /* Get the leadout track */
    tocentry.address_format = CD_MSF_FORMAT;

    tocentry.starting_track = 0xAA;
    if (ioctl(fd, CDIOREADTOCENTRYS, &tocentry))
        return FALSE;
    info->leadout.minute = tocentry.data->addr[1];
    info->leadout.second = tocentry.data->addr[2];
    info->leadout.frame = tocentry.data->addr[3];

    return TRUE;
}

#else                           /* FreeBSD */

static gboolean
cdda_get_toc_lowlevel(int fd, cdda_disc_toc_t * info)
{
    struct ioc_toc_header tochdr;
    struct ioc_read_toc_single_entry tocentry;
    int i;

    if (ioctl(fd, CDIOREADTOCHEADER, &tochdr))
        return FALSE;

    for (i = tochdr.starting_track; i <= tochdr.ending_track; i++) {
        tocentry.address_format = CD_MSF_FORMAT;

        tocentry.track = i;
        if (ioctl(fd, CDIOREADTOCENTRY, &tocentry))
            return FALSE;
        info->track[i].minute = tocentry.entry.addr.msf.minute;
        info->track[i].second = tocentry.entry.addr.msf.second;
        info->track[i].frame = tocentry.entry.addr.msf.frame;
        info->track[i].flags.data_track = (tocentry.entry.control & 4) == 4;
    }

    /* Get the leadout track */
    tocentry.address_format = CD_MSF_FORMAT;

    tocentry.track = 0xAA;
    if (ioctl(fd, CDIOREADTOCENTRY, &tocentry))
        return FALSE;
    info->leadout.minute = tocentry.entry.addr.msf.minute;
    info->leadout.second = tocentry.entry.addr.msf.second;
    info->leadout.frame = tocentry.entry.addr.msf.frame;

    info->first_track = tochdr.starting_track;
    info->last_track = tochdr.ending_track;

    return TRUE;
}
#endif

#endif











extern gboolean
is_mounted(const char *device_name)
{
#if defined(HAVE_MNTENT_H) || defined(HAVE_GETMNTINFO)
    char devname[256];
    struct stat st;
#if defined(HAVE_MNTENT_H)
    FILE *mounts;
    struct mntent *mnt;
#elif defined(HAVE_GETMNTINFO)
#if defined __NetBSD__ && HAVE_STATVFS
    struct statvfs *fsp;
#else
    struct statfs *fsp;
#endif
    int entries;
#endif

    if (lstat(device_name, &st) < 0)
        return -1;

    if (S_ISLNK(st.st_mode))
        readlink(device_name, devname, 256);
    else
        strncpy(devname, device_name, 256);

#if defined(HAVE_MNTENT_H)
    if ((mounts = setmntent(MOUNTED, "r")) == NULL)
        return TRUE;

    while ((mnt = getmntent(mounts)) != NULL) {
        if (strcmp(mnt->mnt_fsname, devname) == 0) {
            endmntent(mounts);
            return TRUE;
        }
    }
    endmntent(mounts);
#elif defined(HAVE_GETMNTINFO)
    entries = getmntinfo(&fsp, MNT_NOWAIT);
    if (entries < 0)
        return FALSE;

    while (entries-- > 0) {
        if (!strcmp(fsp->f_mntfromname, devname))
            return TRUE;
        fsp++;
    }
#endif
#endif
    return FALSE;
}


gboolean
cdda_get_toc(cdda_disc_toc_t * info, const char *device)
{
    gboolean retv = FALSE;
    int fd;

    if (is_mounted(device))
        return FALSE;

    if ((fd = open(device, CDOPENFLAGS)) == -1)
        return FALSE;

    memset(info, 0, sizeof(cdda_disc_toc_t));

    retv = cdda_get_toc_lowlevel(fd, info);
    close(fd);

    return retv;
}

static void
cdda_init(void)
{
    ConfigDb *db;
    struct driveinfo *drive = g_malloc0(sizeof(struct driveinfo));
    int ndrives = 1, i;

    cdda_playing.fd = -1;
    memset(&cdda_cfg, 0, sizeof(CDDAConfig));

#ifdef HAVE_OSS
    drive->mixer = CDDA_MIXER_OSS;
    drive->oss_mixer = SOUND_MIXER_CD;
#endif

    db = bmp_cfg_db_open();

    /* These names are used for backwards compatibility */
    bmp_cfg_db_get_string(db, "CDDA", "device", &drive->device);
    bmp_cfg_db_get_string(db, "CDDA", "directory", &drive->directory);
    bmp_cfg_db_get_int(db, "CDDA", "mixer", &drive->mixer);
    if (bmp_cfg_db_get_int(db, "CDDA", "readmode", &drive->dae)==FALSE)
        drive->dae = 1; /* default to digital */

    if (!drive->device)
        drive->device = g_strdup(CDDA_DEVICE);
    if (!drive->directory)
        drive->directory = g_strdup(CDDA_DIRECTORY);

    cdda_cfg.drives = g_list_append(cdda_cfg.drives, drive);

    bmp_cfg_db_get_int(db, "CDDA", "num_drives", &ndrives);
    for (i = 1; i < ndrives; i++) {
        char label[20];
        drive = g_malloc0(sizeof(struct driveinfo));

        sprintf(label, "device%d", i);
        bmp_cfg_db_get_string(db, "CDDA", label, &drive->device);

        sprintf(label, "directory%d", i);
        bmp_cfg_db_get_string(db, "CDDA", label, &drive->directory);

        sprintf(label, "mixer%d", i);
        bmp_cfg_db_get_int(db, "CDDA", label, &drive->mixer);

        sprintf(label, "readmode%d", i);
        bmp_cfg_db_get_int(db, "CDDA", label, &drive->dae);

        cdda_cfg.drives = g_list_append(cdda_cfg.drives, drive);
    }
    bmp_cfg_db_get_bool(db, "CDDA", "title_override",
                        &cdda_cfg.title_override);
    bmp_cfg_db_get_string(db, "CDDA", "name_format", &cdda_cfg.name_format);
    bmp_cfg_db_get_bool(db, "CDDA", "use_cddb", &cdda_cfg.use_cddb);
    bmp_cfg_db_get_string(db, "CDDA", "cddb_server", &cdda_cfg.cddb_server);
    bmp_cfg_db_close(db);

    if (!cdda_cfg.cddb_server)
        cdda_cfg.cddb_server = g_strdup(CDDB_DEFAULT_SERVER);
    if (!cdda_cfg.name_format)
        cdda_cfg.name_format = g_strdup("%p - %t");
}

struct driveinfo *
cdda_find_drive(char *filename)
{
    GList *node;

    // FIXME: Will always return the first drive

    for (node = cdda_cfg.drives; node; node = node->next) {
        struct driveinfo *d = node->data;
        if (!strncmp(d->directory, filename, strlen(d->directory)))
            return d;
    }

    return NULL;

}

static void
timeout_destroy(struct timeout *entry)
{
    g_free(entry->device);
    g_free(entry);
    timeout_list = g_list_remove(timeout_list, entry);
}

static void
timeout_remove_for_device(char *device)
{
    GList *node;

    for (node = timeout_list; node; node = node->next) {
        struct timeout *t = node->data;

        if (!strcmp(t->device, device)) {
            gtk_timeout_remove(t->id);
            timeout_destroy(t);
            return;
        }
    }

}

static void
cleanup(void)
{
    GList *node;
    struct driveinfo *drive;

    g_free(cdda_ip.description);
    cdda_ip.description = NULL;

    if (cdda_cfg.drives) {
        for (node = g_list_first(cdda_cfg.drives); node; node = node->next) {
            drive = (struct driveinfo *)node->data;
            if (!drive)
                continue;

            if (drive->device)
                free(drive->device);

            if (drive->directory)
                free(drive->directory);

            free(drive);
        }

        g_list_free(cdda_cfg.drives);
        cdda_cfg.drives = NULL;
    }

    if (cdda_cfg.name_format) {
        free(cdda_cfg.name_format);
        cdda_cfg.name_format = NULL;
    }

    if (cdda_cfg.cddb_server) {
        free(cdda_cfg.cddb_server);
        cdda_cfg.cddb_server = NULL;
    }

    while (timeout_list) {
        struct timeout *t = timeout_list->data;
        gtk_timeout_remove(t->id);
        stop_timeout(t);
        timeout_destroy(t);
    }
    cddb_quit();
}

static int
is_our_file(char *filename)
{
    char *ext = ".cda";

    if (cdda_find_drive(filename) == NULL) {
        return FALSE;
    }

    if (g_str_has_suffix(filename, ext)) {
        return TRUE;
    }
    return FALSE;
}


static GList *
scan_dir(char *dir)
{
    GList *list = NULL;
    int i;
    cdda_disc_toc_t toc;
    struct driveinfo *drive;

    if ((drive = cdda_find_drive(dir)) == NULL)
        return NULL;

    if (!cdda_get_toc(&toc, drive->device))
        return NULL;

    for (i = toc.last_track; i >= toc.first_track; i--)
        if (!toc.track[i].flags.data_track) {
            list = g_list_prepend(list, g_strdup_printf("Track %02d.cda", i));
        }
    return list;
}

guint
cdda_calculate_track_length(cdda_disc_toc_t * toc, int track)
{
    if (track == toc->last_track)
        return (LBA(toc->leadout) - LBA(toc->track[track]));
    else
        return (LBA(toc->track[track + 1]) - LBA(toc->track[track]));
}

static void *
dae_play_loop(void *data)
{
    InputPlayback *playback = data;
    char *buffer = g_malloc(CD_FRAMESIZE_RAW * CDDA_DAE_FRAMES);
    int pos = LBA(cdda_playing.cd_toc.track[cdda_playing.track]);
    int end, frames;

    if (cdda_playing.track == cdda_playing.cd_toc.last_track)
        end = LBA(cdda_playing.cd_toc.leadout);
    else
        end = LBA(cdda_playing.cd_toc.track[cdda_playing.track + 1]);

    while (cdda_playing.playing) {
        int left;
        char *data;

        if (dae_data.seek != -1) {
	    playback->output->flush(dae_data.seek * 1000);
            pos = LBA(cdda_playing.cd_toc.track[cdda_playing.track])
                + dae_data.seek * 75;
            dae_data.seek = -1;
            dae_data.eof = FALSE;
        }
        frames = MIN(CDDA_DAE_FRAMES, end - pos);
        if (frames == 0)
            dae_data.eof = TRUE;

        if (dae_data.eof) {
            xmms_usleep(30000);
            continue;
        }

        frames = read_audio_data(cdda_playing.fd, pos, frames, buffer);
        if (frames <= 0) {
            int err = -frames;
            if (err == EOPNOTSUPP)
                dae_data.eof = TRUE;
            else {
                /*
                 * If the read failed, skip ahead to
                 * avoid getting stuck on scratches
                 * and such.
                 */
                g_message("read_audio_data() failed:  %s (%d)",
                          strerror(err), err);
                pos += MIN(CDDA_DAE_FRAMES, end - pos);
            }
            continue;
        }
        left = frames * CD_FRAMESIZE_RAW;
        data = buffer;
        while (cdda_playing.playing && left > 0 && dae_data.seek == -1) {
            int cur = MIN(512 * 2 * 2, left);
            cdda_ip.add_vis_pcm(playback->output->written_time(),
                                FMT_S16_LE, 2, cur, data);
            while (playback->output->buffer_free() < cur &&
                   cdda_playing.playing && dae_data.seek == -1)
                xmms_usleep(30000);
            if (cdda_playing.playing && dae_data.seek == -1)
                produce_audio(playback->output->written_time(), FMT_S16_LE, 2, cur, data, &cdda_playing.playing);
            left -= cur;
            data += cur;
        }
        pos += frames;
    }

    playback->output->buffer_free();
    playback->output->buffer_free();
    g_free(buffer);

    g_thread_exit(NULL);
    return NULL;
}

static void
dae_play(InputPlayback *playback)
{
    if (playback->output->open_audio(FMT_S16_LE, 44100, 2) == 0) {
        dae_data.audio_error = TRUE;
        cdda_playing.playing = FALSE;
        return;
    }
    dae_data.seek = -1;
    dae_data.eof = FALSE;
    dae_data.audio_error = FALSE;
    dae_data.thread = g_thread_create(dae_play_loop, playback, TRUE, NULL);
}

static void
play_file(InputPlayback *playback)
{
    char *filename = playback->filename;
    char *tmp;
    struct driveinfo *drive;
    int track;
    int track_len;

//      g_message(g_strdup_printf("** CD_AUDIO: trying to play file %s",filename));

    if ((drive = cdda_find_drive(filename)) == NULL) {
//              g_message("** CD_AUDIO: find drive check failed");
        return;
    }
    if (is_mounted(drive->device)) {
//              g_message("** CD_AUDIO: drive is mounted");
        return;
    }
    tmp = strrchr(filename, '/');
    if (tmp)
        tmp++;
    else
        tmp = filename;

    if (!sscanf(tmp, "Track %d.cda", &track)) {
//              g_message("** CD_AUDIO: filename check failed");                
        return;
    }

    if (!cdda_get_toc(&cdda_playing.cd_toc, drive->device) ||
        cdda_playing.cd_toc.track[track].flags.data_track ||
        track < cdda_playing.cd_toc.first_track ||
        track > cdda_playing.cd_toc.last_track) {
//              g_message("** CD_AUDIO: toc check failed");             
        return;
    }

    if ((cdda_playing.fd = open(drive->device, CDOPENFLAGS)) == -1) {
//              g_message("** CD_AUDIO: device open failed");           
        return;
    }
    track_len = cdda_calculate_track_length(&cdda_playing.cd_toc, track);
    cdda_ip.set_info(get_song_title(cdda_get_tuple(&cdda_playing.cd_toc, track)),
                     (track_len * 1000) / 75, 44100 * 2 * 2 * 8, 44100, 2);

    memcpy(&cdda_playing.drive, drive, sizeof(struct driveinfo));
#ifndef CDDA_HAS_READAUDIO
    cdda_playing.drive.dae = FALSE;
#endif

    cdda_playing.track = track;

    is_paused = FALSE;
    timeout_remove_for_device(drive->device);

    cdda_playing.playing = TRUE;
    if (drive->dae)
        dae_play(playback);
    else
        seek(playback, 0);
    return;
}

static TitleInput *
cdda_get_tuple(cdda_disc_toc_t * toc, int track)
{
    G_LOCK_DEFINE_STATIC(tuple);

    static guint32 cached_id;
    static cdinfo_t cdinfo;
    static gchar *performer, *album_name, *track_name;
    TitleInput *tuple = NULL;
    guint32 disc_id;

    disc_id = cdda_cddb_compute_discid(toc);

    /*
     * We want to avoid looking up a album from two threads simultaneously.
     * This can happen since we are called both from the main-thread and
     * from the playlist-thread.
     */

    G_LOCK(tuple);
    if (!(disc_id == cached_id && cdinfo.is_valid)) {
        /*
         * We try to look up the disc again if the info is not
         * valid.  The user might have configured a new server
         * in the meantime.
         */
        cdda_cdinfo_flush(&cdinfo);
        cached_id = disc_id;

        if (!cdda_cdinfo_read_file(disc_id, &cdinfo)) {
            if (cdda_cfg.use_cddb)
                cdda_cddb_get_info(toc, &cdinfo);
            if (cdinfo.is_valid)
                cdda_cdinfo_write_file(disc_id, &cdinfo);
        }
    }

    tuple = bmp_title_input_new();
    cdda_cdinfo_get(&cdinfo, track, &performer, &album_name, &track_name);
    G_UNLOCK(tuple);

    tuple->performer = g_strdup(performer);
    tuple->album_name = g_strdup(album_name);
    tuple->track_name = g_strdup(track_name);
    tuple->track_number = (track);
    tuple->file_name = g_strdup(tuple->file_path);
    tuple->file_path = g_strdup_printf(_("CD Audio Track %02u"), track);
    tuple->file_ext = "cda";
    tuple->length = ((cdda_calculate_track_length(toc, track) * 1000) / 75);

    if (!tuple->track_name)
        tuple->track_name = g_strdup_printf(_("CD Audio Track %02u"), track);

    tuple->genre = g_strdup(cdinfo.genre);
    tuple->year = cdinfo.year;

    return tuple;
}

static gchar *
get_song_title(TitleInput *tuple)
{
    return xmms_get_titlestring(cdda_cfg.title_override ?
                                cdda_cfg.name_format :
                                xmms_get_gentitle_format(), tuple);
}

static gboolean
stop_timeout(gpointer data)
{
    int fd;
    struct timeout *to = data;

    fd = open(to->device, CDOPENFLAGS);
    if (fd != -1) {
        ioctl(fd, XMMS_STOP, 0);
        close(fd);
    }
    timeout_destroy(to);
    return FALSE;
}

static void
stop(InputPlayback * playback)
{
    struct timeout *to_info;
    if (cdda_playing.fd < 0)
        return;

    cdda_playing.playing = FALSE;

    if (cdda_playing.drive.dae) {
        g_thread_join(dae_data.thread);
        playback->output->close_audio();
    }
    else
        ioctl(cdda_playing.fd, XMMS_PAUSE, 0);

    close(cdda_playing.fd);
    cdda_playing.fd = -1;

    if (!cdda_playing.drive.dae) {
        to_info = g_malloc(sizeof(*to_info));
        to_info->device = g_strdup(cdda_playing.drive.device);
        to_info->id = gtk_timeout_add(STOP_DELAY * 100, stop_timeout,
                                      to_info);
        timeout_list = g_list_prepend(timeout_list, to_info);
    }
}

static void
cdda_pause(InputPlayback *playback, short p)
{
    if (cdda_playing.drive.dae) {
        playback->output->pause(p);
        return;
    }
    if (p) {
        pause_time = get_time(playback);
        ioctl(cdda_playing.fd, XMMS_PAUSE, 0);
    }
    else {
        ioctl(cdda_playing.fd, XMMS_RESUME, 0);
        pause_time = -1;
    }
    is_paused = p;
}



static void
seek(InputPlayback *data, int time)
{
    struct cdda_msf *end, start;
    int track = cdda_playing.track;

//      g_message("** CD_AUDIO: seeking...");
    if (cdda_playing.drive.dae) {
        dae_data.seek = time;
        while (dae_data.seek != -1)
            xmms_usleep(20000);
        return;
    }

    start.minute = (cdda_playing.cd_toc.track[track].minute * 60 +
                    cdda_playing.cd_toc.track[track].second + time) / 60;
    start.second = (cdda_playing.cd_toc.track[track].second + time) % 60;
    start.frame = cdda_playing.cd_toc.track[track].frame;
    if (track == cdda_playing.cd_toc.last_track)
        end = &cdda_playing.cd_toc.leadout;
    else
        end = &cdda_playing.cd_toc.track[track + 1];

    play_ioctl(&start, end);

    if (is_paused) {
        cdda_pause(data, TRUE);
        pause_time = time * 1000;
    }
}

static int
get_time_analog(void)
{
    int frame, start_frame, length;
    int track = cdda_playing.track;

    if (is_paused && pause_time != -1)
        return pause_time;

    frame = get_current_frame();

    if (frame == -1)
        return -1;

    start_frame = LBA(cdda_playing.cd_toc.track[track]);
    length = cdda_calculate_track_length(&cdda_playing.cd_toc, track);

    if (frame - start_frame >= length - 20) /* 20 seems to work better */
        return -1;

    return ((frame - start_frame) * 1000) / 75;
}

static int
get_time_dae(InputPlayback *playback)
{
    if (dae_data.audio_error)
        return -2;
    if (!cdda_playing.playing ||
        (dae_data.eof && !playback->output->buffer_playing()))
        return -1;
    return playback->output->output_time();
}

static int
get_time(InputPlayback *playback)
{
    if (cdda_playing.fd == -1)
        return -1;

    if (cdda_playing.drive.dae)
        return get_time_dae(playback);
    else
        return get_time_analog();
}

static void
get_song_info(char *filename, char **title, int *len)
{
    cdda_disc_toc_t toc;
    int t;
    char *tmp;
    struct driveinfo *drive;
    TitleInput *tuple;

    *title = NULL;
    *len = -1;

//      g_message("** CD_AUDIO: getting song info");

    if ((drive = cdda_find_drive(filename)) == NULL)
        return;

    tmp = strrchr(filename, '/');
    if (tmp)
        tmp++;
    else
        tmp = filename;

    if (!sscanf(tmp, "Track %d.cda", &t))
        return;
    if (!cdda_get_toc(&toc, drive->device))
        return;
    if (t < toc.first_track || t > toc.last_track
        || toc.track[t].flags.data_track)
        return;

    if ((tuple = cdda_get_tuple(&toc, t)) != NULL) {
        *len = tuple->length;
        *title = get_song_title(tuple);
    }
    bmp_title_input_free(tuple);
}

static TitleInput *
get_song_tuple(char *filename)
{
    cdda_disc_toc_t toc;
    int t;
    char *tmp;
    struct driveinfo *drive;
    TitleInput *tuple = NULL;

//      g_message("** CD_AUDIO: getting song info");

    if ((drive = cdda_find_drive(filename)) == NULL)
        return tuple;

    tmp = strrchr(filename, '/');
    if (tmp)
        tmp++;
    else
        tmp = filename;

    if (!sscanf(tmp, "Track %d.cda", &t))
        return tuple;
    if (!cdda_get_toc(&toc, drive->device))
        return tuple;
    if (t < toc.first_track || t > toc.last_track
        || toc.track[t].flags.data_track)
        return tuple;

    tuple = cdda_get_tuple(&toc, t);
    return tuple;
}

#ifdef HAVE_OSS
static void
oss_get_volume(int *l, int *r, int mixer_line)
{
    int fd, v;

    fd = open(DEV_MIXER, O_RDONLY);
    if (fd != -1) {
        ioctl(fd, MIXER_READ(mixer_line), &v);
        *r = (v & 0xFF00) >> 8;
        *l = (v & 0x00FF);
        close(fd);
    }
}

static void
oss_set_volume(int l, int r, int mixer_line)
{
    int fd, v;

    fd = open(DEV_MIXER, O_RDONLY);
    if (fd != -1) {
        v = (r << 8) | l;
        ioctl(fd, MIXER_WRITE(mixer_line), &v);
        close(fd);
    }
}
#else
static void
oss_get_volume(int *l, int *r, int mixer_line)
{
}
static void
oss_set_volume(int l, int r, int mixer_line)
{
}
#endif


static gint
get_volume(int *l, int *r)
{
    if (cdda_playing.drive.dae)
        return 0;
    else if (cdda_playing.drive.mixer == CDDA_MIXER_OSS)
        oss_get_volume(l, r, cdda_playing.drive.oss_mixer);
    else if (cdda_playing.drive.mixer == CDDA_MIXER_DRIVE)
        drive_get_volume(l, r);
    return 1;
}

static gint
set_volume(int l, int r)
{
    if (cdda_playing.drive.dae)
        return 0;
    else if (cdda_playing.drive.mixer == CDDA_MIXER_OSS)
        oss_set_volume(l, r, cdda_playing.drive.oss_mixer);
    else if (cdda_playing.drive.mixer == CDDA_MIXER_DRIVE)
        drive_set_volume(l, r);
    return 1;
}