/*
 * Sample Wine Driver for Advanced Linux Sound System (ALSA)
 *      Based on version <final> of the ALSA API
 *
 * This file performs the initialisation and scanning of the sound subsystem.
 *
 * Copyright    2002 Eric Pouech
 *              2002 Marco Pietrobono
 *              2003 Christian Costa : WaveIn support
 *              2006-2007 Maarten Lankhorst
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
 */

#include "config.h"

#ifdef HAVE_ALSA

#include "wine/port.h"

#include <stdlib.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#ifdef HAVE_UNISTD_H
# include <unistd.h>
#endif
#include <errno.h>
#include <limits.h>
#include <fcntl.h>
#ifdef HAVE_SYS_IOCTL_H
# include <sys/ioctl.h>
#endif
#ifdef HAVE_SYS_MMAN_H
# include <sys/mman.h>
#endif
#include "windef.h"
#include "winbase.h"
#include "wingdi.h"
#include "winerror.h"
#include "winuser.h"
#include "winnls.h"
#include "winreg.h"
#include "mmddk.h"

/* ksmedia.h defines KSDATAFORMAT_SUBTYPE_PCM and KSDATAFORMAT_SUBTYPE_IEEE_FLOAT
 * However either all files that use it will define it, or no files will
 * The only way to solve this is by adding initguid.h here, and include the guid that way
 */
#include "initguid.h"
#include "alsa.h"

#include "wine/library.h"
#include "wine/unicode.h"
#include "wine/debug.h"

WINE_DEFAULT_DEBUG_CHANNEL(wave);

/*----------------------------------------------------------------------------
**  ALSA_TestDeviceForWine
**
**      Test to see if a given device is sufficient for Wine.
*/
static int ALSA_TestDeviceForWine(int card, int device,  snd_pcm_stream_t streamtype)
{
    snd_pcm_t *pcm = NULL;
    char pcmname[256];
    int retcode;
    snd_pcm_hw_params_t *hwparams;
    const char *reason = NULL;
    unsigned int rrate;

    hwparams = HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, snd_pcm_hw_params_sizeof() );

    /* Note that the plug: device masks out a lot of info, we want to avoid that */
    sprintf(pcmname, "hw:%d,%d", card, device);
    retcode = snd_pcm_open(&pcm, pcmname, streamtype, SND_PCM_NONBLOCK);
    if (retcode < 0)
    {
        /* Note that a busy device isn't automatically disqualified */
        if (retcode == (-1 * EBUSY))
            retcode = 0;
        goto exit;
    }

    retcode = snd_pcm_hw_params_any(pcm, hwparams);
    if (retcode < 0)
    {
        reason = "Could not retrieve hw_params";
        goto exit;
    }

    /* set the count of channels */
    retcode = snd_pcm_hw_params_set_channels(pcm, hwparams, 2);
    if (retcode < 0)
    {
        reason = "Could not set channels";
        goto exit;
    }

    rrate = 44100;
    retcode = snd_pcm_hw_params_set_rate_near(pcm, hwparams, &rrate, 0);
    if (retcode < 0)
    {
        reason = "Could not set rate";
        goto exit;
    }

    if (rrate == 0)
    {
        reason = "Rate came back as 0";
        goto exit;
    }

    /* write the parameters to device */
    retcode = snd_pcm_hw_params(pcm, hwparams);
    if (retcode < 0)
    {
        reason = "Could not set hwparams";
        goto exit;
    }

    retcode = 0;

exit:
    if (pcm)
        snd_pcm_close(pcm);
    HeapFree( GetProcessHeap(), 0, hwparams );

    if (retcode != 0 && retcode != (-1 * ENOENT))
        TRACE("Discarding card %d/device %d:  %s [%d(%s)]\n", card, device, reason, retcode, snd_strerror(retcode));

    return retcode;
}

/*----------------------------------------------------------------------------
** ALSA_RegGetString
**  Retrieve a string from a registry key
*/
static int ALSA_RegGetString(HKEY key, const char *value, char **bufp)
{
    DWORD rc;
    DWORD type;
    DWORD bufsize;

    *bufp = NULL;
    rc = RegQueryValueExA(key, value, NULL, &type, NULL, &bufsize);
    if (rc != ERROR_SUCCESS)
        return(rc);

    if (type != REG_SZ)
        return 1;

    *bufp = HeapAlloc(GetProcessHeap(), 0, bufsize);
    if (! *bufp)
        return 1;

    rc = RegQueryValueExA(key, value, NULL, NULL, (LPBYTE)*bufp, &bufsize);
    return rc;
}

/*----------------------------------------------------------------------------
** ALSA_RegGetBoolean
**  Get a string and interpret it as a boolean
*/

