shithub: sox

ref: 4976f1410cc69ae475fdaedc574a404ed606dafd
dir: /src/alsa.c/

View raw version
/*
 * Copyright 1997-2005 Jimen Ching And Sundry Contributors
 * This source code is freely redistributable and may be used for
 * any purpose.  This copyright notice must be maintained.
 * Jimen Ching And Sundry Contributors are not
 * responsible for the consequences of using this software.
 */

/* ALSA sound driver
 *
 * converted to alsalib cbagwell 20050914
 * added by Jimen Ching (jching@flex.com) 19990207
 * based on info grabed from aplay.c in alsa-utils package.
 * Updated for ALSA 0.9.X API 20020824.
 */

#include "st_i.h"

#if defined(HAVE_ALSA)

#include <alsa/asoundlib.h>

typedef struct alsa_priv
{
    snd_pcm_t *pcm_handle;
    void *buf;
    st_ssize_t buf_size;
} *alsa_priv_t;

static int get_format(ft_t ft, snd_pcm_format_mask_t *fmask, int *fmt);

extern void st_ub_write_buf(char* buf1, st_sample_t *buf2, st_ssize_t len, char swap);
extern void st_sb_write_buf(char *buf1, st_sample_t *buf2, st_ssize_t len, char swap);
extern void st_uw_write_buf(char *buf1, st_sample_t *buf2, st_ssize_t len, char swap);
extern void st_sw_write_buf(char *buf1, st_sample_t *buf2, st_ssize_t len, char swap);
extern void st_ub_read_buf(st_sample_t *buf1, char *buf2, st_ssize_t len, char swap);
extern void st_sb_read_buf(st_sample_t *buf1, char *buf2, st_ssize_t len, char swap);
extern void st_uw_read_buf(st_sample_t *buf1, char *buf2, st_ssize_t len, char swap);
extern void st_sw_read_buf(st_sample_t *buf1, char *buf2, st_ssize_t len, char swap);

