/* Copyright (c) 1997-2003 Guenter Geiger, Miller Puckette, Larry Troxler, Winfried Ritsch, Karl MacMillan, and others.
* For information on usage and redistribution, and for a DISCLAIMER OF ALL
* WARRANTIES, see the file, "LICENSE.txt," in this distribution.  */

/* this file inputs and outputs audio using the ALSA API available on linux. */

/* support for ALSA pcmv2 api by Karl MacMillan<karlmac@peabody.jhu.edu> */ 
/* support for ALSA MMAP noninterleaved by Winfried Ritsch, IEM */

#include <alsa/asoundlib.h>

#include "desire.h"
using namespace desire;
#include <errno.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <sched.h>
#include <sys/mman.h>
#include "s_audio_alsa.h"

/* Defines */
#define DEBUG(x) x
#define DEBUG2(x) {x;}

/* needed for alsa 0.9 compatibility: */
#if (SND_LIB_MAJOR < 1)
#define ALSAAPI9
#endif

//static void alsa_close_audio();
static void alsa_checkiosync();
static void alsa_numbertoname(int iodev, char *devname, int nchar);
static int alsa_jittermax;
static void alsa_close_audio();
#define ALSA_DEFJITTERMAX 3

    /* don't assume we can turn all 31 bits when doing float-to-fix; 
    otherwise some audio drivers (e.g. Midiman/ALSA) wrap around. */
#define FMAX 0x7ffff000
#define CLIP32(x) (((x)>FMAX)?FMAX:((x) < -FMAX)?-FMAX:(x))

static char *alsa_snd_buf;
static int alsa_snd_bufsize;
static int alsa_buf_samps;
static snd_pcm_status_t *alsa_status; 
static int alsa_usemmap;
t_alsa alsai,alsao;

static void check_error(int err, const char *why) {if (err<0) error("%s: %s", why, snd_strerror(err));}

static int alsaio_canmmap(t_alsa_dev *dev) {
    snd_pcm_hw_params_t *hw_params;
    int err1, err2;
    snd_pcm_hw_params_alloca(&hw_params);
    err1 = snd_pcm_hw_params_any(dev->a_handle, hw_params);
    if (err1 < 0) {
      check_error(err1,"Broken configuration: no configurations available"); 
      return 0;
    }
    err1 =     snd_pcm_hw_params_set_access(dev->a_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED);
    if (err1 < 0) {
        err2 = snd_pcm_hw_params_set_access(dev->a_handle, hw_params, SND_PCM_ACCESS_MMAP_NONINTERLEAVED);
    } else err2 = -1;
#if 0
    error("err 1 %d (%s), err2 %d (%s)", err1, snd_strerror(err1), err2, snd_strerror(err2));
#endif
    return err1<0 && err2>=0;
}