/* Possible truths:
   Y(es), T(rue), 1, E(nabled) */

#define IS_OPTION_TRUE(ch) ((ch) == 'y' || (ch) == 'Y' || (ch) == 't' || (ch) == 'T' || (ch) == '1' || (ch) == 'e' || (ch) == 'E')
static int ALSA_RegGetBoolean(HKEY key, const char *value, BOOL *answer)
{
    DWORD rc;
    char *buf = NULL;

    rc = ALSA_RegGetString(key, value, &buf);
    if (buf)
    {
        *answer = FALSE;
        if (IS_OPTION_TRUE(*buf))
            *answer = TRUE;

        HeapFree(GetProcessHeap(), 0, buf);
    }

    return rc;
}

/*----------------------------------------------------------------------------
** ALSA_RegGetInt
**  Get a string and interpret it as a DWORD
*/
static int ALSA_RegGetInt(HKEY key, const char *value, DWORD *answer)
{
    DWORD rc;
    char *buf = NULL;

    rc = ALSA_RegGetString(key, value, &buf);
    if (buf)
    {
        *answer = atoi(buf);
        HeapFree(GetProcessHeap(), 0, buf);
    }

    return rc;
}

/* return a string duplicated on the win32 process heap, free with HeapFree */
static char* ALSA_strdup(const char *s) {
    char *result = HeapAlloc(GetProcessHeap(), 0, strlen(s)+1);
    if (!result)
        return NULL;
    strcpy(result, s);
    return result;
}

#define ALSA_RETURN_ONFAIL(mycall)                                      \
{                                                                       \
    int rc;                                                             \
    {rc = mycall;}                                                      \
    if ((rc) < 0)                                                       \
    {                                                                   \
        ERR("%s failed:  %s(%d)\n", #mycall, snd_strerror(rc), rc);     \
        return(rc);                                                     \
    }                                                                   \
}

/*----------------------------------------------------------------------------
**  ALSA_ComputeCaps
**
**      Given an ALSA PCM, figure out our HW CAPS structure info.
**  ctl can be null, pcm is required, as is all output parms.
**
*/
static int ALSA_ComputeCaps(snd_ctl_t *ctl, snd_pcm_t *pcm,
        WORD *channels, DWORD *flags, DWORD *formats, DWORD *supports)
{
    snd_pcm_hw_params_t *hw_params;
    snd_pcm_format_mask_t *fmask;
    snd_pcm_access_mask_t *acmask;
    unsigned int ratemin = 0;
    unsigned int ratemax = 0;
    unsigned int chmin = 0;
    unsigned int chmax = 0;
    int rc, dir = 0;

    hw_params = HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, snd_pcm_hw_params_sizeof() );
    fmask = HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, snd_pcm_format_mask_sizeof() );
    acmask = HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, snd_pcm_access_mask_sizeof() );

    if ((rc = snd_pcm_hw_params_any(pcm, hw_params)) < 0) goto done;

    snd_pcm_hw_params_get_format_mask(hw_params, fmask);

    if ((rc = snd_pcm_hw_params_get_access_mask(hw_params, acmask)) < 0) goto done;

    if ((rc = snd_pcm_hw_params_get_rate_min(hw_params, &ratemin, &dir)) < 0) goto done;
    if ((rc = snd_pcm_hw_params_get_rate_max(hw_params, &ratemax, &dir)) < 0) goto done;
    if ((rc = snd_pcm_hw_params_get_channels_min(hw_params, &chmin)) < 0) goto done;
    if ((rc = snd_pcm_hw_params_get_channels_max(hw_params, &chmax)) < 0) goto done;

#define X(r,v) \
    if ( (r) >= ratemin && ( (r) <= ratemax || ratemax == -1) ) \
    { \
       if (snd_pcm_format_mask_test( fmask, SND_PCM_FORMAT_U8)) \
       { \
          if (chmin <= 1 && 1 <= chmax) \
              *formats |= WAVE_FORMAT_##v##M08; \
          if (chmin <= 2 && 2 <= chmax) \
              *formats |= WAVE_FORMAT_##v##S08; \
       } \
       if (snd_pcm_format_mask_test( fmask, SND_PCM_FORMAT_S16_LE)) \
       { \
          if (chmin <= 1 && 1 <= chmax) \
              *formats |= WAVE_FORMAT_##v##M16; \
          if (chmin <= 2 && 2 <= chmax) \
              *formats |= WAVE_FORMAT_##v##S16; \
       } \
    }
    X(11025,1);
    X(22050,2);
    X(44100,4);
    X(48000,48);
    X(96000,96);
