/*
 * import_alsa.c -- module for importing audio through ALSA
 * (C) 2008-2010 - Francesco Romani <fromani at gmail dot com>
 *
 * This file is part of transcode, a video stream processing tool.
 *
 * transcode 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.
 *
 * transcode 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, see <http://www.gnu.org/licenses/>.
 */



#include "transcode.h"
#include "libtc/optstr.h"

#include "libtc/tcmodule-plugin.h"

#include "config.h"

#include <alsa/asoundlib.h>
#ifdef HAVE_GETTIMEOFDAY
# include <sys/time.h>
# include <time.h>
#endif
#include <string.h>

/*%*
 *%* DESCRIPTION 
 *%*   This module reads audio samples from an ALSA device using libalsa.
 *%*
 *%* BUILD-DEPENDS
 *%*   alsa-lib >= 1.0.0
 *%*
 *%* #DEPENDS
 *%*
 *%* PROCESSING
 *%*   import/demuxer
 *%*
 *%* MEDIA
 *%*   audio
 *%*
 *%* #INPUT
 *%*
 *%* OUTPUT
 *%*   PCM*
 *%*
 *%* OPTION
 *%*   device (string)
 *%*     selects ALSA device to use for capturing audio.
 *%*/

#define LEGACY 1

#ifdef LEGACY
# define MOD_NAME    "import_alsa.so"
#else
# define MOD_NAME    "demultiplex_alsa.so"
#endif

#define MOD_VERSION "v0.0.5 (2007-05-12)"
#define MOD_CAP     "capture audio using ALSA"

#define MOD_FEATURES \
    TC_MODULE_FEATURE_DEMULTIPLEX|TC_MODULE_FEATURE_AUDIO
#define MOD_FLAGS \
    TC_MODULE_FLAG_RECONFIGURABLE

static const char tc_alsa_help[] = ""
    "Overview:\n"
    "    This module reads audio samples from an ALSA device using libalsa.\n"
    "Options:\n"
    "    device=dev  selects ALSA device to use\n"
    "    help        produce module overview and options explanations\n";


/*
 * TODO:
 * - device naming fix (this will likely require some core changes)
 * - probing/integration with core
 * - suspend recovery?
 * - smarter resync?
 */

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

typedef struct tcalsasource_ TCALSASource;
struct tcalsasource_ {
    snd_pcm_t *pcm;

    int rate;
    int channels;
    int precision;
};


/*************************************************************************/
/* some support functions shamelessly borrowed^Hinspired from alsa-utils */
/*************************************************************************/

#ifdef HAVE_GETTIMEOFDAY

#define TIMERSUB(a, b, result) do { \
    (result)->tv_sec  = (a)->tv_sec  - (b)->tv_sec; \
    (result)->tv_usec = (a)->tv_usec - (b)->tv_usec; \
    if ((result)->tv_usec < 0) { \
        --(result)->tv_sec; \
        (result)->tv_usec += 1000000; \
    } \
} while (0)

#endif

#define ALSA_PREPARE(HANDLE) do { \
    int ret = snd_pcm_prepare((HANDLE)->pcm); \
    if (ret < 0) { \
        tc_log_error(MOD_NAME, "ALSA prepare error: %s", snd_strerror(ret)); \
        return TC_ERROR; \
    } \
} while (0)

/* I/O error handler */
static int alsa_source_xrun(TCALSASource *handle)
{
    snd_pcm_status_t *status = NULL;
    snd_pcm_state_t state = 0;
    int ret = 0;

    TC_MODULE_SELF_CHECK(handle, "alsa_source_xrun");
    
    snd_pcm_status_alloca(&status);
    ret = snd_pcm_status(handle->pcm, status);
    if (ret < 0) {
        tc_log_error(__FILE__, "error while fetching status: %s",
                     snd_strerror(ret));
        return TC_ERROR;
    }

    state = snd_pcm_status_get_state(status);

    if (state == SND_PCM_STATE_XRUN) {
#ifdef HAVE_GETTIMEOFDAY
        struct timeval now, diff, tstamp;

        gettimeofday(&now, NULL);
        snd_pcm_status_get_trigger_tstamp(status, &tstamp);
        TIMERSUB(&now, &tstamp, &diff);

        tc_log_warn(__FILE__, "overrun at least %.3f ms long",
                    diff.tv_sec * 1000 + diff.tv_usec / 1000.0);
#else /* ! HAVE_GETTIMEOFDAY */
        tc_log_warn(__FILE__, "overrun");
#endif /* HAVE_GETTIMEOFDAY */
        ALSA_PREPARE(handle);
    } else if (state == SND_PCM_STATE_DRAINING) {
        tc_log_warn(__FILE__, "capture stream format change? attempting recover...");
        ALSA_PREPARE(handle);
    } else { /* catch all */
        tc_log_error(__FILE__, "read error, state = %s", snd_pcm_state_name(state));
        return TC_ERROR;
    }
    return TC_OK;
}