int st_alsasetup(ft_t ft, snd_pcm_stream_t mode)
{
    int fmt = SND_PCM_FORMAT_S16;
    int err;
    alsa_priv_t alsa = (alsa_priv_t)ft->priv;
    snd_pcm_hw_params_t *hw_params;
    unsigned int min_rate, max_rate;
    unsigned int min_chan, max_chan;
    unsigned int rate;
    int dir;
    snd_pcm_format_mask_t *fmask;

    /* Reserve buffer for 16-bit data.  FIXME: Whats a good size? */
    alsa->buf_size = ST_BUFSIZ*2;

    if ((alsa->buf = malloc(alsa->buf_size)) == NULL) 
    {
        st_fail_errno(ft,ST_ENOMEM,
                      "unable to allocate output buffer of size %d", 
                      ft->file.size);
        return(ST_EOF);
    }

    if ((err = snd_pcm_open(&(alsa->pcm_handle), ft->filename, 
                            mode, 0)) < 0) 
    {
        st_fail_errno(ft, ST_EPERM, "cannot open audio device\n");
        return ST_EOF;
    }

    if ((err = snd_pcm_hw_params_malloc(&hw_params)) < 0) 
    {
        st_fail_errno(ft, ST_ENOMEM, 
                      "cannot allocate hardware parameter structure\n");
        return ST_EOF;
    }

    if ((err = snd_pcm_hw_params_any(alsa->pcm_handle, hw_params)) < 0) 
    {
        st_fail_errno(ft, ST_EPERM,
                      "cannot initialize hardware parameter structure\n");
        return ST_EOF;
    }

    if ((err = snd_pcm_hw_params_set_access(alsa->pcm_handle, hw_params, 
                                            SND_PCM_ACCESS_RW_INTERLEAVED)) < 0)
    {
        st_fail_errno(ft, ST_EPERM,
                      "cannot set access type\n");
        return ST_EOF;
    }

    snd_pcm_hw_params_get_channels_min(hw_params, &min_chan);
    snd_pcm_hw_params_get_channels_max(hw_params, &max_chan);
    if (ft->info.channels == -1) 
        ft->info.channels = min_chan;
    else 
        if (ft->info.channels > max_chan) 
            ft->info.channels = max_chan;
        else if (ft->info.channels < min_chan) 
            ft->info.channels = min_chan;

    snd_pcm_format_mask_malloc(&fmask);
    snd_pcm_hw_params_get_format_mask(hw_params, fmask);

    if (get_format(ft, fmask, &fmt) < 0)
        return (ST_EOF);

    snd_pcm_format_mask_free(fmask);

    if ((err = snd_pcm_hw_params_set_format(alsa->pcm_handle, 
                                            hw_params, fmt)) < 0) 
    {
        st_fail_errno(ft, ST_EPERM, "cannot set sample format\n");
        return ST_EOF;
    }

    snd_pcm_hw_params_get_rate_min(hw_params, &min_rate, &dir);
    snd_pcm_hw_params_get_rate_max(hw_params, &max_rate, &dir);

    rate = ft->info.rate;
    if (rate < min_rate) 
        rate = min_rate;
    else 
        if (rate > max_rate) 
            rate = max_rate;
    if (rate != ft->info.rate)
    {
        st_warn("alsa: Hardware does not support %d.  Forcing sample rate to %d.\n", ft->info.rate, rate);
    }

    dir = 0;
    if ((err = snd_pcm_hw_params_set_rate_near(alsa->pcm_handle, 
                                               hw_params, 
                                               &rate,
                                               &dir)) < 0) 
    {
        st_fail_errno(ft, ST_EPERM, "cannot set sample rate\n");
        return ST_EOF;
    }
    if (rate != ft->info.rate)
    {
        st_warn("Could not set exact rate of %d.  Approximating with %d\n",
                ft->info.rate, rate);
    }

    snd_pcm_hw_params_get_rate(hw_params, &rate, &dir);

    if ((err = snd_pcm_hw_params_set_channels(alsa->pcm_handle,
                                              hw_params, 
                                              ft->info.channels)) < 0) 
    {
        st_fail_errno(ft, ST_EPERM, "cannot set channel count\n");
        return ST_EOF;
    }

#if 0
    /* Set number of periods. Periods used to be called fragments. */ 
    if (snd_pcm_hw_params_set_periods(alsa->pcm_handle, hw_params, 2, 0) < 0) 
    {
        st_fail_errno(ft, ST_EPERM, "Error setting periods.\n");
        return ST_EOF;
    }

    /* Set buffer size (in frames). The resulting latency is given by */
    /* latency = periodsize * periods / (rate * bytes_per_frame)     */
    if (snd_pcm_hw_params_set_buffer_size(alsa->pcm_handle, hw_params, (ST_BUFSIZ * 8)>>2) < 0) {
      st_fail_errno(ft, ST_EPERM, "Error setting buffersize.\n");
      return ST_EOF;
    }
#endif

    if ((err = snd_pcm_hw_params(alsa->pcm_handle, hw_params)) < 0) 
    {
        st_fail_errno(ft, ST_EPERM, "cannot set parameters\n");
        return ST_EOF;
    }

    snd_pcm_hw_params_free(hw_params);

    if ((err = snd_pcm_prepare(alsa->pcm_handle)) < 0) 
    {
        st_fail_errno(ft, ST_EPERM, "cannot prepare audio interface for use\n");
        return ST_EOF;
    }

    sigintreg(ft);      /* Prepare to catch SIGINT */

    return (ST_SUCCESS);
}

/*
 * Do anything required before you start reading samples.
 * Read file header.
 *      Find out sampling rate,
 *      size and encoding of samples,
 *      mono/stereo/quad.
 */
int st_alsastartread(ft_t ft)
{
    return st_alsasetup(ft, SND_PCM_STREAM_CAPTURE);
}

