/*  XMMS - ALSA output plugin
 *  Copyright (C) 2001-2003 Matthieu Sozeau <mattam@altern.org>
 *  Copyright (C) 1998-2003  Peter Alm, Mikael Alm, Olle Hallnas,
 *                           Thomas Nilsson and 4Front Technologies
 *  Copyright (C) 1999-2004  Haavard Kvaalen
 *
 *  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.
 *
 *
 *  CHANGES
 *
 *  2005.01.05  Takashi Iwai <tiwai@suse.de>
 *      Impelemented the multi-threaded mode with an audio-thread.
 *      Many fixes and cleanups.
 */

#include "alsa.h"
#include <ctype.h>
#include <glib.h>
#include <libbeep/xconvert.h>

static snd_pcm_t *alsa_pcm = NULL;

static snd_output_t *logs = NULL;

static guint64 alsa_total_written = 0;        /* input bytes */
static guint64 alsa_hw_written    = 0;        /* output bytes */
static gint    output_time_offset = 0;

/* device buffer/period sizes in bytes */
static int hw_buffer_size, hw_period_size;              /* in output bytes */
static int hw_buffer_size_in, hw_period_size_in;        /* in input bytes */

/* Set/Get volume */
static snd_mixer_elem_t *pcm_element = NULL;
static snd_mixer_t *mixer = NULL;

static gboolean mmap, going = FALSE, paused, multi_thread, mixer_start = TRUE;;

static gboolean alsa_can_pause;

static guint mixer_timeout;

/* for audio thread */
static GThread *audio_thread;           /* audio loop thread */
static int thread_buffer_size;          /* size of intermediate buffer in bytes */
static char *thread_buffer;             /* audio intermediate buffer */
static int rd_index, wr_index;          /* current read/write position in int-buffer */
static gboolean pause_request;          /* pause status currently requested */
static gint flush_request;              /* flush status (time) currently requested */

struct snd_format {
    unsigned int rate;
    unsigned int channels;
    snd_pcm_format_t format;
    AFormat xmms_format;
    int sample_bits;
    int bps;
};

static struct snd_format *inputf = NULL;
static struct snd_format *effectf = NULL;
static struct snd_format *outputf = NULL;

static int alsa_setup(struct snd_format *f);
static void alsa_mmap_audio(char *data, int length);
static void alsa_write_audio(char *data, int length);

static struct snd_format *snd_format_from_xmms(AFormat fmt, int rate,
                                               int channels);

static struct xmms_convert_buffers *convertb;

static convert_func_t alsa_convert_func;
static convert_channel_func_t alsa_stereo_convert_func;
static convert_freq_func_t alsa_frequency_convert_func;

static const struct {
    AFormat xmms;
    snd_pcm_format_t alsa;
} format_table[] = {
    {FMT_S16_LE, SND_PCM_FORMAT_S16_LE},
    {FMT_S16_BE, SND_PCM_FORMAT_S16_BE},
    {FMT_S16_NE, SND_PCM_FORMAT_S16},
    {FMT_U16_LE, SND_PCM_FORMAT_U16_LE},
    {FMT_U16_BE, SND_PCM_FORMAT_U16_BE},
    {FMT_U16_NE, SND_PCM_FORMAT_U16},
    {FMT_U8, SND_PCM_FORMAT_U8},
    {FMT_S8, SND_PCM_FORMAT_S8},
};


static void
debug(char *str, ...)
     G_GNUC_PRINTF(1, 2);

     static void debug(char *str, ...)
{
    va_list args;

    if (alsa_cfg.debug) {
        va_start(args, str);
        g_logv(NULL, G_LOG_LEVEL_MESSAGE, str, args);
        va_end(args);
    }
}

/*
 * mixer stuff
 */
static void
parse_mixer_name(char *str, char **name, int *index)
{
    char *end;

    while (isspace(*str))
        str++;

    if ((end = strchr(str, ',')) != NULL) {
        *name = g_strndup(str, end - str);
        end++;
        *index = atoi(end);
    }
    else {
        *name = g_strdup(str);
        *index = 0;
    }
}

int
alsa_get_mixer(snd_mixer_t ** mixer, int card)
{
    char *dev;
    int err;

    debug("alsa_get_mixer");

    dev = g_strdup_printf("hw:%i", card);

    if ((err = snd_mixer_open(mixer, 0)) < 0) {
        g_warning("alsa_get_mixer(): Failed to open empty mixer: %s",
                  snd_strerror(-err));
        mixer = NULL;
        return -1;
    }
    if ((err = snd_mixer_attach(*mixer, dev)) < 0) {
        g_warning("alsa_get_mixer(): Attaching to mixer %s failed: %s",
                  dev, snd_strerror(-err));
        return -1;
    }
    if ((err = snd_mixer_selem_register(*mixer, NULL, NULL)) < 0) {
        g_warning("alsa_get_mixer(): Failed to register mixer: %s",
                  snd_strerror(-err));
        return -1;
    }
    if ((err = snd_mixer_load(*mixer)) < 0) {
        g_warning("alsa_get_mixer(): Failed to load mixer: %s",
                  snd_strerror(-err));
        return -1;
    }

    g_free(dev);

    return (*mixer != NULL);
}