#define RETURN_IF_ALSA_FAIL(RET, MSG) do { \
    if ((RET) < 0) { \
        tc_log_error(__FILE__, "%s (%s)", (MSG), snd_strerror((RET))); \
        return TC_ERROR; \
    } \
} while (0) 

static int tc_alsa_source_open(TCALSASource *handle, const char *dev,
                               int rate, int precision, int channels)
{
    int ret = 0, alsa_rate = rate;
    snd_pcm_hw_params_t *hwparams = NULL;

    TC_MODULE_SELF_CHECK(handle, "alsa_source_open");

    /* some basic sanity checks */
    if (!strcmp(dev, "/dev/null") || !strcmp(dev, "/dev/zero")) {
        return TC_OK;
    }

    if (!dev || !strlen(dev)) {
        tc_log_warn(__FILE__, "bad ALSA device");
        return TC_ERROR;
    }
    if (precision != 8 && precision != 16) {
        tc_log_warn(__FILE__, "bits/sample must be 8 or 16");
        return TC_ERROR;
    }

    handle->rate      = rate;
    handle->channels  = channels;
    handle->precision = precision;

    /* ok, time to rock */
    snd_pcm_hw_params_alloca(&(hwparams));
    if (hwparams == NULL) {
        tc_log_warn(__FILE__, "cannot allocate ALSA HW parameters");
        return TC_ERROR;
    }

    tc_log_info(__FILE__, "using PCM capture device: %s", dev);
    ret = snd_pcm_open(&(handle->pcm), dev, SND_PCM_STREAM_CAPTURE, 0);
    if (ret < 0) {
        tc_log_warn(__FILE__, "error opening PCM device %s\n", dev);
        return TC_ERROR;
    }

    ret = snd_pcm_hw_params_any(handle->pcm, hwparams);
    RETURN_IF_ALSA_FAIL(ret, "cannot preconfigure PCM device");

    ret = snd_pcm_hw_params_set_access(handle->pcm, hwparams,
                                       SND_PCM_ACCESS_RW_INTERLEAVED);
    RETURN_IF_ALSA_FAIL(ret, "cannot setup PCM access");

    ret = snd_pcm_hw_params_set_format(handle->pcm, hwparams,
                                       (precision == 16) ?SND_PCM_FORMAT_S16_LE
                                                         :SND_PCM_FORMAT_S8);
    RETURN_IF_ALSA_FAIL(ret, "cannot setup PCM format");

    ret = snd_pcm_hw_params_set_rate_near(handle->pcm, hwparams, &alsa_rate, 0);
    RETURN_IF_ALSA_FAIL(ret, "cannot setup PCM rate");

    if (rate != alsa_rate) {
        tc_log_warn(__FILE__, "rate %d Hz unsupported by hardware, using %d Hz instead",
                    rate, alsa_rate);
    }

    ret = snd_pcm_hw_params_set_channels(handle->pcm, hwparams, channels);
    RETURN_IF_ALSA_FAIL(ret, "cannot setup PCM channels");

    ret = snd_pcm_hw_params(handle->pcm, hwparams);
    RETURN_IF_ALSA_FAIL(ret, "cannot setup hardware parameters");

    tc_log_info(__FILE__, "ALSA audio capture: "
                          "%i Hz, %i bps, %i channels",
                          alsa_rate, precision, channels);

    return TC_OK;
}

/* frame size = sample size (bytes) * sample number (= channels number) */
#define ALSA_FRAME_SIZE(HANDLE) \
    ((HANDLE)->channels * (HANDLE)->precision / 8)