static int alsaio_setup(t_alsa_dev *dev, int out, int *channels, int *rate, int nfrags, int frag_size) {
    int bufsizeforthis, err;
    snd_pcm_hw_params_t* hw_params;
    unsigned int tmp_uint;
    snd_pcm_uframes_t tmp_snd_pcm_uframes;
    if (sys_verbose) {
	if (out) post("configuring sound output...");
        else     post("configuring sound input...");
    }
    /* set hardware parameters... */
    snd_pcm_hw_params_alloca(&hw_params);
    /* get the default params */
    err = snd_pcm_hw_params_any(dev->a_handle, hw_params);
    check_error(err, "snd_pcm_hw_params_any");
    /* try to set interleaved access */
    err = snd_pcm_hw_params_set_access(dev->a_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED);
    if (err < 0) return -1;
    check_error(err, "snd_pcm_hw_params_set_access");
    /* Try to set 32 bit format first */
    err = snd_pcm_hw_params_set_format(dev->a_handle, hw_params, SND_PCM_FORMAT_S32);
    if (err<0) {
        error("PD-ALSA: 32 bit format not available - using 16");
        err = snd_pcm_hw_params_set_format(dev->a_handle, hw_params,SND_PCM_FORMAT_S16);
        check_error(err, "snd_pcm_hw_params_set_format");
        dev->a_sampwidth = 2;
    } else dev->a_sampwidth = 4;
    if (sys_verbose) post("Sample width set to %d bytes", dev->a_sampwidth);
    /* set the subformat */
    err = snd_pcm_hw_params_set_subformat(dev->a_handle, hw_params, SND_PCM_SUBFORMAT_STD);
    check_error(err, "snd_pcm_hw_params_set_subformat");
    /* set the number of channels */
    tmp_uint = *channels;
    err = snd_pcm_hw_params_set_channels_min(dev->a_handle, hw_params, &tmp_uint);
    check_error(err, "snd_pcm_hw_params_set_channels");
    if (tmp_uint != (unsigned)*channels) post("ALSA: set input channels to %d", tmp_uint);
    *channels = tmp_uint;
    dev->a_channels = *channels;
    /* set the sampling rate */
    err = snd_pcm_hw_params_set_rate_min(dev->a_handle, hw_params, (unsigned int *)rate, 0);
    check_error(err, "snd_pcm_hw_params_set_rate_min (input)");
#if 0
    err = snd_pcm_hw_params_get_rate(hw_params, &subunitdir);
    post("input sample rate %d", err);
#endif
    /* set the period - ie frag size */
    /* LATER try this to get a recommended period size...
       right now, it trips an assertion failure in ALSA lib */
#ifdef ALSAAPI9
    err = snd_pcm_hw_params_set_period_size_near(dev->a_handle, hw_params, (snd_pcm_uframes_t)frag_size, 0);
#else
    tmp_snd_pcm_uframes = frag_size;
    err = snd_pcm_hw_params_set_period_size_near(dev->a_handle, hw_params, &tmp_snd_pcm_uframes, 0);
#endif
    check_error(err, "snd_pcm_hw_params_set_period_size_near (input)");
    /* set the number of periods - ie numfrags */
#ifdef ALSAAPI9
    err = snd_pcm_hw_params_set_periods_near(dev->a_handle, hw_params, nfrags, 0);
#else
    tmp_uint = nfrags;
    err = snd_pcm_hw_params_set_periods_near(dev->a_handle, hw_params, &tmp_uint, 0);
#endif
    check_error(err, "snd_pcm_hw_params_set_periods_near (input)");
    /* set the buffer size */
#ifdef ALSAAPI9
    err = snd_pcm_hw_params_set_buffer_size_near(dev->a_handle, hw_params, nfrags * frag_size);
#else
    tmp_snd_pcm_uframes = nfrags * frag_size;
    err = snd_pcm_hw_params_set_buffer_size_near(dev->a_handle, hw_params, &tmp_snd_pcm_uframes);
#endif
    check_error(err, "snd_pcm_hw_params_set_buffer_size_near (input)");
    err = snd_pcm_hw_params(dev->a_handle, hw_params);
    check_error(err, "snd_pcm_hw_params (input)");
    /* set up the buffer */
    bufsizeforthis = sys_dacblocksize * dev->a_sampwidth * *channels;
    if (alsa_snd_buf) {
        if (alsa_snd_bufsize < bufsizeforthis) {
            if (!(alsa_snd_buf = (char *)realloc(alsa_snd_buf, bufsizeforthis))) {error("out of memory"); return 0;}
            memset(alsa_snd_buf, 0, bufsizeforthis);
            alsa_snd_bufsize = bufsizeforthis;
        }
    } else {
        if (!(alsa_snd_buf = (char *)malloc(bufsizeforthis))) {error("out of memory"); return 0;}
        memset(alsa_snd_buf, 0, bufsizeforthis);
        alsa_snd_bufsize = bufsizeforthis;
    }
    return 1;
}