st_ssize_t st_alsaread(ft_t ft, st_sample_t *buf, st_ssize_t nsamp)
{
    st_ssize_t len;
    alsa_priv_t alsa = (alsa_priv_t)ft->priv;
    void (*read_buf)(st_sample_t *, char *, st_ssize_t, char) = 0;

    /* Check to see if user sent SIGINT and if so return ST_EOF to
     * stop playing.
     */
    if (ft->file.eof)
        return ST_EOF;


    switch(ft->info.size) {
        case ST_SIZE_BYTE:
            switch(ft->info.encoding)
            {
                case ST_ENCODING_SIGN2:
                    read_buf = st_sb_read_buf;
                    break;
                case ST_ENCODING_UNSIGNED:
                    read_buf = st_ub_read_buf;
                    break;
                default:
                    st_fail_errno(ft,ST_EFMT,"Do not support this encoding for this data size");
                    return ST_EOF;
            }
            break;
        case ST_SIZE_WORD:
            switch(ft->info.encoding)
            {
                case ST_ENCODING_SIGN2:
                    read_buf = st_sw_read_buf;
                    break;
                case ST_ENCODING_UNSIGNED:
                    read_buf = st_uw_read_buf;
                    break;
                default:
                    st_fail_errno(ft,ST_EFMT,"Do not support this encoding for this data size");
                    return ST_EOF;
            }
            break;
        default:
            st_fail_errno(ft,ST_EFMT,"Do not support this data size for this handler");
            return ST_EOF;
    }

    /* Prevent overflow */
    if (nsamp > alsa->buf_size/ft->info.size)
        nsamp = (alsa->buf_size/ft->info.size);

    len = snd_pcm_readi(alsa->pcm_handle, alsa->buf, nsamp);

    read_buf(buf, alsa->buf, len, ft->swap);

    return len;
}

int st_alsastopread(ft)
ft_t ft;
{
    alsa_priv_t alsa = (alsa_priv_t)ft->priv;

    snd_pcm_close(alsa->pcm_handle);

    free(alsa->buf);

    return ST_SUCCESS;
}