static snd_mixer_elem_t* alsa_get_mixer_elem(snd_mixer_t *mixer, char *name, int index)
{
    snd_mixer_selem_id_t *selem_id;
    snd_mixer_elem_t *elem;
    snd_mixer_selem_id_alloca(&selem_id);

    if (index != -1)
        snd_mixer_selem_id_set_index(selem_id, index);
    if (name != NULL)
        snd_mixer_selem_id_set_name(selem_id, name);

    elem = snd_mixer_find_selem(mixer, selem_id);

    return elem;
}

static int
alsa_setup_mixer(void)
{
    char *name;
    long int a, b;
    long alsa_min_vol, alsa_max_vol;
    int err, index;

    debug("alsa_setup_mixer");

    if ((err = alsa_get_mixer(&mixer, alsa_cfg.mixer_card)) < 0)
        return err;

    parse_mixer_name(alsa_cfg.mixer_device, &name, &index);

    pcm_element = alsa_get_mixer_elem(mixer, name, index);

    g_free(name);

    if (!pcm_element) {
        g_warning("alsa_setup_mixer(): Failed to find mixer element: %s",
                  alsa_cfg.mixer_device);
        return -1;
    }

    /*
     * Work around a bug in alsa-lib up to 1.0.0rc2 where the
     * new range don't take effect until the volume is changed.
     * This hack should be removed once we depend on Alsa 1.0.0.
     */
    snd_mixer_selem_get_playback_volume(pcm_element,
                                        SND_MIXER_SCHN_FRONT_LEFT, &a);
    snd_mixer_selem_get_playback_volume(pcm_element,
                                        SND_MIXER_SCHN_FRONT_RIGHT, &b);

    snd_mixer_selem_get_playback_volume_range(pcm_element,
                                              &alsa_min_vol, &alsa_max_vol);
    snd_mixer_selem_set_playback_volume_range(pcm_element, 0, 100);

    if (alsa_max_vol == 0) {
        pcm_element = NULL;
        return -1;
    }

    if (!alsa_cfg.soft_volume)
        alsa_set_volume(a * 100 / alsa_max_vol, b * 100 / alsa_max_vol);

    debug("alsa_setup_mixer: end");

    return 0;
}

static int
alsa_mixer_timeout(void *data)
{
    if (mixer) {
        snd_mixer_close(mixer);
        mixer = NULL;
        pcm_element = NULL;
    }
    mixer_timeout = 0;
    mixer_start = TRUE;

    g_message("alsa mixer timed out");
    return FALSE;
}


static void alsa_cleanup_mixer(void)
{
    pcm_element = NULL;
    if (mixer) {
        snd_mixer_close(mixer);
        mixer = NULL;
    }
}


void
alsa_get_volume(int *l, int *r)
{
    long ll = *l, lr = *r;

    if (mixer_start) {
        alsa_setup_mixer();
        mixer_start = FALSE;
    }

    if (!pcm_element)
        return;

    snd_mixer_handle_events(mixer);

    if (alsa_cfg.soft_volume) {
        *l = alsa_cfg.vol.left;
        *r = alsa_cfg.vol.right;
    }
    else {
        snd_mixer_selem_get_playback_volume(pcm_element,
                                            SND_MIXER_SCHN_FRONT_LEFT, &ll);
        snd_mixer_selem_get_playback_volume(pcm_element,
                                            SND_MIXER_SCHN_FRONT_RIGHT, &lr);
        *l = ll;
        *r = lr;
    }
    if (mixer_timeout)
        gtk_timeout_remove(mixer_timeout);
    mixer_timeout = gtk_timeout_add(5000, alsa_mixer_timeout, NULL);
}


void
alsa_set_volume(int l, int r)
{
    if (!pcm_element)
        return;

    if (alsa_cfg.soft_volume) {
        alsa_cfg.vol.left = l;
        alsa_cfg.vol.right = r;
    }
    else {
        snd_mixer_selem_set_playback_volume(pcm_element,
                                            SND_MIXER_SCHN_FRONT_LEFT, l);
        snd_mixer_selem_set_playback_volume(pcm_element,
                                            SND_MIXER_SCHN_FRONT_RIGHT, r);
    }
}

/*
 * audio stuff
 */