/* return 0 on success */
static int alsa_open_audio(
int  naudioindev, int * audioindev, int  nchindev, int * chindev,
int naudiooutdev, int *audiooutdev, int nchoutdev, int *choutdev, int rate, int dummy) {
    int err, inchans = 0, outchans = 0;
    char devname[512];
    int frag_size = (sys_blocksize ? sys_blocksize : ALSA_DEFFRAGSIZE);
    int nfrags, i;
    nfrags = int(sys_schedadvance * (float)rate / (1e6 * frag_size));
    /* save our belief as to ALSA's buffer size for later */
    alsa_buf_samps = nfrags * frag_size;
    alsai.ndev = alsao.ndev = 0;
    alsa_jittermax = ALSA_DEFJITTERMAX;
    if (sys_verbose) post("audio buffer set to %d", (int)(0.001 * sys_schedadvance));
    for (int i=0; i<naudioindev; i++) {
        alsa_numbertoname(audioindev[i], devname, 512);
        err = snd_pcm_open(&alsai.dev[alsai.ndev].a_handle, devname, SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK);
        check_error(err, "snd_pcm_open (input)");
        if (err<0) continue;
        alsai.dev[alsai.ndev].a_devno = audioindev[i];
        snd_pcm_nonblock(alsai.dev[alsai.ndev].a_handle, 1);
        if (sys_verbose) post("opened input device name %s", devname);
        alsai.ndev++;
    }
    for (int i=0; i<naudiooutdev; i++) {
        alsa_numbertoname(audiooutdev[i], devname, 512);
        err = snd_pcm_open(&alsao.dev[alsao.ndev].a_handle, devname, SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK);
        check_error(err, "snd_pcm_open (output)");
        if (err<0) continue;
        alsao.dev[alsao.ndev].a_devno = audiooutdev[i];
        snd_pcm_nonblock(alsao.dev[alsao.ndev].a_handle, 1);
        alsao.ndev++;
    }
    if (!alsai.ndev && !alsao.ndev) goto blewit;
    /* If all the open devices support mmap_noninterleaved, let's call Wini's code in s_audio_alsamm.c */
    alsa_usemmap = 1;
    for (int i=0; i<alsai.ndev; i++) if (!alsaio_canmmap(&alsai.dev[i])) alsa_usemmap = 0;
    for (int i=0; i<alsao.ndev; i++) if (!alsaio_canmmap(&alsao.dev[i])) alsa_usemmap = 0;
    if (alsa_usemmap) {
        post("using mmap audio interface");
        if (alsamm_open_audio(rate)) goto blewit; else return 0;
    }
    for (int i=0; i<alsai.ndev; i++) {
        int channels = chindev[i];
        if (alsaio_setup(&alsai.dev[i], 0, &channels, &rate, nfrags, frag_size) < 0) goto blewit;
        inchans += channels;
    }
    for (int i=0; i<alsao.ndev; i++) {
        int channels = choutdev[i];
        if (alsaio_setup(&alsao.dev[i], 1, &channels, &rate, nfrags, frag_size) < 0) goto blewit;
        outchans += channels;
    }
    if (!inchans && !outchans) goto blewit;
    for (int i=0; i<alsai.ndev; i++) snd_pcm_prepare(alsai.dev[i].a_handle);
    for (int i=0; i<alsao.ndev; i++) snd_pcm_prepare(alsao.dev[i].a_handle);
    /* if duplex we can link the channels so they start together; however j is not used, so wtf */
    for (int i=0; i<alsai.ndev; i++) {
        //for (int j=0; j<alsao.ndev; j++) {
            if (alsai.dev[i].a_devno == alsao.dev[i].a_devno) {
                snd_pcm_link(alsai.dev[i].a_handle,alsao.dev[i].a_handle);
            }
        //}
    }
    /* allocate the status variables */
    if (!alsa_status) {
        err = snd_pcm_status_malloc(&alsa_status);
        check_error(err, "snd_pcm_status_malloc");
    }
    /* fill the buffer with silence */
    memset(alsa_snd_buf, 0, alsa_snd_bufsize);
    if (outchans) {
        i = (frag_size * nfrags)/sys_dacblocksize + 1;
        while (i--) {
            for (int i=0; i<alsao.ndev; i++)
                snd_pcm_writei(alsao.dev[i].a_handle, alsa_snd_buf, sys_dacblocksize);
        }
    } else if (inchans) {
        for (int i=0; i<alsai.ndev; i++)
            if ((err = snd_pcm_start(alsai.dev[i].a_handle)) < 0) check_error(err, "input start failed");
    }
    return 0;
blewit:
    sys_inchannels = 0;
    sys_outchannels = 0;
    alsa_close_audio();
    return 1;
}