#undef X

    if (chmin > 1)
        FIXME("Device has a minimum of %d channels\n", chmin);
    *channels = chmax;

    /* FIXME: is sample accurate always true ?
    ** Can we do WAVECAPS_PITCH, WAVECAPS_SYNC, or WAVECAPS_PLAYBACKRATE? */
    *supports |= WAVECAPS_SAMPLEACCURATE;

    /* FIXME: NONITERLEAVED and COMPLEX are not supported right now */
    if ( snd_pcm_access_mask_test( acmask, SND_PCM_ACCESS_MMAP_INTERLEAVED ) )
        *supports |= WAVECAPS_DIRECTSOUND;

    /* check for volume control support */
    if (ctl) {
        if (snd_ctl_name(ctl))
        {
            snd_hctl_t *hctl;
            if (snd_hctl_open(&hctl, snd_ctl_name(ctl), 0) >= 0)
            {
                snd_hctl_load(hctl);
                if (!ALSA_CheckSetVolume( hctl, NULL, NULL, NULL, NULL, NULL, NULL, NULL ))
                {
                    *supports |= WAVECAPS_VOLUME;
                    if (chmin <= 2 && 2 <= chmax)
                        *supports |= WAVECAPS_LRVOLUME;
                }
                snd_hctl_free(hctl);
                snd_hctl_close(hctl);
            }
        }
    }

    *flags = DSCAPS_CERTIFIED | DSCAPS_CONTINUOUSRATE;
    *flags |= DSCAPS_SECONDARYMONO | DSCAPS_SECONDARYSTEREO;
    *flags |= DSCAPS_SECONDARY8BIT | DSCAPS_SECONDARY16BIT;

    if (*formats & (WAVE_FORMAT_1M08  | WAVE_FORMAT_2M08  |
                               WAVE_FORMAT_4M08  | WAVE_FORMAT_48M08 |
                               WAVE_FORMAT_96M08 | WAVE_FORMAT_1M16  |
                               WAVE_FORMAT_2M16  | WAVE_FORMAT_4M16  |
                               WAVE_FORMAT_48M16 | WAVE_FORMAT_96M16) )
        *flags |= DSCAPS_PRIMARYMONO;

    if (*formats & (WAVE_FORMAT_1S08  | WAVE_FORMAT_2S08  |
                               WAVE_FORMAT_4S08  | WAVE_FORMAT_48S08 |
                               WAVE_FORMAT_96S08 | WAVE_FORMAT_1S16  |
                               WAVE_FORMAT_2S16  | WAVE_FORMAT_4S16  |
                               WAVE_FORMAT_48S16 | WAVE_FORMAT_96S16) )
        *flags |= DSCAPS_PRIMARYSTEREO;

    if (*formats & (WAVE_FORMAT_1M08  | WAVE_FORMAT_2M08  |
                               WAVE_FORMAT_4M08  | WAVE_FORMAT_48M08 |
                               WAVE_FORMAT_96M08 | WAVE_FORMAT_1S08  |
                               WAVE_FORMAT_2S08  | WAVE_FORMAT_4S08  |
                               WAVE_FORMAT_48S08 | WAVE_FORMAT_96S08) )
        *flags |= DSCAPS_PRIMARY8BIT;

    if (*formats & (WAVE_FORMAT_1M16  | WAVE_FORMAT_2M16  |
                               WAVE_FORMAT_4M16  | WAVE_FORMAT_48M16 |
                               WAVE_FORMAT_96M16 | WAVE_FORMAT_1S16  |
                               WAVE_FORMAT_2S16  | WAVE_FORMAT_4S16  |
                               WAVE_FORMAT_48S16 | WAVE_FORMAT_96S16) )
        *flags |= DSCAPS_PRIMARY16BIT;

    rc = 0;

done:
    if (rc < 0) ERR("failed: %s(%d)\n", snd_strerror(rc), rc);
    HeapFree( GetProcessHeap(), 0, hw_params );
    HeapFree( GetProcessHeap(), 0, fmask );
    HeapFree( GetProcessHeap(), 0, acmask );
    return rc;
}