int st_alsastartwrite(ft_t ft)
{
    return st_alsasetup(ft, SND_PCM_STREAM_PLAYBACK);

    int fmt = SND_PCM_FORMAT_S16;
    int err;
    alsa_priv_t alsa = (alsa_priv_t)ft->priv;
    snd_pcm_hw_params_t *hw_params;
    unsigned int min_rate, max_rate;
    unsigned int min_chan, max_chan;
    unsigned int rate;
    int dir;
    snd_pcm_format_mask_t *fmask;

    /* Reserve buffer for 16-bit data.  FIXME: Whats a good size? */
    alsa->buf_size = ST_BUFSIZ*2;

    if ((alsa->buf = malloc(alsa->buf_size)) == NULL) 
    {
        st_fail_errno(ft,ST_ENOMEM,
                      "unable to allocate output buffer of size %d", 
                      ft->file.size);
        return(ST_EOF);
    }

    if ((err = snd_pcm_open(&(alsa->pcm_handle), ft->filename, 
                            SND_PCM_STREAM_PLAYBACK, 0)) < 0) 
    {
        st_fail_errno(ft, ST_EPERM, "cannot open audio device\n");
        return ST_EOF;
    }

    if ((err = snd_pcm_hw_params_malloc(&hw_params)) < 0) 
    {
        st_fail_errno(ft, ST_ENOMEM, 
                      "cannot allocate hardware parameter structure\n");
        return ST_EOF;
    }

    if ((err = snd_pcm_hw_params_any(alsa->pcm_handle, hw_params)) < 0) 
    {
        st_fail_errno(ft, ST_EPERM,
                      "cannot initialize hardware parameter structure\n");
        return ST_EOF;
    }

    if ((err = snd_pcm_hw_params_set_access(alsa->pcm_handle, hw_params, 
                                            SND_PCM_ACCESS_RW_INTERLEAVED)) < 0)
    {
        st_fail_errno(ft, ST_EPERM,
                      "cannot set access type\n");
        return ST_EOF;
    }

    snd_pcm_hw_params_get_channels_min(hw_params, &min_chan);
    snd_pcm_hw_params_get_channels_max(hw_params, &max_chan);
    if (ft->info.channels == -1) 
        ft->info.channels = min_chan;
    else 
        if (ft->info.channels > max_chan) 
            ft->info.channels = max_chan;
        else if (ft->info.channels < min_chan) 
            ft->info.channels = min_chan;

    snd_pcm_format_mask_malloc(&fmask);
    snd_pcm_hw_params_get_format_mask(hw_params, fmask);

    if (get_format(ft, fmask, &fmt) < 0)
        return (ST_EOF);

    snd_pcm_format_mask_free(fmask);

    if ((err = snd_pcm_hw_params_set_format(alsa->pcm_handle, 
                                            hw_params, fmt)) < 0) 
    {
        st_fail_errno(ft, ST_EPERM, "cannot set sample format\n");
        return ST_EOF;
    }

    snd_pcm_hw_params_get_rate_min(hw_params, &min_rate, &dir);
    snd_pcm_hw_params_get_rate_max(hw_params, &max_rate, &dir);

    rate = ft->info.rate;
    if (rate < min_rate) 
        rate = min_rate;
    else 
        if (rate > max_rate) 
            rate = max_rate;
    if (rate != ft->info.rate)
    {
        st_warn("alsa: Hardware does not support %d.  Forcing sample rate to %d.\n", ft->info.rate, rate);
    }

    dir = 0;
    if ((err = snd_pcm_hw_params_set_rate_near(alsa->pcm_handle, 
                                               hw_params, 
                                               &rate,
                                               &dir)) < 0) 
    {
        st_fail_errno(ft, ST_EPERM, "cannot set sample rate\n");
        return ST_EOF;
    }
    if (rate != ft->info.rate)
    {
        st_warn("Could not set exact rate of %d.  Approximating with %d\n",
                ft->info.rate, rate);
    }

    snd_pcm_hw_params_get_rate(hw_params, &rate, &dir);

    if ((err = snd_pcm_hw_params_set_channels(alsa->pcm_handle,
                                              hw_params, 
                                              ft->info.channels)) < 0) 
    {
        st_fail_errno(ft, ST_EPERM, "cannot set channel count\n");
        return ST_EOF;
    }

#if 0
    /* Set number of periods. Periods used to be called fragments. */ 
    if (snd_pcm_hw_params_set_periods(alsa->pcm_handle, hw_params, 2, 0) < 0) 
    {
        st_fail_errno(ft, ST_EPERM, "Error setting periods.\n");
        return ST_EOF;
    }

    /* Set buffer size (in frames). The resulting latency is given by */
    /* latency = periodsize * periods / (rate * bytes_per_frame)     */
    if (snd_pcm_hw_params_set_buffer_size(alsa->pcm_handle, hw_params, (ST_BUFSIZ * 2)>>2) < 0) {
      st_fail_errno(ft, ST_EPERM, "Error setting buffersize.\n");
      return ST_EOF;
    }
#endif

    if ((err = snd_pcm_hw_params(alsa->pcm_handle, hw_params)) < 0) 
    {
        st_fail_errno(ft, ST_EPERM, "cannot set parameters\n");
        return ST_EOF;
    }

    snd_pcm_hw_params_free(hw_params);

    if ((err = snd_pcm_prepare(alsa->pcm_handle)) < 0) 
    {
        st_fail_errno(ft, ST_EPERM, "cannot prepare audio interface for use\n");
        return ST_EOF;
    }

    sigintreg(ft);      /* Prepare to catch SIGINT */

    return (ST_SUCCESS);
}