static void alsa_close_audio() {
    int err;
    if (alsa_usemmap) {alsamm_close_audio(); return;}
    for (int i=0; i<alsai.ndev; i++) {
        err = snd_pcm_close(alsai.dev[i].a_handle);
        check_error(err, "snd_pcm_close (input)");
    }
    for (int i=0; i<alsao.ndev; i++) {
        err = snd_pcm_close(alsao.dev[i].a_handle);
        check_error(err, "snd_pcm_close (output)");
    }
    alsai.ndev = alsao.ndev = 0;
}

int alsa_send_dacs() {
#ifdef DEBUG_ALSA_XFER
    static int xferno = 0;
    static int callno = 0;
#endif
    static double timenow;
    double timelast;
    t_sample *fp1, *fp2;
    int j, k, iodev, result, ch;
    int chansintogo, chansouttogo;
    unsigned int transfersize;
    if (alsa_usemmap) return alsamm_send_dacs();
    if (!alsai.ndev && !alsao.ndev) return SENDDACS_NO;
    chansintogo = sys_inchannels;
    chansouttogo = sys_outchannels;
    transfersize = sys_dacblocksize;
    timelast = timenow;
    timenow = sys_getrealtime();
#ifdef DEBUG_ALSA_XFER
    if (timenow - timelast > 0.050) post("(%d)", int(1000 * (timenow - timelast)));
    callno++;
#endif
    alsa_checkiosync();     /* check I/O are in sync and data not late */
    for (int i=0; i<alsai.ndev; i++) {
        snd_pcm_status(alsai.dev[i].a_handle, alsa_status);
        if (snd_pcm_status_get_avail(alsa_status) < transfersize) return SENDDACS_NO;
    }
    for (int i=0; i<alsao.ndev; i++) {
        snd_pcm_status(alsao.dev[i].a_handle, alsa_status);
        if (snd_pcm_status_get_avail(alsa_status) < transfersize) return SENDDACS_NO;
    }
    /* do output */
    fp1 = sys_soundout; ch = 0;
    for (int iodev=0; iodev<alsao.ndev; iodev++) {
        int thisdevchans = alsao.dev[iodev].a_channels;
        int chans = min(chansouttogo,thisdevchans);
        chansouttogo -= chans;
	int i;
        if (alsao.dev[iodev].a_sampwidth == 4) {
            for (i=0; i<chans; i++, ch++, fp1 += sys_dacblocksize) {
	        fp2 = fp1;
                for (int j = ch, k = sys_dacblocksize; k--; j += thisdevchans, fp2++) {
                    float s1 = *fp2 * INT32_MAX;
                    ((t_alsa_sample32 *)alsa_snd_buf)[j] = CLIP32(int(s1));
		}
            }
            for (; i<thisdevchans; i++, ch++)
                for (int j = ch, k = sys_dacblocksize; k--; j += thisdevchans) ((t_alsa_sample32 *)alsa_snd_buf)[j] = 0;
        } else {
            for (i=0; i<chans; i++, ch++, fp1 += sys_dacblocksize) {
	        fp2=fp1;
                for (int j=ch, k=sys_dacblocksize; k--; j += thisdevchans, fp2++) {
                    int s = int(*fp2 * 32767.);
                    if (s > 32767) s = 32767; else if (s < -32767) s = -32767;
                    ((t_alsa_sample16 *)alsa_snd_buf)[j] = s;
		}
            }
            for (; i < thisdevchans; i++, ch++)
                for (int j = ch, k = sys_dacblocksize; k--; j += thisdevchans) ((t_alsa_sample16 *)alsa_snd_buf)[j] = 0;
        }
        result = snd_pcm_writei(alsao.dev[iodev].a_handle, alsa_snd_buf, transfersize);
        if (result != (int)transfersize) {
    #ifdef DEBUG_ALSA_XFER
            if (result >= 0 || errno == EAGAIN) post("ALSA: write returned %d of %d", result, transfersize);
            else error("ALSA: write: %s", snd_strerror(errno));
            post("inputcount %d, outputcount %d, outbufsize %d",
                    inputcount, outputcount, (ALSA_EXTRABUFFER + sys_advance_samples) * alsao.dev[iodev].a_sampwidth * outchannels);
    #endif
            sys_log_error(ERR_DACSLEPT);
            return SENDDACS_NO;
        }
        /* zero out the output buffer */
        memset(sys_soundout, 0, sys_dacblocksize * sizeof(*sys_soundout) * sys_outchannels);
        if (sys_getrealtime() - timenow > 0.002) {
    #ifdef DEBUG_ALSA_XFER
            post("output %d took %d msec", callno, int(1000 * (timenow - timelast)));
    #endif
            timenow = sys_getrealtime();
            sys_log_error(ERR_DACSLEPT);
        }
    }
    /* do input */
    for (iodev = 0, fp1 = sys_soundin, ch = 0; iodev < alsai.ndev; iodev++) {
        int thisdevchans = alsai.dev[iodev].a_channels;
        int chans = (chansintogo < thisdevchans ? chansintogo : thisdevchans);
        chansouttogo -= chans;
        result = snd_pcm_readi(alsai.dev[iodev].a_handle, alsa_snd_buf, transfersize);
        if (result < (int)transfersize) {
#ifdef DEBUG_ALSA_XFER
            if (result<0) error("snd_pcm_read %d %d: %s", callno, xferno, snd_strerror(errno));
            else post("snd_pcm_read %d %d returned only %d", callno, xferno, result);
            post("inputcount %d, outputcount %d, inbufsize %d",
                    inputcount, outputcount, (ALSA_EXTRABUFFER + sys_advance_samples) * alsai.dev[iodev].a_sampwidth * inchannels);
#endif
            sys_log_error(ERR_ADCSLEPT);
            return SENDDACS_NO;
        }
        if (alsai.dev[iodev].a_sampwidth == 4) {
            for (int i=0; i<chans; i++, ch++, fp1 += sys_dacblocksize) {
                for (j = ch, k = sys_dacblocksize, fp2 = fp1; k--; j += thisdevchans, fp2++)
                    *fp2 = (float) ((t_alsa_sample32 *)alsa_snd_buf)[j] * (1./ INT32_MAX);
            }
        } else {
            for (int i=0; i<chans; i++, ch++, fp1 += sys_dacblocksize) {
                for (j = ch, k = sys_dacblocksize, fp2 = fp1; k--; j += thisdevchans, fp2++)
                    *fp2 = (float) ((t_alsa_sample16 *)alsa_snd_buf)[j] * 3.051850e-05;
            }
        }
    }
#ifdef DEBUG_ALSA_XFER
    xferno++;
#endif
    if (sys_getrealtime() - timenow > 0.002) {
#ifdef DEBUG_ALSA_XFER
        post("routine took %d msec", int(1000 * (sys_getrealtime() - timenow)));
#endif
        sys_log_error(ERR_ADCSLEPT);
    }
    return SENDDACS_YES;
}