/*----------------------------------------------------------------------------
**  ALSA_AddCommonDevice
**
**      Perform Alsa initialization common to both capture and playback
**
**  Side Effect:  ww->pcname and ww->ctlname may need to be freed.
**
**  Note:  this was originally coded by using snd_pcm_name(pcm), until
**         I discovered that with at least one version of alsa lib,
**         the use of a pcm named default:0 would cause snd_pcm_name() to fail.
**         So passing the name in is logically extraneous.  Sigh.
*/
static int ALSA_AddCommonDevice(snd_ctl_t *ctl, snd_pcm_t *pcm, const char *pcmname, WINE_WAVEDEV *ww)
{
    snd_pcm_info_t *infop;
    int rc;

    infop = HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, snd_pcm_info_sizeof() );
    if ((rc = snd_pcm_info(pcm, infop)) < 0)
    {
        HeapFree( GetProcessHeap(), 0, infop );
        return rc;
    }

    if (pcm && pcmname)
        ww->pcmname = ALSA_strdup(pcmname);
    else
    {
        HeapFree( GetProcessHeap(), 0, infop );
        return -1;
    }

    if (ctl && snd_ctl_name(ctl))
        ww->ctlname = ALSA_strdup(snd_ctl_name(ctl));

    strcpy(ww->interface_name, "winealsa: ");
    memcpy(ww->interface_name + strlen(ww->interface_name),
            ww->pcmname,
            min(strlen(ww->pcmname), sizeof(ww->interface_name) - strlen("winealsa:   ")));

    strcpy(ww->ds_desc.szDrvname, "winealsa.drv");

    memcpy(ww->ds_desc.szDesc, snd_pcm_info_get_name(infop),
            min( (sizeof(ww->ds_desc.szDesc) - 1), strlen(snd_pcm_info_get_name(infop))) );

    ww->ds_caps.dwMinSecondarySampleRate = DSBFREQUENCY_MIN;
    ww->ds_caps.dwMaxSecondarySampleRate = DSBFREQUENCY_MAX;
    ww->ds_caps.dwPrimaryBuffers = 1;

    HeapFree( GetProcessHeap(), 0, infop );
    return 0;
}

/*----------------------------------------------------------------------------
** ALSA_FreeDevice
*/
static void ALSA_FreeDevice(WINE_WAVEDEV *ww)
{
    HeapFree(GetProcessHeap(), 0, ww->pcmname);
    ww->pcmname = NULL;

    HeapFree(GetProcessHeap(), 0, ww->ctlname);
    ww->ctlname = NULL;
}

/*----------------------------------------------------------------------------
**  ALSA_AddDeviceToArray
**
**      Dynamically size one of the wavein or waveout arrays of devices,
**  and add a fully configured device node to the array.
**
*/
static int ALSA_AddDeviceToArray(WINE_WAVEDEV *ww, WINE_WAVEDEV **array,
        DWORD *count, DWORD *alloced, int isdefault)
{
    int i = *count;

    if (*count >= *alloced)
    {
        (*alloced) += WAVEDEV_ALLOC_EXTENT_SIZE;
        if (! (*array))
            *array = HeapAlloc(GetProcessHeap(), 0, sizeof(*ww) * (*alloced));
        else
            *array = HeapReAlloc(GetProcessHeap(), 0, *array, sizeof(*ww) * (*alloced));

        if (!*array)
        {
            return -1;
        }
    }

    /* If this is the default, arrange for it to be the first element */
    if (isdefault && i > 0)
    {
        (*array)[*count] = (*array)[0];
        i = 0;
    }

    (*array)[i] = *ww;

    (*count)++;
    return 0;
}

/*----------------------------------------------------------------------------
**  ALSA_AddPlaybackDevice
**
**      Add a given Alsa device to Wine's internal list of Playback
**  devices.
*/
static int ALSA_AddPlaybackDevice(snd_ctl_t *ctl, snd_pcm_t *pcm, const char *pcmname, int isdefault)
{
    WINE_WAVEDEV    wwo;
    int rc;

    memset(&wwo, '\0', sizeof(wwo));

    rc = ALSA_AddCommonDevice(ctl, pcm, pcmname, &wwo);
    if (rc)
        return(rc);

    MultiByteToWideChar(CP_UNIXCP, 0, wwo.ds_desc.szDesc, -1,
                        wwo.outcaps.szPname, sizeof(wwo.outcaps.szPname)/sizeof(WCHAR));
    wwo.outcaps.szPname[sizeof(wwo.outcaps.szPname)/sizeof(WCHAR) - 1] = '\0';

    wwo.outcaps.wMid = MM_CREATIVE;
    wwo.outcaps.wPid = MM_CREATIVE_SBP16_WAVEOUT;
    wwo.outcaps.vDriverVersion = 0x0100;

    rc = ALSA_ComputeCaps(ctl, pcm, &wwo.outcaps.wChannels, &wwo.ds_caps.dwFlags,
            &wwo.outcaps.dwFormats, &wwo.outcaps.dwSupport);
    if (rc)
    {
        WARN("Error calculating device caps for pcm [%s]\n", wwo.pcmname);
        ALSA_FreeDevice(&wwo);
        return(rc);
    }

    rc = ALSA_AddDeviceToArray(&wwo, &WOutDev, &ALSA_WodNumDevs, &ALSA_WodNumMallocedDevs, isdefault);
    if (rc)
        ALSA_FreeDevice(&wwo);
    return (rc);
}