int alsa_playing(void)
{
    if (!going || paused || alsa_pcm == NULL)
        return FALSE;

    return(snd_pcm_state(alsa_pcm) == SND_PCM_STATE_RUNNING);
}


/* handle generic errors */
static int alsa_handle_error(int err)
{
    switch (err) {
    case -EPIPE: /* XRUN */
        if (alsa_cfg.debug) {
            snd_pcm_status_t *alsa_status;
            snd_pcm_status_alloca(&alsa_status);
            if (snd_pcm_status(alsa_pcm, alsa_status) < 0)
                g_warning("xrun_recover(): snd_pcm_status() failed");
            else {
                printf("Status:\n");
                snd_pcm_status_dump(alsa_status, logs);
            }
        }
        return snd_pcm_prepare(alsa_pcm);

    case -ESTRPIPE: /* suspend */
        while ((err = snd_pcm_resume(alsa_pcm)) == -EAGAIN)
            sleep(1);   /* wait until suspend flag is released */
        if (err < 0) {
            g_warning("suspend_recover(): snd_pcm_resume() failed.");
            return snd_pcm_prepare(alsa_pcm);
        }
        break;
    }

    return err;
}

/* update and get the available space on h/w buffer (in frames) */
static snd_pcm_sframes_t alsa_get_avail(void)
{
    snd_pcm_sframes_t ret;

    if (alsa_pcm == NULL)
        return 0;

    while ((ret = snd_pcm_avail_update(alsa_pcm)) < 0) {
        ret = alsa_handle_error(ret);
        if (ret < 0) {
            g_warning("alsa_get_avail(): snd_pcm_avail_update() failed: %s",
                      snd_strerror(-ret));
            return 0;
        }
    }
    return ret;
}

/* do pause operation */
static void alsa_do_pause(gboolean p)
{
    if (paused == p)
        return;

    if (alsa_pcm) {
        if (alsa_can_pause) {
            snd_pcm_pause(alsa_pcm, p);
        } else if (p) {
            snd_pcm_drop(alsa_pcm);
            snd_pcm_prepare(alsa_pcm);
        }
    }
    paused = p;
}

void alsa_pause(short p)
{
    debug("alsa_pause");
    if (multi_thread)
        pause_request = p;
    else
        alsa_do_pause(p);
}

/* close PCM and release associated resources */
static void alsa_close_pcm(void)
{
    if (alsa_pcm) {
        int err;
        snd_pcm_drop(alsa_pcm);
        if ((err = snd_pcm_close(alsa_pcm)) < 0)
            g_warning("alsa_pcm_close() failed: %s",
                      snd_strerror(-err));
        alsa_pcm = NULL;
    }
}

/* reopen ALSA PCM */
static int alsa_reopen(struct snd_format *f)
{
    /* remember the current position */
    output_time_offset += (alsa_hw_written * 1000) / outputf->bps;
    alsa_hw_written = 0;

    alsa_close_pcm();

    return alsa_setup(f);
}

/* do flush (drop) operation */
static void alsa_do_flush(int time)
{
    if (alsa_pcm) {
        snd_pcm_drop(alsa_pcm);
        snd_pcm_prepare(alsa_pcm);
    }
    /* correct the offset */
    output_time_offset = time;
    alsa_total_written = (guint64) time * inputf->bps / 1000;
    rd_index = wr_index = alsa_hw_written = 0;
}

void alsa_flush(int time)
{
    if (multi_thread) {
        flush_request = time;
        while (flush_request != -1)
            xmms_usleep(10000);
    } else
        alsa_do_flush(time);
}

void alsa_close(void)
{
    if (! going)
        return;

    debug("Closing device");

    going = 0;

    if (multi_thread)
        g_thread_join(audio_thread);
    else
        alsa_close_pcm();

    alsa_cleanup_mixer();

    xmms_convert_buffers_destroy(convertb);
    convertb = NULL;
    g_free(inputf);
    inputf = NULL;
    g_free(effectf);
    effectf = NULL;
    g_free(outputf);
    outputf = NULL;

    alsa_save_config();

    if (alsa_cfg.debug)
        snd_output_close(logs);
    debug("Device closed");
}

/* return the size of audio data filled in the audio thread buffer */
static int get_thread_buffer_filled(void)
{
    if (wr_index >= rd_index)
        return wr_index - rd_index;
    return thread_buffer_size - (rd_index - wr_index);
}

/* get the free space on buffer */
int alsa_free(void)
{
    int result = 0;
    if (multi_thread)
        result = thread_buffer_size - get_thread_buffer_filled() - 1;
    else if (! paused && alsa_pcm)
        result = snd_pcm_frames_to_bytes(alsa_pcm, alsa_get_avail());
    return result;
}