void alsa_printstate() {
    int result, i=0;
    snd_pcm_sframes_t indelay, outdelay;
    if (sys_audioapi != API_ALSA) {
        error("restart-audio: implemented for ALSA only.");
        return;
    }
    if (sys_inchannels) {
        result = snd_pcm_delay(alsai.dev[i].a_handle, &indelay);
        if (result<0) error("snd_pcm_delay 1 failed"); else post( "in delay %d", int( indelay));
    }
    if (sys_outchannels) {
        result = snd_pcm_delay(alsao.dev[i].a_handle, &outdelay);
        if (result<0) error("snd_pcm_delay 2 failed"); else post("out delay %d", int(outdelay));
    }
    post("sum %ld (%ld mod 64)", indelay + outdelay, (indelay+outdelay)%64);
    post("buf samples %d", alsa_buf_samps);
}


void alsa_putzeros(int iodev, int n) {
    memset(alsa_snd_buf, 0, alsao.dev[iodev].a_sampwidth * sys_dacblocksize * alsao.dev[iodev].a_channels);
    for (int i=0; i<n; i++) snd_pcm_writei(alsao.dev[iodev].a_handle, alsa_snd_buf, sys_dacblocksize);
}

void alsa_getzeros(int iodev, int n) {
    for (int i=0; i<n; i++) snd_pcm_readi(alsai.dev[iodev].a_handle, alsa_snd_buf, sys_dacblocksize);
}