/*----------------------------------------------------------------------------
**  ALSA_AddCaptureDevice
**
**      Add a given Alsa device to Wine's internal list of Capture
**  devices.
*/
static int ALSA_AddCaptureDevice(snd_ctl_t *ctl, snd_pcm_t *pcm, const char *pcmname, int isdefault)
{
    WINE_WAVEDEV    wwi;
    int rc;

    memset(&wwi, '\0', sizeof(wwi));

    rc = ALSA_AddCommonDevice(ctl, pcm, pcmname, &wwi);
    if (rc)
        return(rc);

    MultiByteToWideChar(CP_UNIXCP, 0, wwi.ds_desc.szDesc, -1,
                        wwi.incaps.szPname, sizeof(wwi.incaps.szPname) / sizeof(WCHAR));
    wwi.incaps.szPname[sizeof(wwi.incaps.szPname)/sizeof(WCHAR) - 1] = '\0';

    wwi.incaps.wMid = MM_CREATIVE;
    wwi.incaps.wPid = MM_CREATIVE_SBP16_WAVEOUT;
    wwi.incaps.vDriverVersion = 0x0100;

    rc = ALSA_ComputeCaps(ctl, pcm, &wwi.incaps.wChannels, &wwi.ds_caps.dwFlags,
            &wwi.incaps.dwFormats, &wwi.dwSupport);
    if (rc)
    {
        WARN("Error calculating device caps for pcm [%s]\n", wwi.pcmname);
        ALSA_FreeDevice(&wwi);
        return(rc);
    }

    rc = ALSA_AddDeviceToArray(&wwi, &WInDev, &ALSA_WidNumDevs, &ALSA_WidNumMallocedDevs, isdefault);
    if (rc)
        ALSA_FreeDevice(&wwi);
    return(rc);
}

/*----------------------------------------------------------------------------
**  ALSA_CheckEnvironment
**
**      Given an Alsa style configuration node, scan its subitems
**  for environment variable names, and use them to find an override,
**  if appropriate.
**      This is essentially a long and convoluted way of doing:
**          getenv("ALSA_CARD")
**          getenv("ALSA_CTL_CARD")
**          getenv("ALSA_PCM_CARD")
**          getenv("ALSA_PCM_DEVICE")
**
**  The output value is set with the atoi() of the first environment
**  variable found to be set, if any; otherwise, it is left alone
*/
static void ALSA_CheckEnvironment(snd_config_t *node, int *outvalue)
{
    snd_config_iterator_t iter;

    for (iter = snd_config_iterator_first(node);
         iter != snd_config_iterator_end(node);
         iter = snd_config_iterator_next(iter))
    {
        snd_config_t *leaf = snd_config_iterator_entry(iter);
        if (snd_config_get_type(leaf) == SND_CONFIG_TYPE_STRING)
        {
            const char *value;
            if (snd_config_get_string(leaf, &value) >= 0)
            {
                char *p = getenv(value);
                if (p)
                {
                    *outvalue = atoi(p);
                    return;
                }
            }
        }
    }
}