int
alsa_get_output_time(void)
{
    snd_pcm_sframes_t delay;
    guint64 bytes = 0;

    if (!going || alsa_pcm == NULL)
        return 0;

    if (!snd_pcm_delay(alsa_pcm, &delay)) {
        bytes = snd_pcm_frames_to_bytes(alsa_pcm, delay);
        if (alsa_hw_written < bytes)
            bytes = 0;
        else
            bytes = alsa_hw_written - bytes;
    }
    return output_time_offset + (bytes * 1000) / outputf->bps;
}

int
alsa_get_written_time(void)
{
    if (!going)
        return 0;
    return (alsa_total_written * 1000) / inputf->bps;
}

#define STEREO_ADJUST(type, type2, endian)                                      \
do {                                                                            \
        type *ptr = data;                                                       \
        for (i = 0; i < length; i += 4)                                         \
        {                                                                       \
                *ptr = type2##_TO_##endian(type2##_FROM_## endian(*ptr) *       \
                                           alsa_cfg.vol.left / 100);            \
                ptr++;                                                          \
                *ptr = type2##_TO_##endian(type2##_FROM_##endian(*ptr) *        \
                                           alsa_cfg.vol.right / 100);           \
                ptr++;                                                          \
        }                                                                       \
} while (0)

#define MONO_ADJUST(type, type2, endian)                                        \
do {                                                                            \
        type *ptr = data;                                                       \
        for (i = 0; i < length; i += 4)                                         \
        {                                                                       \
                *ptr = type2##_TO_##endian(type2##_FROM_## endian(*ptr) *       \
                                           vol / 100);                          \
                ptr++;                                                          \
        }                                                                       \
} while (0)

#define VOLUME_ADJUST(type, type2, endian)              \
do {                                                    \
        if (channels == 2)                              \
                STEREO_ADJUST(type, type2, endian);     \
        else                                            \
                MONO_ADJUST(type, type2, endian);       \
} while (0)

#define STEREO_ADJUST8(type)                            \
do {                                                    \
        type *ptr = data;                               \
        for (i = 0; i < length; i += 2)                 \
        {                                               \
                *ptr = *ptr * alsa_cfg.vol.left / 100;  \
                ptr++;                                  \
                *ptr = *ptr * alsa_cfg.vol.right / 100; \
                ptr++;                                  \
        }                                               \
} while (0)

#define MONO_ADJUST8(type)                      \
do {                                            \
        type *ptr = data;                       \
        for (i = 0; i < length; i += 4)         \
        {                                       \
                *ptr = *ptr * vol / 100;        \
                ptr++;                          \
        }                                       \
} while (0)

#define VOLUME_ADJUST8(type)                    \
do {                                            \
        if (channels == 2)                      \
                STEREO_ADJUST8(type);           \
        else                                    \
                MONO_ADJUST8(type);             \
} while (0)


static void
volume_adjust(void *data, int length, AFormat fmt, int channels)
{
    int i, vol;

    if ((alsa_cfg.vol.left == 100 && alsa_cfg.vol.right == 100) ||
        (channels == 1 &&
         (alsa_cfg.vol.left == 100 || alsa_cfg.vol.right == 100)))
        return;

    vol = MAX(alsa_cfg.vol.left, alsa_cfg.vol.right);

    switch (fmt) {
    case FMT_S16_LE:
        VOLUME_ADJUST(gint16, GINT16, LE);
        break;
    case FMT_U16_LE:
        VOLUME_ADJUST(guint16, GUINT16, LE);
        break;
    case FMT_S16_BE:
        VOLUME_ADJUST(gint16, GINT16, BE);
        break;
    case FMT_U16_BE:
        VOLUME_ADJUST(guint16, GUINT16, BE);
        break;
    case FMT_S8:
        VOLUME_ADJUST8(gint8);
        break;
    case FMT_U8:
        VOLUME_ADJUST8(guint8);
        break;
    default:
        g_warning("volume_adjust(): unhandled format: %d", fmt);
        break;
    }
}


/* transfer data to audio h/w; length is given in bytes
 *
 * data can be modified via effect plugin, rate conversion or
 * software volume before passed to audio h/w
 */