st_ssize_t st_alsawrite(ft_t ft, st_sample_t *buf, st_ssize_t nsamp)
{
    st_ssize_t len;
    alsa_priv_t alsa = (alsa_priv_t)ft->priv;
    void (*write_buf)(char *, st_sample_t *, st_ssize_t, char) = 0;

    /* Check to see if user sent SIGINT and if so return ST_EOF to
     * stop recording
     */
    if (ft->file.eof)
        return ST_EOF;

    switch(ft->info.size) {
        case ST_SIZE_BYTE:
            switch(ft->info.encoding)
            {
                case ST_ENCODING_SIGN2:
                    write_buf = st_sb_write_buf;
                    break;
                case ST_ENCODING_UNSIGNED:
                    write_buf = st_ub_write_buf;
                    break;
                default:
                    st_fail_errno(ft,ST_EFMT,"Do not support this encoding for this data size");
                    return ST_EOF;
            }
            break;
        case ST_SIZE_WORD:
            switch(ft->info.encoding)
            {
                case ST_ENCODING_SIGN2:
                    write_buf = st_sw_write_buf;
                    break;
                case ST_ENCODING_UNSIGNED:
                    write_buf = st_uw_write_buf;
                    break;
                default:
                    st_fail_errno(ft,ST_EFMT,"Do not support this encoding for this data size");
                    return ST_EOF;
            }
            break;
        default:
            st_fail_errno(ft,ST_EFMT,"Do not support this data size for this handler");
            return ST_EOF;
    }

    /* Prevent overflow */
    if (nsamp > alsa->buf_size/ft->info.size)
        nsamp = (alsa->buf_size/ft->info.size);

    write_buf(alsa->buf, buf, nsamp, ft->swap);

    if ((len = snd_pcm_writei(alsa->pcm_handle, alsa->buf, nsamp)) != nsamp) {
        snd_pcm_prepare(alsa->pcm_handle);
        fprintf(stderr, "Report me!  Buffer underrun thats not dealt with\n");
    }

    return len;
}


int st_alsastopwrite(ft)
ft_t ft;
{
    alsa_priv_t alsa = (alsa_priv_t)ft->priv;

    snd_pcm_drain(alsa->pcm_handle);
    snd_pcm_close(alsa->pcm_handle);

    free(alsa->buf);

    return ST_SUCCESS;
}

#define EMSGFMT "ALSA driver does not support %s %s output"