/*----------------------------------------------------------------------------
**  ALSA_DefaultDevices
**
**      Jump through Alsa style hoops to (hopefully) properly determine
**  Alsa defaults for CTL Card #, as well as for PCM Card + Device #.
**  We'll also find out if the user has set any of the environment
**  variables that specify we're to use a specific card or device.
**
**  Parameters:
**      directhw        Whether to use a direct hardware device or not;
**                      essentially switches the pcm device name from
**                      one of 'default:X' or 'plughw:X' to "hw:X"
**      defctlcard      If !NULL, will hold the ctl card number given
**                      by the ALSA config as the default
**      defpcmcard      If !NULL, default pcm card #
**      defpcmdev       If !NULL, default pcm device #
**      fixedctlcard    If !NULL, and the user set the appropriate
**                          environment variable, we'll set to the
**                          card the user specified.
**      fixedpcmcard    If !NULL, and the user set the appropriate
**                          environment variable, we'll set to the
**                          card the user specified.
**      fixedpcmdev     If !NULL, and the user set the appropriate
**                          environment variable, we'll set to the
**                          device the user specified.
**
**  Returns:  0 on success, < 0 on failure
*/
static int ALSA_DefaultDevices(int directhw,
            long *defctlcard,
            long *defpcmcard, long *defpcmdev,
            int *fixedctlcard,
            int *fixedpcmcard, int *fixedpcmdev)
{
    snd_config_t   *configp;
    char pcmsearch[256];

    ALSA_RETURN_ONFAIL(snd_config_update());

    if (defctlcard)
        if (snd_config_search(snd_config, "defaults.ctl.card", &configp) >= 0)
            snd_config_get_integer(configp, defctlcard);

    if (defpcmcard)
        if (snd_config_search(snd_config, "defaults.pcm.card", &configp) >= 0)
            snd_config_get_integer(configp, defpcmcard);

    if (defpcmdev)
        if (snd_config_search(snd_config, "defaults.pcm.device", &configp) >= 0)
            snd_config_get_integer(configp, defpcmdev);


    if (fixedctlcard)
    {
        if (snd_config_search(snd_config, "ctl.hw.@args.CARD.default.vars", &configp) >= 0)
            ALSA_CheckEnvironment(configp, fixedctlcard);
    }

    if (fixedpcmcard)
    {
        sprintf(pcmsearch, "pcm.%s.@args.CARD.default.vars", directhw ? "hw" : "plughw");
        if (snd_config_search(snd_config, pcmsearch, &configp) >= 0)
            ALSA_CheckEnvironment(configp, fixedpcmcard);
    }

    if (fixedpcmdev)
    {
        sprintf(pcmsearch, "pcm.%s.@args.DEV.default.vars", directhw ? "hw" : "plughw");
        if (snd_config_search(snd_config, pcmsearch, &configp) >= 0)
            ALSA_CheckEnvironment(configp, fixedpcmdev);
    }

    return 0;
}


/*----------------------------------------------------------------------------
**  ALSA_ScanDevices
**
**      Iterate through all discoverable ALSA cards, searching
**  for usable PCM devices.
**
**  Parameters:
**      directhw        Whether to use a direct hardware device or not;
**                      essentially switches the pcm device name from
**                      one of 'default:X' or 'plughw:X' to "hw:X"
**      defctlcard      Alsa's notion of the default ctl card.
**      defpcmcard         . pcm card
**      defpcmdev          . pcm device
**      fixedctlcard    If not -1, then gives the value of ALSA_CTL_CARD
**                          or equivalent environment variable
**      fixedpcmcard    If not -1, then gives the value of ALSA_PCM_CARD
**                          or equivalent environment variable
**      fixedpcmdev     If not -1, then gives the value of ALSA_PCM_DEVICE
**                          or equivalent environment variable
**
**  Returns:  0 on success, < 0 on failure
*/
static int ALSA_ScanDevices(int directhw,
        long defctlcard, long defpcmcard, long defpcmdev,
        int fixedctlcard, int fixedpcmcard, int fixedpcmdev)
{
    int card = fixedpcmcard;
    int scan_devices = (fixedpcmdev == -1);

    /*------------------------------------------------------------------------
    ** Loop through all available cards
    **----------------------------------------------------------------------*/
    if (card == -1)
        snd_card_next(&card);

    for (; card != -1; snd_card_next(&card))
    {
        char ctlname[256];
        snd_ctl_t *ctl;
        int rc;
        int device;

        /*--------------------------------------------------------------------
        ** Try to open a ctl handle; Wine doesn't absolutely require one,
        **  but it does allow for volume control and for device scanning
        **------------------------------------------------------------------*/
        sprintf(ctlname, "hw:%d", fixedctlcard == -1 ? card : fixedctlcard);
        rc = snd_ctl_open(&ctl, ctlname, SND_CTL_NONBLOCK);
        if (rc < 0)
        {
            ctl = NULL;
            WARN("Unable to open an alsa ctl for [%s] (pcm card %d): %s; not scanning devices\n",
                    ctlname, card, snd_strerror(rc));
            if (fixedpcmdev == -1)
                fixedpcmdev = 0;
        }

        /*--------------------------------------------------------------------
        ** Loop through all available devices on this card
        **------------------------------------------------------------------*/
        device = fixedpcmdev;
        if (device == -1)
            snd_ctl_pcm_next_device(ctl, &device);

        for (; device != -1; snd_ctl_pcm_next_device(ctl, &device))
        {
            char defaultpcmname[256];
            char plugpcmname[256];
            char hwpcmname[256];
            char *pcmname = NULL;
            snd_pcm_t *pcm;

            sprintf(defaultpcmname, "default");
            sprintf(plugpcmname,    "plughw:%d,%d", card, device);
            sprintf(hwpcmname,      "hw:%d,%d", card, device);

            /*----------------------------------------------------------------
            ** See if it's a valid playback device
            **--------------------------------------------------------------*/
            if (ALSA_TestDeviceForWine(card, device, SND_PCM_STREAM_PLAYBACK) == 0)
            {
                /* If we can, try the default:X device name first */
                if (! scan_devices && ! directhw)
                {
                    pcmname = defaultpcmname;
                    rc = snd_pcm_open(&pcm, pcmname, SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK);
                }
                else
                    rc = -1;

                if (rc < 0)
                {
                    pcmname = directhw ? hwpcmname : plugpcmname;
                    rc = snd_pcm_open(&pcm, pcmname, SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK);
                }

                if (rc >= 0)
                {
                    if (defctlcard == card && defpcmcard == card && defpcmdev == device)
                        ALSA_AddPlaybackDevice(ctl, pcm, pcmname, TRUE);
                    else
                        ALSA_AddPlaybackDevice(ctl, pcm, pcmname, FALSE);
                    snd_pcm_close(pcm);
                }
                else
                {
                    TRACE("Device [%s/%s] failed to open for playback: %s\n",
                        directhw || scan_devices ? "(N/A)" : defaultpcmname,
                        directhw ? hwpcmname : plugpcmname,
                        snd_strerror(rc));
                }
            }

            /*----------------------------------------------------------------
            ** See if it's a valid capture device
            **--------------------------------------------------------------*/
            if (ALSA_TestDeviceForWine(card, device, SND_PCM_STREAM_CAPTURE) == 0)
            {
                /* If we can, try the default:X device name first */
                if (! scan_devices && ! directhw)
                {
                    pcmname = defaultpcmname;
                    rc = snd_pcm_open(&pcm, pcmname, SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK);
                }
                else
                    rc = -1;

                if (rc < 0)
                {
                    pcmname = directhw ? hwpcmname : plugpcmname;
                    rc = snd_pcm_open(&pcm, pcmname, SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK);
                }

                if (rc >= 0)
                {
                    if (defctlcard == card && defpcmcard == card && defpcmdev == device)
                        ALSA_AddCaptureDevice(ctl, pcm, pcmname, TRUE);
                    else
                        ALSA_AddCaptureDevice(ctl, pcm, pcmname, FALSE);

                    snd_pcm_close(pcm);
                }
                else
                {
                    TRACE("Device [%s/%s] failed to open for capture: %s\n",
                        directhw || scan_devices ? "(N/A)" : defaultpcmname,
                        directhw ? hwpcmname : plugpcmname,
                        snd_strerror(rc));
                }
            }

            if (! scan_devices)
                break;
        }

        if (ctl)
            snd_ctl_close(ctl);

        /*--------------------------------------------------------------------
        ** If the user has set env variables such that we're pegged to
        **  a specific card, then break after we've examined it
        **------------------------------------------------------------------*/
        if (fixedpcmcard != -1)
            break;
    }

    return 0;

}