static void alsa_do_write(gpointer data, int length)
{
    EffectPlugin *ep = NULL;
    int new_freq;
    int new_chn;
    AFormat f;

    if (paused)
        return;

    new_freq = inputf->rate;
    new_chn = inputf->channels;
    f = inputf->xmms_format;

    if (effects_enabled() && (ep = get_current_effect_plugin()) &&
        ep->query_format)
        ep->query_format(&f, &new_freq, &new_chn);

    if (f != effectf->xmms_format || new_freq != effectf->rate ||
        new_chn != effectf->channels) {
        debug("Changing audio format for effect plugin");
        g_free(effectf);
        effectf = snd_format_from_xmms(f, new_freq, new_chn);
        if (alsa_reopen(effectf) < 0) {
            /* fatal error... */
            alsa_close();
            return;
        }
    }

    if (ep) {
        length = ep->mod_samples(&data, length,
                                 inputf->xmms_format,
                                 inputf->rate, inputf->channels);
    }

    if (alsa_convert_func != NULL)
        length = alsa_convert_func(convertb, &data, length);
    if (alsa_stereo_convert_func != NULL)
        length = alsa_stereo_convert_func(convertb, &data, length);
    if (alsa_frequency_convert_func != NULL)
        length = alsa_frequency_convert_func(convertb, &data, length,
                                             effectf->rate, outputf->rate);

    if (alsa_cfg.soft_volume)
        volume_adjust(data, length, outputf->xmms_format, outputf->channels);

    if (mmap)
        alsa_mmap_audio(data, length);
    else
        alsa_write_audio(data, length);
}

/* write callback */
void alsa_write(gpointer data, int length)
{
    if (multi_thread) {
        int cnt;
        char *src = (char *)data;

        alsa_total_written += length;
        while (length > 0) {
            int wr;
            cnt = MIN(length, thread_buffer_size - wr_index);
            memcpy(thread_buffer + wr_index, src, cnt);
            wr = (wr_index + cnt) % thread_buffer_size;
            wr_index = wr;
            length -= cnt;
            src += cnt;
        }
    } else {
        alsa_do_write(data, length);
        alsa_total_written += length;
    }
}

/* transfer data to audio h/w via normal write */
static void alsa_write_audio(char *data, int length)
{
    snd_pcm_sframes_t written_frames;

    while (length > 0) {
        int frames = snd_pcm_bytes_to_frames(alsa_pcm, length);
        written_frames = snd_pcm_writei(alsa_pcm, data, frames);

        if (written_frames > 0) {
            int written = snd_pcm_frames_to_bytes(alsa_pcm,
                                                  written_frames);
            length -= written;
            data += written;
            alsa_hw_written += written;
        }
        else {
            int err = alsa_handle_error((int)written_frames);
            if (err < 0) {
                g_warning("alsa_write_audio(): write error: %s",
                          snd_strerror(-err));
                break;
            }
        }
    }
}

/* transfer data to audio h/w via mmap
 *
 * basically, it makes sense only in the single thread mode.
 * also, don't expect too much efficiency over mmap...
 */
static void
alsa_mmap_audio(char *data, int length)
{
    int cnt, err;
    snd_pcm_uframes_t offset, frames;
    const snd_pcm_channel_area_t *chan_areas;
    snd_pcm_channel_area_t src_area;
    int ch, channels, sample_bits;

    if (snd_pcm_state(alsa_pcm) == SND_PCM_STATE_XRUN)
        alsa_handle_error(-EPIPE);

    /* need to call this before snd_pcm_mmap_begin() */
    alsa_get_avail();

    channels = outputf->channels;
    sample_bits = outputf->sample_bits;
    while (length > 0) {
        frames = snd_pcm_bytes_to_frames(alsa_pcm, length);
        if ((err = snd_pcm_mmap_begin(alsa_pcm, &chan_areas, &offset, &frames) < 0)) {
            g_warning("alsa_mmap_audio(): snd_pcm_mmap_begin() " "failed: %s",
                      snd_strerror(-err));
            break;
        }

        cnt = snd_pcm_frames_to_bytes(alsa_pcm, frames);

        src_area.addr = data;
        src_area.first = 0;
        src_area.step = channels * sample_bits;
        for (ch = 0; ch < channels; ch++) {
            snd_pcm_area_copy(&chan_areas[ch], offset,
                              &src_area, 0, frames, outputf->format);
            src_area.first += sample_bits;
        }

        err = snd_pcm_mmap_commit(alsa_pcm, offset, frames);
        if (err < 0) {
            err = alsa_handle_error(err);
            if (err < 0)
                g_warning("alsa_mmap_audio(): snd_pcm_mmap_commit() "
                          "failed: %s", snd_strerror(-err));
        }
        else {
            if (err != frames)
                g_warning("alsa_mmap_audio(): snd_pcm_mmap_commit "
                          "returned %d, expected %d", err, (int)frames);
            data += cnt;
            length -= cnt;
            alsa_hw_written += cnt;
        }
    }

    /* PCM isn't started automatically in the case of mmap mode, so
     * we need to trigger manually
     */
    if (snd_pcm_state(alsa_pcm) == SND_PCM_STATE_PREPARED) {
        if (alsa_hw_written >= hw_period_size)
            snd_pcm_start(alsa_pcm);
    }
}