/* call this only if both input and output are open */
static void alsa_checkiosync() {
    int result, giveup = 1000, alreadylogged = 0;
    snd_pcm_sframes_t minphase, maxphase, thisphase, outdelay;
    while (1) {
        if (giveup-- <= 0) {post("tried but couldn't sync A/D/A"); alsa_jittermax += 1; return;}
        minphase = 0x7fffffff;
        maxphase = -0x7fffffff;
        for (int i=0; i<alsao.ndev; i++) {
            result     = snd_pcm_delay(alsao.dev[i].a_handle, &outdelay);
            if (result < 0) {
                snd_pcm_prepare(alsao.dev[i].a_handle);
                result = snd_pcm_delay(alsao.dev[i].a_handle, &outdelay);
            }
            if (result<0) {
                error("output snd_pcm_delay failed: %s", snd_strerror(result));
                if (snd_pcm_status(alsao.dev[i].a_handle, alsa_status)<0) error("output snd_pcm_status failed");
                else post("astate %d", snd_pcm_status_get_state(alsa_status));
                return;
            }
            thisphase = alsa_buf_samps - outdelay;
            if (thisphase < minphase) minphase = thisphase;
            if (thisphase > maxphase) maxphase = thisphase;
            if (outdelay < 0)
                sys_log_error(ERR_DATALATE), alreadylogged = 1;
        }
        for (int i=0; i<alsai.ndev; i++) {
            result =     snd_pcm_delay(alsai.dev[i].a_handle, &thisphase);
            if (result < 0) {
                snd_pcm_prepare(alsai.dev[i].a_handle);
                result = snd_pcm_delay(alsai.dev[i].a_handle, &thisphase);
            }
            if (result < 0) {
                error("output snd_pcm_delay failed: %s", snd_strerror(result));
                if (snd_pcm_status(alsao.dev[i].a_handle, alsa_status) < 0) error("output snd_pcm_status failed");
                else post("astate %d", snd_pcm_status_get_state(alsa_status));
                return;
            }
            if (thisphase < minphase) minphase = thisphase;
            if (thisphase > maxphase) maxphase = thisphase;
        }
        /* the "correct" position is for all the phases to be exactly equal;
           but since we only make corrections sys_dacblocksize samples at a time,
           we just ask that the spread be not more than 3/4 of a block.  */
        if (maxphase <= minphase + (alsa_jittermax * (sys_dacblocksize / 4))) break;
        if (!alreadylogged) sys_log_error(ERR_RESYNC), alreadylogged = 1;
        for (int i=0; i<alsao.ndev; i++) {
            result = snd_pcm_delay(alsao.dev[i].a_handle, &outdelay);
            if (result < 0) break;
            thisphase = alsa_buf_samps - outdelay;
            if (thisphase > minphase + sys_dacblocksize) {
                alsa_putzeros(i,1);
#if DEBUGSYNC
                post("putz %d %d", (int)thisphase, (int)minphase);
#endif
            }
        }
        for (int i=0; i<alsai.ndev; i++) {
            result = snd_pcm_delay(alsai.dev[i].a_handle, &thisphase);
            if (result < 0) break;
            if (thisphase > minphase + sys_dacblocksize) {
                alsa_getzeros(i, 1);
#if DEBUGSYNC
                post("getz %d %d", (int)thisphase, (int)minphase);
#endif
            }
        }
    }
#if DEBUGSYNC
    if (alreadylogged) post("done");
#endif
}