static int tc_alsa_source_grab(TCALSASource *handle, uint8_t *buf,
                               size_t bufsize, size_t *buflen)
{
    snd_pcm_uframes_t frames = bufsize / ALSA_FRAME_SIZE(handle);
    snd_pcm_sframes_t ret = 0;

    TC_MODULE_SELF_CHECK(handle, "alsa_source_grab");
    TC_MODULE_SELF_CHECK(buf, "alsa_source_grab");
    
    ret = snd_pcm_readi(handle->pcm, buf, frames);
    if (ret == -EAGAIN || (ret >= 0 && (snd_pcm_uframes_t)ret < frames)) {
        /* this can really happen? */
        snd_pcm_wait(handle->pcm, -1);
    } else if (ret == -EPIPE) { /* xrun (overrun) */
        return alsa_source_xrun(handle);
    } else if (ret == -ESTRPIPE) { /* suspend */
        tc_log_error(__FILE__, "stream suspended (unrecoverable, yet)");
        return TC_ERROR;
    } else if (ret < 0) {
        tc_log_error(__FILE__, "ALSA read error: %s", snd_strerror(ret));
        return TC_ERROR;
    }

    if (buflen != NULL) {
        *buflen = (size_t)ret;
    }
    return TC_OK;
}

static int tc_alsa_source_close(TCALSASource *handle)
{
    TC_MODULE_SELF_CHECK(handle, "alsa_source_close");

    if (handle->pcm != NULL) {
        snd_pcm_close(handle->pcm);
        handle->pcm = NULL;
    }
    return TC_OK;
}

#undef RETURN_IF_ALSA_FAIL


/* ------------------------------------------------------------
 * New-Style module interface
 * ------------------------------------------------------------*/

typedef struct tcalsaprivatedata_ TCALSAPrivateData;
struct tcalsaprivatedata_ {
    TCALSASource handle;
};


static int tc_alsa_init(TCModuleInstance *self, uint32_t features)
{
    TCALSAPrivateData *priv = NULL;

    TC_MODULE_SELF_CHECK(self, "init");
    TC_MODULE_INIT_CHECK(self, MOD_FEATURES, features);

    if (verbose) {
        tc_log_info(MOD_NAME, "%s %s", MOD_VERSION, MOD_CAP);
    }
    priv = tc_zalloc(sizeof(TCALSAPrivateData));
    if (priv == NULL) {
        return TC_ERROR;
    }

    self->userdata = priv;    
    return TC_OK;
}

static int tc_alsa_fini(TCModuleInstance *self)
{
    TC_MODULE_SELF_CHECK(self, "fini");

    tc_free(self->userdata);
    self->userdata = NULL;

    return TC_OK;
}

static int tc_alsa_configure(TCModuleInstance *self,
                             const char *options, vob_t *vob)
{
    TCALSAPrivateData *priv = NULL;
    int ret = 0;
    char device[1024];

    TC_MODULE_SELF_CHECK(self, "configure");

    priv = self->userdata;

    strlcpy(device, "default", TC_BUF_MAX);
    if (options != NULL) {
        optstr_get(options, "device", "%1024s", device);
        device[1024-1] = '\0';
        /* yeah, this is pretty ugly -- FR */
    }
 
    /* it would be nice to have some more validation in here */
    ret = tc_alsa_source_open(&(priv->handle), device,
                              vob->a_rate, vob->a_bits, vob->a_chan);
    if (ret != 0) {
        tc_log_error(MOD_NAME, "configure: failed to open ALSA device"
                               "'%s'", device);
        return TC_ERROR;
    }
    return TC_OK;
}

static int tc_alsa_inspect(TCModuleInstance *self,
                          const char *param, const char **value)
{
    TC_MODULE_SELF_CHECK(self, "inspect");

    if (optstr_lookup(param, "help")) {
        *value = tc_alsa_help;
    }

    return TC_OK;
}

static int tc_alsa_stop(TCModuleInstance *self)
{
    TCALSAPrivateData *priv = NULL;
    int ret = 0;

    TC_MODULE_SELF_CHECK(self, "stop");

    priv = self->userdata;

    ret = tc_alsa_source_close(&(priv->handle));
    if (ret != TC_OK) {
        tc_log_error(MOD_NAME, "stop: failed to close ALSA device");
        return TC_ERROR;
    }

    return TC_OK;
}