/* transfer audio data from thread buffer to h/w */
static void alsa_write_out_thread_data(void)
{
    gint length, cnt, avail;
    int err;

    length = MIN(hw_period_size_in, get_thread_buffer_filled());
    avail = snd_pcm_frames_to_bytes(alsa_pcm, alsa_get_avail());
    length = MIN(length, avail);
    while (length > 0) {
        int rd;
        cnt = MIN(length, thread_buffer_size - rd_index);
        alsa_do_write(thread_buffer + rd_index, cnt);
        rd = (rd_index + cnt) % thread_buffer_size;
        rd_index = rd;
        length -= cnt;

        if (length > 0 && snd_pcm_state(alsa_pcm) == SND_PCM_STATE_PREPARED) {
            if ((err = snd_pcm_start(alsa_pcm)) < 0)
                g_warning("alsa_mmap_audio(): snd_pcm_start() "
                          "failed: %s", snd_strerror(-err));
        }
    }
}

/* audio thread loop */
/* FIXME: proper lock? */
static void *alsa_loop(void *arg)
{
    int npfds = snd_pcm_poll_descriptors_count(alsa_pcm);
    struct pollfd *pfds;
    unsigned short *revents;

    if (npfds <= 0)
        goto _error;
    pfds = alloca(sizeof(*pfds) * npfds);
    revents = alloca(sizeof(*revents) * npfds);
    while (going && alsa_pcm) {
        if (! paused && get_thread_buffer_filled() > hw_period_size_in) {
            snd_pcm_poll_descriptors(alsa_pcm, pfds, npfds);
            if (poll(pfds, npfds, 10) > 0) { 
                /* need to check revents.  poll() with dmix returns
                 * a postive value even if no data is available
                 */
                int i;
                snd_pcm_poll_descriptors_revents(alsa_pcm, pfds, npfds, revents);
                for (i = 0; i < npfds; i++)
                    if (revents[i] & POLLOUT) {
                        alsa_write_out_thread_data();
                        break;
                    }
            }
        } else
            xmms_usleep(10000);

        if (pause_request != paused)
            alsa_do_pause(pause_request);

        if (flush_request != -1) {
            alsa_do_flush(flush_request);
            flush_request = -1;
        }
    }

    _error:
    alsa_close_pcm();
    g_free(thread_buffer);
    thread_buffer = NULL;

    g_thread_exit(NULL);

    /* shut GCC up */
    return NULL;
}

/* open callback */
int
alsa_open(AFormat fmt, int rate, int nch)
{
    debug("Opening device");
    inputf = snd_format_from_xmms(fmt, rate, nch);
    effectf = snd_format_from_xmms(fmt, rate, nch);

    if (alsa_cfg.debug)
        snd_output_stdio_attach(&logs, stdout, 0);

    mmap = alsa_cfg.mmap;

    if (alsa_setup(inputf) < 0) {
        alsa_close();
        return 0;
    }

    if (!mixer)
        alsa_setup_mixer();

    convertb = xmms_convert_buffers_new();

    output_time_offset = 0;
    alsa_total_written = alsa_hw_written = 0;
    going = TRUE;
    paused = FALSE;

    multi_thread = alsa_cfg.multi_thread;
    debug("ALSA: multi_thread = %d\n", multi_thread);

    if (multi_thread) {
        thread_buffer_size = (guint64)alsa_cfg.thread_buffer_time * inputf->bps / 1000;
        if (thread_buffer_size < hw_buffer_size)
            thread_buffer_size = hw_buffer_size * 2;
        if (thread_buffer_size < 8192)
            thread_buffer_size = 8192;
        thread_buffer_size += hw_buffer_size;
        thread_buffer_size -= thread_buffer_size % hw_period_size;
        thread_buffer = g_malloc0(thread_buffer_size);
        wr_index = rd_index = 0;
        pause_request = FALSE;
        flush_request = -1;

        audio_thread = g_thread_create(alsa_loop, NULL, TRUE, NULL);
    }

    return 1;
}

static struct snd_format *
snd_format_from_xmms(AFormat fmt, int rate, int channels)
{
    struct snd_format *f = g_malloc(sizeof(struct snd_format));
    int i;

    f->xmms_format = fmt;
    f->format = SND_PCM_FORMAT_UNKNOWN;

    for (i = 0; i < sizeof(format_table) / sizeof(format_table[0]); i++)
        if (format_table[i].xmms == fmt) {
            f->format = format_table[i].alsa;
            break;
        }

    /* Get rid of _NE */
    for (i = 0; i < sizeof(format_table) / sizeof(format_table[0]); i++)
        if (format_table[i].alsa == f->format) {
            f->xmms_format = format_table[i].xmms;
            break;
        }


    f->rate = rate;
    f->channels = channels;
    f->sample_bits = snd_pcm_format_physical_width(f->format);
    f->bps = (rate * f->sample_bits * channels) >> 3;

    return f;
}