/*----------------------------------------------------------------------------
** ALSA_PerformDefaultScan
**  Perform the basic default scanning for devices within ALSA.
**  The hope is that this routine implements a 'correct'
**  scanning algorithm from the Alsalib point of view.
**
**      Note that Wine, overall, has other mechanisms to
**  override and specify exact CTL and PCM device names,
**  but this routine is imagined as the default that
**  99% of users will use.
**
**      The basic algorithm is simple:
**  Use snd_card_next to iterate cards; within cards, use
**  snd_ctl_pcm_next_device to iterate through devices.
**
**      We add a little complexity by taking into consideration
**  environment variables such as ALSA_CARD (et all), and by
**  detecting when a given device matches the default specified
**  by Alsa.
**
**  Parameters:
**      directhw        If !0, indicates we should use the hw:X
**                      PCM interface, rather than first try
**                      the 'default' device followed by the plughw
**                      device.  (default and plughw do fancy mixing
**                      and audio scaling, if they are available).
**      devscan         If TRUE, we should scan all devices, not
**                      juse use device 0 on each card
**
**  Returns:
**      0   on success
**
**  Effects:
**      Invokes the ALSA_AddXXXDevice functions on valid
**  looking devices
*/
static int ALSA_PerformDefaultScan(int directhw, BOOL devscan)
{
    long defctlcard = -1, defpcmcard = -1, defpcmdev = -1;
    int fixedctlcard = -1, fixedpcmcard = -1, fixedpcmdev = -1;
    int rc;

    /* FIXME:  We should dlsym the new snd_names_list/snd_names_list_free 1.0.9 apis,
    **          and use them instead of this scan mechanism if they are present         */

    rc = ALSA_DefaultDevices(directhw, &defctlcard, &defpcmcard, &defpcmdev,
            &fixedctlcard, &fixedpcmcard, &fixedpcmdev);
    if (rc)
        return(rc);

    if (fixedpcmdev == -1 && ! devscan)
        fixedpcmdev = 0;

    return(ALSA_ScanDevices(directhw, defctlcard, defpcmcard, defpcmdev, fixedctlcard, fixedpcmcard, fixedpcmdev));
}