static int tc_alsa_demultiplex(TCModuleInstance *self,
                               vframe_list_t *vframe, aframe_list_t *aframe)
{
    TCALSAPrivateData *priv = NULL;
    int ret = TC_OK;
    size_t len = 0;

    TC_MODULE_SELF_CHECK(self, "demultiplex");
    
    priv = self->userdata;

    if (vframe != NULL) {
        vframe->video_len = 0; /* no audio from here */
        ret = TC_OK;
    }

    if (aframe != NULL) {
        ret = tc_alsa_source_grab(&(priv->handle), aframe->audio_buf,
                                  aframe->audio_size, &len);
        aframe->audio_len = (size_t)len;
    }
    return ret;
}

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

static const TCCodecID tc_alsa_codecs_in[] = { TC_CODEC_ERROR };

/* a multiplexor is at the end of pipeline */
static const TCCodecID tc_alsa_codecs_out[] = { 
    TC_CODEC_PCM,
    TC_CODEC_ERROR,
};

static const TCFormatID tc_alsa_formats_in[] = {
    TC_FORMAT_ALSA,
    TC_FORMAT_ERROR, 
};

static const TCFormatID tc_alsa_formats_out[] = { TC_FORMAT_ERROR };

static const TCModuleInfo tc_alsa_info = {
    .features    = MOD_FEATURES,
    .flags       = MOD_FLAGS,
    .name        = MOD_NAME,
    .version     = MOD_VERSION,
    .description = MOD_CAP,
    .codecs_in   = tc_alsa_codecs_in,
    .codecs_out  = tc_alsa_codecs_out,
    .formats_in  = tc_alsa_formats_in,
    .formats_out = tc_alsa_formats_out
};

static const TCModuleClass tc_alsa_class = {
    TC_MODULE_CLASS_HEAD(tc_alsa),

    .init         = tc_alsa_init,
    .fini         = tc_alsa_fini,
    .configure    = tc_alsa_configure,
    .stop         = tc_alsa_stop,
    .inspect      = tc_alsa_inspect,

    .demultiplex  = tc_alsa_demultiplex,
};

TC_MODULE_ENTRY_POINT(tc_alsa)

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

/* ------------------------------------------------------------
 * Old-Style module interface
 * ------------------------------------------------------------*/

static int verbose_flag = TC_QUIET;
static int capability_flag = TC_CAP_PCM;

#define MOD_PRE alsa
#define MOD_CODEC   "(audio) pcm"

#include "import_def.h"


static TCALSASource handle = {
    .pcm       = NULL,
    .rate      = RATE,
    .channels  = CHANNELS,
    .precision = BITS,
};


MOD_open
{
    int ret = TC_ERROR;
    char device[1024];

    switch (param->flag) {
      case TC_VIDEO:
        tc_log_warn(MOD_NAME, "unsupported request (init video)");
        break;
      case TC_AUDIO:
        if (verbose_flag & TC_DEBUG) {
            tc_log_info(MOD_NAME, "ALSA audio grabbing");
        }

        strlcpy(device, "default", 1024);
        if (vob->im_a_string != NULL) {
            optstr_get(vob->im_a_string, "device", "%1024s", device);
            device[1024-1] = '\0';
            /* yeah, this too is pretty ugly -- FR */
        }

        ret = tc_alsa_source_open(&handle, device,
                                  vob->a_rate, vob->a_bits, vob->a_chan);
        break;
      default:
        tc_log_warn(MOD_NAME, "unsupported request (init)");
        break;
    }

    return ret;
}


MOD_decode
{
    int ret = TC_ERROR;

    switch (param->flag) {
      case TC_VIDEO:
        tc_log_warn(MOD_NAME, "unsupported request (decode video)");
        break;
      case TC_AUDIO:
        ret = tc_alsa_source_grab(&handle, param->buffer,
                                  param->size, NULL);
        break;
      default:
        tc_log_warn(MOD_NAME, "unsupported request (decode)");
        break;
    }

    return ret;
}


MOD_close
{
    int ret = TC_ERROR;

    switch (param->flag) {
      case TC_VIDEO:
        tc_log_warn(MOD_NAME, "unsupported request (close video)");
        break;
      case TC_AUDIO:
        ret = tc_alsa_source_close(&handle);
        break;
      default:
        tc_log_warn(MOD_NAME, "unsupported request (close)");
        break;
    }

    return ret;
}

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

/*
 * Local variables:
 *   c-file-style: "stroustrup"
 *   c-file-offsets: ((case-label . *) (statement-case-intro . *))
 *   indent-tabs-mode: nil
 * End:
 *
 * vim: expandtab shiftwidth=4:
 */