static int
format_from_alsa(snd_pcm_format_t fmt)
{
    int i;
    for (i = 0; i < sizeof(format_table) / sizeof(format_table[0]); i++)
        if (format_table[i].alsa == fmt)
            return format_table[i].xmms;
    g_warning("Unsupported format: %s", snd_pcm_format_name(fmt));
    return -1;
}

static int
alsa_setup(struct snd_format *f)
{
    int err;
    snd_pcm_hw_params_t *hwparams;
    snd_pcm_sw_params_t *swparams;
    int alsa_buffer_time;
    unsigned int alsa_period_time;
    snd_pcm_uframes_t alsa_buffer_size, alsa_period_size;

    debug("alsa_setup");

    alsa_convert_func = NULL;
    alsa_stereo_convert_func = NULL;
    alsa_frequency_convert_func = NULL;

    g_free(outputf);
    outputf = snd_format_from_xmms(f->xmms_format, f->rate, f->channels);

    debug("Opening device: %s", alsa_cfg.pcm_device);
    /* FIXME: Can snd_pcm_open() return EAGAIN? */
    if ((err = snd_pcm_open(&alsa_pcm, alsa_cfg.pcm_device,
                            SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK)) < 0) {
        g_warning("alsa_setup(): Failed to open pcm device (%s): %s",
                  alsa_cfg.pcm_device, snd_strerror(-err));
        alsa_pcm = NULL;
        g_free(outputf);
        outputf = NULL;
        return -1;
    }

    /* doesn't care about non-blocking */
    /* snd_pcm_nonblock(alsa_pcm, 0); */

    if (alsa_cfg.debug) {
        snd_pcm_info_t *info;
        int alsa_card, alsa_device, alsa_subdevice;

        snd_pcm_info_alloca(&info);
        snd_pcm_info(alsa_pcm, info);
        alsa_card = snd_pcm_info_get_card(info);
        alsa_device = snd_pcm_info_get_device(info);
        alsa_subdevice = snd_pcm_info_get_subdevice(info);
        printf("Card %i, Device %i, Subdevice %i\n",
               alsa_card, alsa_device, alsa_subdevice);
    }

    snd_pcm_hw_params_alloca(&hwparams);

    if ((err = snd_pcm_hw_params_any(alsa_pcm, hwparams)) < 0) {
        g_warning("alsa_setup(): No configuration available for "
                  "playback: %s", snd_strerror(-err));
        return -1;
    }

    if (mmap &&
        (err = snd_pcm_hw_params_set_access(alsa_pcm, hwparams,
                                            SND_PCM_ACCESS_MMAP_INTERLEAVED))
        < 0) {
        g_message("alsa_setup(): Cannot set mmap'ed mode: %s. "
                  "falling back to direct write", snd_strerror(-err));
        mmap = 0;
    }

    if (!mmap &&
        (err = snd_pcm_hw_params_set_access(alsa_pcm, hwparams,
                                            SND_PCM_ACCESS_RW_INTERLEAVED)) <
        0) {
        g_warning("alsa_setup(): Cannot set direct write mode: %s",
                  snd_strerror(-err));
        return -1;
    }

    if ((err =
         snd_pcm_hw_params_set_format(alsa_pcm, hwparams,
                                      outputf->format)) < 0) {
        /*
         * Try if one of these format work (one of them should work
         * on almost all soundcards)
         */
        snd_pcm_format_t formats[] = { SND_PCM_FORMAT_S16_LE,
                                       SND_PCM_FORMAT_S16_BE,
                                       SND_PCM_FORMAT_U8
        };
        int i;

        for (i = 0; i < sizeof(formats) / sizeof(formats[0]); i++) {
            if (snd_pcm_hw_params_set_format(alsa_pcm, hwparams,
                                             formats[i]) == 0) {
                outputf->format = formats[i];
                break;
            }
        }
        if (outputf->format != f->format) {
            outputf->xmms_format = format_from_alsa(outputf->format);
            debug("Converting format from %d to %d",
                  f->xmms_format, outputf->xmms_format);
            if (outputf->xmms_format < 0)
                return -1;
            alsa_convert_func =
                xmms_convert_get_func(outputf->xmms_format,
                                      f->xmms_format);
            if (alsa_convert_func == NULL)
                return -1;
        }
        else {
            g_warning("alsa_setup(): Sample format not "
                      "available for playback: %s", snd_strerror(-err));
            return -1;
        }
    }

    snd_pcm_hw_params_set_channels_near(alsa_pcm, hwparams,
                                        &outputf->channels);
    if (outputf->channels != f->channels) {
        debug("Converting channels from %d to %d",
              f->channels, outputf->channels);
        alsa_stereo_convert_func =
            xmms_convert_get_channel_func(outputf->xmms_format,
                                          outputf->channels,
                                          f->channels);
        if (alsa_stereo_convert_func == NULL)
            return -1;
    }

    snd_pcm_hw_params_set_rate_near(alsa_pcm, hwparams, &outputf->rate, 0);
    if (outputf->rate == 0) {
        g_warning("alsa_setup(): No usable samplerate available.");
        return -1;
    }
    if (outputf->rate != f->rate) {
        debug("Converting samplerate from %d to %d",
              f->rate, outputf->rate);
        alsa_frequency_convert_func =
            xmms_convert_get_frequency_func(outputf->xmms_format,
                                            outputf->channels);
        if (alsa_frequency_convert_func == NULL)
            return -1;
    }

    outputf->sample_bits = snd_pcm_format_physical_width(outputf->format);
    outputf->bps = (outputf->rate * outputf->sample_bits * outputf->channels) >> 3;

    alsa_buffer_time = alsa_cfg.buffer_time * 1000;
    if ((err = snd_pcm_hw_params_set_buffer_time_near(alsa_pcm, hwparams,
                                                      &alsa_buffer_time,
                                                      0)) < 0) {
        g_warning("alsa_setup(): Set buffer time failed: %s.",
                  snd_strerror(-err));
        return -1;
    }

    alsa_period_time = alsa_cfg.period_time * 1000;
    if ((err = snd_pcm_hw_params_set_period_time_near(alsa_pcm, hwparams,
                                                      &alsa_period_time,
                                                      0)) < 0) {
        g_warning("alsa_setup(): Set period time failed: %s.",
                  snd_strerror(-err));
        return -1;
    }

    if (snd_pcm_hw_params(alsa_pcm, hwparams) < 0) {
        if (alsa_cfg.debug)
            snd_pcm_hw_params_dump(hwparams, logs);
        g_warning("alsa_setup(): Unable to install hw params");
        return -1;
    }

    if ((err =
         snd_pcm_hw_params_get_buffer_size(hwparams,
                                           &alsa_buffer_size)) < 0) {
        g_warning("alsa_setup(): snd_pcm_hw_params_get_buffer_size() "
                  "failed: %s", snd_strerror(-err));
        return -1;
    }

    if ((err =
         snd_pcm_hw_params_get_period_size(hwparams, &alsa_period_size,
                                           0)) < 0) {
        g_warning("alsa_setup(): snd_pcm_hw_params_get_period_size() "
                  "failed: %s", snd_strerror(-err));
        return -1;
    }

    alsa_can_pause = snd_pcm_hw_params_can_pause(hwparams);

    snd_pcm_sw_params_alloca(&swparams);
    snd_pcm_sw_params_current(alsa_pcm, swparams);

    /* This has effect for non-mmap only */
    if ((err = snd_pcm_sw_params_set_start_threshold(alsa_pcm,
                                                     swparams,
                                                     alsa_buffer_size -
                                                     alsa_period_size) < 0))
        g_warning("alsa_setup(): setting start " "threshold failed: %s",
                  snd_strerror(-err));
    if (snd_pcm_sw_params(alsa_pcm, swparams) < 0) {
        g_warning("alsa_setup(): Unable to install sw params");
        return -1;
    }

    if (alsa_cfg.debug) {
        snd_pcm_sw_params_dump(swparams, logs);
        snd_pcm_dump(alsa_pcm, logs);
    }

    hw_buffer_size = snd_pcm_frames_to_bytes(alsa_pcm, alsa_buffer_size);
    hw_period_size = snd_pcm_frames_to_bytes(alsa_pcm, alsa_period_size);
    if (inputf->bps != outputf->bps) {
        hw_buffer_size_in = ((guint64)hw_buffer_size * inputf->bps +
                             outputf->bps/2) / outputf->bps;
        hw_period_size_in = ((guint64)hw_period_size * inputf->bps +
                             outputf->bps/2) / outputf->bps;
    } else {
        hw_buffer_size_in = hw_buffer_size;
        hw_period_size_in = hw_period_size;
    }

    debug("Device setup: buffer time: %i, size: %i.", alsa_buffer_time,
          hw_buffer_size);
    debug("Device setup: period time: %i, size: %i.", alsa_period_time,
          hw_period_size);
    debug("bits per sample: %i; frame size: %i; Bps: %i",
          snd_pcm_format_physical_width(outputf->format),
          snd_pcm_frames_to_bytes(alsa_pcm, 1), outputf->bps);

    return 0;
}