static int   alsa_nnames = 0;
static char **alsa_names = 0;

void alsa_adddev(char *name) {
    if (alsa_nnames) alsa_names = (char **)t_resizebytes(alsa_names, alsa_nnames*sizeof(char *), (alsa_nnames+1)*sizeof(char *));
    else alsa_names = (char **)t_getbytes(sizeof(char *));
    alsa_names[alsa_nnames] = gensym(name)->s_name;
    alsa_nnames++;
}

static void alsa_numbertoname(int devno, char *devname, int nchar) {
    int ndev = 0, cardno = -1;
    while (!snd_card_next(&cardno) && cardno >= 0) ndev++;
    if (devno < 2*ndev) {
        if (devno & 1) snprintf(devname, nchar, "plughw:%d", devno/2);
        else snprintf(devname, nchar, "hw:%d", devno/2);
    } else if (devno <2*ndev + alsa_nnames)
        snprintf(devname, nchar, "%s", alsa_names[devno - 2*ndev]);
    else snprintf(devname, nchar, "???");
}

/* For each hardware card found, we list two devices, the "hard" and
   "plug" one.  The card scan is derived from portaudio code. */
static void alsa_getdevs(char *indevlist, int *nindevs, char *outdevlist, int *noutdevs, int *canmulti, int maxndev, int devdescsize) {
    int ndev = 0, cardno = -1, i, j;
    *canmulti = 2;  /* supports multiple devices */
    while (!snd_card_next(&cardno) && cardno >= 0) {
        snd_ctl_t *ctl;
        snd_ctl_card_info_t *info;
        char devname[80];
        char *desc;
        if (2*ndev + 2 > maxndev) break;
        /* apparently, "cardno" is just a counter; but check that here */
        if (ndev != cardno) post("oops: ALSA cards not reported in order?");
        sprintf(devname, "hw:%d", cardno);
        /* post("try %s..", devname); */
        if (snd_ctl_open(&ctl, devname, 0) >= 0) {
            snd_ctl_card_info_malloc(&info);
            snd_ctl_card_info(ctl, info);
            desc = strdup(snd_ctl_card_info_get_name(info));
            snd_ctl_card_info_free(info);
        } else {
            error("ALSA card scan error");
            desc = strdup("???");
        }
        sprintf(indevlist  +  2*ndev    * devdescsize, "%s (hardware)", desc);
        sprintf(indevlist  + (2*ndev+1) * devdescsize, "%s (plug-in)",  desc);
        sprintf(outdevlist +  2*ndev    * devdescsize, "%s (hardware)", desc);
        sprintf(outdevlist + (2*ndev+1) * devdescsize, "%s (plug-in)",  desc);
        ndev++;
        free(desc);
    }
    for (i=0, j=2*ndev; i<alsa_nnames; i++, j++) {
        if (j >= maxndev) break;
        snprintf(indevlist + j * devdescsize, devdescsize, "%s", alsa_names[i]);
    }
    *nindevs = *noutdevs = j;
}

struct t_audioapi alsa_api = {
	alsa_open_audio,
	alsa_close_audio,
	alsa_send_dacs,
	alsa_getdevs,
};