/*----------------------------------------------------------------------------
** ALSA_AddUserSpecifiedDevice
**  Add a device given from the registry
*/
static int ALSA_AddUserSpecifiedDevice(const char *ctlname, const char *pcmname)
{
    int rc;
    int okay = 0;
    snd_ctl_t *ctl = NULL;
    snd_pcm_t *pcm = NULL;

    if (ctlname)
    {
        rc = snd_ctl_open(&ctl, ctlname, SND_CTL_NONBLOCK);
        if (rc < 0)
            ctl = NULL;
    }

    rc = snd_pcm_open(&pcm, pcmname, SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK);
    if (rc >= 0)
    {
        ALSA_AddPlaybackDevice(ctl, pcm, pcmname, FALSE);
        okay++;
        snd_pcm_close(pcm);
    }

    rc = snd_pcm_open(&pcm, pcmname, SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK);
    if (rc >= 0)
    {
        ALSA_AddCaptureDevice(ctl, pcm, pcmname, FALSE);
        okay++;
        snd_pcm_close(pcm);
    }

    if (ctl)
        snd_ctl_close(ctl);

    return (okay == 0);
}


/*----------------------------------------------------------------------------
** ALSA_WaveInit
**  Initialize the Wine Alsa sub system.
** The main task is to probe for and store a list of all appropriate playback
** and capture devices.
**  Key control points are from the registry key:
**  [Software\Wine\Alsa Driver]
**  AutoScanCards           Whether or not to scan all known sound cards
**                          and add them to Wine's list (default yes)
**  AutoScanDevices         Whether or not to scan all known PCM devices
**                          on each card (default no)
**  UseDirectHW             Whether or not to use the hw:X device,
**                          instead of the fancy default:X or plughw:X device.
**                          The hw:X device goes straight to the hardware
**                          without any fancy mixing or audio scaling in between.
**  DeviceCount             If present, specifies the number of hard coded
**                          Alsa devices to add to Wine's list; default 0
**  DevicePCMn              Specifies the Alsa PCM devices to open for
**                          Device n (where n goes from 1 to DeviceCount)
**  DeviceCTLn              Specifies the Alsa control devices to open for
**                          Device n (where n goes from 1 to DeviceCount)
**
**                          Using AutoScanCards no, and then Devicexxx info
**                          is a way to exactly specify the devices used by Wine.
**
*/
LONG ALSA_WaveInit(void)
{
    DWORD rc;
    BOOL  AutoScanCards = TRUE;
    BOOL  AutoScanDevices = FALSE;
    BOOL  UseDirectHW = FALSE;
    DWORD DeviceCount = 0;
    HKEY  key = 0;
    int   i;

    if (!wine_dlopen("libasound.so.2", RTLD_LAZY|RTLD_GLOBAL, NULL, 0))
    {
        ERR("Error: ALSA lib needs to be loaded with flags RTLD_LAZY and RTLD_GLOBAL.\n");
        return -1;
    }

    /* @@ Wine registry key: HKCU\Software\Wine\Alsa Driver */
    rc = RegOpenKeyExA(HKEY_CURRENT_USER, "Software\\Wine\\Alsa Driver", 0, KEY_QUERY_VALUE, &key);
    if (rc == ERROR_SUCCESS)
    {
        ALSA_RegGetBoolean(key, "AutoScanCards", &AutoScanCards);
        ALSA_RegGetBoolean(key, "AutoScanDevices", &AutoScanDevices);
        ALSA_RegGetBoolean(key, "UseDirectHW", &UseDirectHW);
        ALSA_RegGetInt(key, "DeviceCount", &DeviceCount);
    }

    if (AutoScanCards)
        rc = ALSA_PerformDefaultScan(UseDirectHW, AutoScanDevices);

    for (i = 0; i < DeviceCount; i++)
    {
        char *ctl_name = NULL;
        char *pcm_name = NULL;
        char value[30];

        sprintf(value, "DevicePCM%d", i + 1);
        if (ALSA_RegGetString(key, value, &pcm_name) == ERROR_SUCCESS)
        {
            sprintf(value, "DeviceCTL%d", i + 1);
            ALSA_RegGetString(key, value, &ctl_name);
            ALSA_AddUserSpecifiedDevice(ctl_name, pcm_name);
        }

        HeapFree(GetProcessHeap(), 0, ctl_name);
        HeapFree(GetProcessHeap(), 0, pcm_name);
    }

    if (key)
        RegCloseKey(key);

    return (rc);
}

#endif /* HAVE_ALSA */