static int get_format(ft_t ft, snd_pcm_format_mask_t *fmask, int *fmt)
{
    if (ft->info.size == -1)
        ft->info.size = ST_SIZE_WORD;

    if (ft->info.encoding == -1)
    {
        if (ft->info.size == ST_SIZE_WORD)
            ft->info.encoding = ST_ENCODING_SIGN2;
        else
            ft->info.encoding = ST_ENCODING_UNSIGNED;
    }

    if (ft->info.size != ST_SIZE_WORD &&
        ft->info.size != ST_SIZE_BYTE)
    {
        st_warn("ALSA driver only supports byte and word samples.  Changing to word.\n");
        ft->info.size = ST_SIZE_WORD;
    }

    if (ft->info.encoding != ST_ENCODING_SIGN2 &&
        ft->info.encoding != ST_ENCODING_UNSIGNED)
    {
        if (ft->info.size == ST_SIZE_WORD)
        {
            st_warn("ALSA driver only supports signed and unsigned samples.  Changing to signed.\n");
            ft->info.encoding = ST_ENCODING_SIGN2;
        }
        else
        {
            st_warn("ALSA driver only supports signed and unsigned samples.  Changing to unsigned.\n");
            ft->info.encoding = ST_ENCODING_UNSIGNED;
        }
    }

    /* Some hardware only wants to work with 8-bit or 16-bit data */
    if (ft->info.size == ST_SIZE_BYTE)
    {
        if (!(snd_pcm_format_mask_test(fmask, SND_PCM_FORMAT_U8)) && 
            !(snd_pcm_format_mask_test(fmask, SND_PCM_FORMAT_S8)))
        {
            st_report("ALSA driver doesn't supported byte samples.  Changing to words.");
            ft->info.size = ST_SIZE_WORD;
        }
    }
    else if (ft->info.size == ST_SIZE_WORD)
    {
        if (!(snd_pcm_format_mask_test(fmask, SND_PCM_FORMAT_U16)) && 
            !(snd_pcm_format_mask_test(fmask, SND_PCM_FORMAT_S16)))
        {
            st_report("ALSA driver doesn't supported word samples.  Changing to bytes.");
            ft->info.size = ST_SIZE_BYTE;
        }
    }
    else
    {
        if ((snd_pcm_format_mask_test(fmask, SND_PCM_FORMAT_U16)) ||
            (snd_pcm_format_mask_test(fmask, SND_PCM_FORMAT_S16)))
        {
            st_report("ALSA driver doesn't supported %s samples.  Changing to words.", st_sizes_str[(unsigned char)ft->info.size]);
            ft->info.size = ST_SIZE_WORD;
        }
        else
        {
            st_report("ALSA driver doesn't supported %s samples.  Changing to bytes.", st_sizes_str[(unsigned char)ft->info.size]);
            ft->info.size = ST_SIZE_BYTE;
        }
    }

    if (ft->info.size == ST_SIZE_BYTE) {
        switch (ft->info.encoding)
        {
            case ST_ENCODING_SIGN2:
                if (!(snd_pcm_format_mask_test(fmask, SND_PCM_FORMAT_S8)))
                {
                    st_report("ALSA driver doesn't supported signed byte samples.  Changing to unsigned bytes.");
                    ft->info.encoding = ST_ENCODING_UNSIGNED;
                }
                break;
            case ST_ENCODING_UNSIGNED:
                if (!(snd_pcm_format_mask_test(fmask, SND_PCM_FORMAT_U8)))
                {
                    st_report("ALSA driver doesn't supported unsigned byte samples.  Changing to signed bytes.");
                    ft->info.encoding = ST_ENCODING_SIGN2;
                }
                break;
        }
        switch (ft->info.encoding)
        {
            case ST_ENCODING_SIGN2:
                if (!(snd_pcm_format_mask_test(fmask, SND_PCM_FORMAT_S8)))
                {
                    st_fail_errno(ft,ST_EFMT,"ALSA driver does not support signed byte samples");
                    return ST_EOF;
                }
                *fmt = SND_PCM_FORMAT_S8;
                break;
            case ST_ENCODING_UNSIGNED:
                if (!(snd_pcm_format_mask_test(fmask, SND_PCM_FORMAT_U8)))
                {
                    st_fail_errno(ft,ST_EFMT,"ALSA driver does not support unsigned byte samples");
                    return ST_EOF;
                }
                *fmt = SND_PCM_FORMAT_U8;
                break;
        }
    }
    else if (ft->info.size == ST_SIZE_WORD) {
        switch (ft->info.encoding)
        {
            case ST_ENCODING_SIGN2:
                if (!(snd_pcm_format_mask_test(fmask, SND_PCM_FORMAT_S16)))
                {
                    st_report("ALSA driver does not support signed word samples.  Changing to unsigned words.");
                    ft->info.encoding = ST_ENCODING_UNSIGNED;
                }
                break;
            case ST_ENCODING_UNSIGNED:
                if (!(snd_pcm_format_mask_test(fmask, SND_PCM_FORMAT_U16)))
                {
                    st_report("ALSA driver does not support unsigned word samples.  Changing to signed words.");
                    ft->info.encoding = ST_ENCODING_SIGN2;
                }
                break;
        }
        switch (ft->info.encoding)
        {
            case ST_ENCODING_SIGN2:
                if (!(snd_pcm_format_mask_test(fmask, SND_PCM_FORMAT_S16)))
                {
                    st_fail_errno(ft,ST_EFMT,"ALSA driver does not support signed word samples");
                    return ST_EOF;
                }
                *fmt = SND_PCM_FORMAT_S16_LE;
                break;
            case ST_ENCODING_UNSIGNED:
                if (!(snd_pcm_format_mask_test(fmask, SND_PCM_FORMAT_U16)))
                {
                    st_fail_errno(ft,ST_EFMT,"ALSA driver does not support unsigned word samples");
                    return ST_EOF;
                }
                *fmt = SND_PCM_FORMAT_U16_LE;
                break;
        }
    }
    else {
        st_fail_errno(ft,ST_EFMT,EMSGFMT,st_encodings_str[(unsigned char)ft->info.encoding], st_sizes_str[(unsigned char)ft->info.size]);
        return ST_EOF;
    }
    return 0;
}

#endif /* HAVE_ALSA */