winealsa: Implement opening/closing and caps of device.
[wine] / dlls / winealsa.drv / mixer.c
1 /*
2  * Alsa MIXER Wine Driver for Linux
3  * Very loosely based on wineoss mixer driver
4  *
5  * Copyright 2007 Maarten Lankhorst
6  *
7  * This library is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU Lesser General Public
9  * License as published by the Free Software Foundation; either
10  * version 2.1 of the License, or (at your option) any later version.
11  *
12  * This library is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15  * Lesser General Public License for more details.
16  *
17  * You should have received a copy of the GNU Lesser General Public
18  * License along with this library; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
20  */
21
22 #include "config.h"
23 #include "wine/port.h"
24
25 #include <stdlib.h>
26 #include <stdarg.h>
27 #include <stdio.h>
28 #include <string.h>
29 #ifdef HAVE_UNISTD_H
30 # include <unistd.h>
31 #endif
32 #include <fcntl.h>
33 #include <errno.h>
34 #include <assert.h>
35 #ifdef HAVE_SYS_IOCTL_H
36 # include <sys/ioctl.h>
37 #endif
38
39 #define NONAMELESSUNION
40 #define NONAMELESSSTRUCT
41
42 #include "windef.h"
43 #include "winbase.h"
44 #include "wingdi.h"
45 #include "winerror.h"
46 #include "winuser.h"
47 #include "winnls.h"
48 #include "mmddk.h"
49 #include "mmsystem.h"
50 #include "alsa.h"
51 #include "wine/unicode.h"
52 #include "wine/debug.h"
53
54 WINE_DEFAULT_DEBUG_CHANNEL(mixer);
55
56 #ifdef HAVE_ALSA
57
58 #define WINE_MIXER_MANUF_ID      0xAA
59 #define WINE_MIXER_PRODUCT_ID    0x55
60 #define WINE_MIXER_VERSION       0x0100
61
62 /* Generic notes:
63  * In windows it seems to be required for all controls to have a volume switch
64  * In alsa that's optional
65  *
66  * I assume for playback controls, that there is always a playback volume switch available
67  * Mute is optional
68  *
69  * For capture controls, it is needed that there is a capture switch and a volume switch,
70  * It doesn't matter wether it is a playback volume switch or a capture volume switch.
71  * The code will first try to get/adjust capture volume, if that fails it tries playback volume
72  * It is not pretty, but under my 3 test cards it seems that there is no other choice:
73  * Most capture controls don't have a capture volume setting
74  *
75  * MUX means that only capture source can be exclusively selected,
76  * MIXER means that multiple sources can be selected simultaneously.
77  */
78
79 static const char * getMessage(UINT uMsg)
80 {
81     static char str[64];
82 #define MSG_TO_STR(x) case x: return #x;
83     switch (uMsg){
84     MSG_TO_STR(DRVM_INIT);
85     MSG_TO_STR(DRVM_EXIT);
86     MSG_TO_STR(DRVM_ENABLE);
87     MSG_TO_STR(DRVM_DISABLE);
88     MSG_TO_STR(MXDM_GETDEVCAPS);
89     MSG_TO_STR(MXDM_GETLINEINFO);
90     MSG_TO_STR(MXDM_GETNUMDEVS);
91     MSG_TO_STR(MXDM_OPEN);
92     MSG_TO_STR(MXDM_CLOSE);
93     MSG_TO_STR(MXDM_GETLINECONTROLS);
94     MSG_TO_STR(MXDM_GETCONTROLDETAILS);
95     MSG_TO_STR(MXDM_SETCONTROLDETAILS);
96     default: break;
97     }
98 #undef MSG_TO_STR
99     sprintf(str, "UNKNOWN(%08x)", uMsg);
100     return str;
101 }
102
103 /* A simple declaration of a line control
104  * These are each of the channels that show up
105  */
106 typedef struct line {
107     /* Name we present to outside world */
108     WCHAR name[MAXPNAMELEN];
109
110     DWORD component;
111     DWORD dst;
112     DWORD capt;
113     DWORD chans;
114     snd_mixer_elem_t *elem;
115 } line;
116
117 /* Mixer device */
118 typedef struct mixer
119 {
120     snd_mixer_t *mix;
121     WCHAR mixername[MAXPNAMELEN];
122
123     int chans, dests;
124     LPDRVCALLBACK callback;
125     DWORD_PTR callbackpriv;
126     HDRVR hmx;
127
128     line *lines;
129 } mixer;
130
131 #define MAX_MIXERS 32
132
133 static int cards = 0;
134 static mixer mixdev[MAX_MIXERS];
135 static HANDLE thread;
136 static int elem_callback(snd_mixer_elem_t *elem, unsigned int mask);
137 static DWORD WINAPI ALSA_MixerPollThread(LPVOID lParam);
138 static CRITICAL_SECTION elem_crst;
139 static int msg_pipe[2];
140 static LONG refcnt;
141
142 /* found channel names in alsa lib, alsa api doesn't have another way for this
143  * map name -> componenttype, worst case we get a wrong componenttype which is
144  * mostly harmless
145  */
146
147 static const struct mixerlinetype {
148     const char *name;  DWORD cmpt;
149 } converttable[] = {
150     { "Master",     MIXERLINE_COMPONENTTYPE_DST_SPEAKERS,    },
151     { "Capture",    MIXERLINE_COMPONENTTYPE_DST_WAVEIN,      },
152     { "PCM",        MIXERLINE_COMPONENTTYPE_SRC_WAVEOUT,     },
153     { "PC Speaker", MIXERLINE_COMPONENTTYPE_SRC_PCSPEAKER,   },
154     { "Synth",      MIXERLINE_COMPONENTTYPE_SRC_SYNTHESIZER, },
155     { "Headphone",  MIXERLINE_COMPONENTTYPE_DST_HEADPHONES,  },
156     { "Mic",        MIXERLINE_COMPONENTTYPE_SRC_MICROPHONE,  },
157     { "Aux",        MIXERLINE_COMPONENTTYPE_SRC_UNDEFINED,   },
158     { "CD",         MIXERLINE_COMPONENTTYPE_SRC_COMPACTDISC, },
159     { "Line",       MIXERLINE_COMPONENTTYPE_SRC_LINE,        },
160     { "Phone",      MIXERLINE_COMPONENTTYPE_SRC_TELEPHONE,   },
161 };
162
163 /* Map name to MIXERLINE_COMPONENTTYPE_XXX */
164 static int getcomponenttype(const char *name)
165 {
166     int x;
167     for (x=0; x< sizeof(converttable)/sizeof(converttable[0]); ++x)
168         if (!strcasecmp(name, converttable[x].name))
169         {
170             TRACE("%d -> %s\n", x, name);
171             return converttable[x].cmpt;
172         }
173     WARN("Unknown mixer name %s, probably harmless\n", name);
174     return MIXERLINE_COMPONENTTYPE_SRC_UNDEFINED;
175 }
176
177 /* Is this control suited for showing up? */
178 static int blacklisted(snd_mixer_elem_t *elem)
179 {
180     const char *name = snd_mixer_selem_get_name(elem);
181     BOOL blisted = 0;
182
183     if (!snd_mixer_selem_has_playback_volume(elem) &&
184         (!snd_mixer_selem_has_capture_volume(elem) ||
185          !snd_mixer_selem_has_capture_switch(elem)))
186         blisted = 1;
187
188     TRACE("%s: %x\n", name, blisted);
189     return blisted;
190 }
191
192 /* get amount of channels for elem */
193 /* Officially we should keep capture/playback seperated,
194  * but that's not going to work in the alsa api */
195 static int chans(mixer *mmixer, snd_mixer_elem_t * elem, DWORD capt)
196 {
197     int ret=0, chn;
198
199     if (capt && snd_mixer_selem_has_capture_volume(elem)) {
200         for (chn = 0; chn <= SND_MIXER_SCHN_LAST; ++chn)
201             if (snd_mixer_selem_has_capture_channel(elem, chn))
202                 ++ret;
203     } else {
204         for (chn = 0; chn <= SND_MIXER_SCHN_LAST; ++chn)
205             if (snd_mixer_selem_has_playback_channel(elem, chn))
206                 ++ret;
207     }
208     if (!ret)
209         FIXME("Mixer channel %s was found for %s, but no channels were found? Wrong selection!\n", snd_mixer_selem_get_name(elem), (snd_mixer_selem_has_playback_volume(elem) ? "playback" : "capture"));
210     return ret;
211 }
212
213 static void ALSA_MixerInit(void)
214 {
215     int x, mixnum = 0;
216
217     for (x = 0; x < MAX_MIXERS; ++x)
218     {
219         int card, err, y;
220         char cardind[6], cardname[10];
221         BOOL hascapt=0, hasmast=0;
222         line *mline;
223
224         snd_ctl_t *ctl;
225         snd_mixer_elem_t *elem, *mastelem = NULL, *captelem = NULL;
226         snd_ctl_card_info_t *info = NULL;
227         snd_ctl_card_info_alloca(&info);
228         mixdev[mixnum].lines = NULL;
229         mixdev[mixnum].callback = 0;
230
231         snprintf(cardind, sizeof(cardind), "%d", x);
232         card = snd_card_get_index(cardind);
233         if (card < 0)
234             continue;
235         snprintf(cardname, sizeof(cardname), "hw:%d", card);
236
237         err = snd_ctl_open(&ctl, cardname, 0);
238         if (err < 0)
239         {
240             WARN("Cannot open card: %s\n", snd_strerror(err));
241             continue;
242         }
243
244         err = snd_ctl_card_info(ctl, info);
245         if (err < 0)
246         {
247             WARN("Cannot get card info: %s\n", snd_strerror(err));
248             snd_ctl_close(ctl);
249             continue;
250         }
251
252         MultiByteToWideChar(CP_UNIXCP, 0, snd_ctl_card_info_get_name(info), -1, mixdev[mixnum].mixername, sizeof(mixdev[mixnum].mixername)/sizeof(WCHAR));
253         snd_ctl_close(ctl);
254
255         err = snd_mixer_open(&mixdev[mixnum].mix,0);
256         if (err < 0)
257         {
258             WARN("Error occured opening mixer: %s\n", snd_strerror(err));
259             continue;
260         }
261
262         err = snd_mixer_attach(mixdev[mixnum].mix, cardname);
263         if (err < 0)
264             goto eclose;
265
266         err = snd_mixer_selem_register(mixdev[mixnum].mix, NULL, NULL);
267         if (err < 0)
268             goto eclose;
269
270         err = snd_mixer_load(mixdev[mixnum].mix);
271         if (err < 0)
272             goto eclose;
273
274         mixdev[mixnum].chans = 0;
275         mixdev[mixnum].dests = 1; /* Master, Capture will be enabled if needed */
276
277         for (elem = snd_mixer_first_elem(mixdev[mixnum].mix); elem; elem = snd_mixer_elem_next(elem))
278             if (!strcasecmp(snd_mixer_selem_get_name(elem), "Master"))
279             {
280                 mastelem = elem;
281                 ++hasmast;
282             }
283             else if (!strcasecmp(snd_mixer_selem_get_name(elem), "Capture"))
284             {
285                 captelem = elem;
286                 ++hascapt;
287             }
288             else if (!blacklisted(elem))
289             {
290                 if (snd_mixer_selem_has_capture_switch(elem))
291                 {
292                     ++mixdev[mixnum].chans;
293                     mixdev[mixnum].dests = 2;
294                 }
295                 if (snd_mixer_selem_has_playback_volume(elem))
296                     ++mixdev[mixnum].chans;
297             }
298
299         /* If there is only 'Capture' and 'Master', this device is not worth it */
300         if (!mixdev[mixnum].chans)
301         {
302             WARN("No channels found, skipping device!\n");
303             snd_mixer_close(mixdev[mixnum].mix);
304             continue;
305         }
306
307         /* If there are no 'Capture' and 'Master', something is wrong */
308         if (hasmast != 1 || hascapt != 1)
309         {
310             if (hasmast != 1)
311                 FIXME("Should have found 1 channel for 'Master', but instead found %d\n", hasmast);
312             if (hascapt != 1)
313                 FIXME("Should have found 1 channel for 'Capture', but instead found %d\n", hascapt);
314             goto eclose;
315         }
316
317         mixdev[mixnum].chans += 2; /* Capture/Master */
318         mixdev[mixnum].lines = calloc(sizeof(MIXERLINEW), mixdev[mixnum].chans);
319         err = -ENOMEM;
320         if (!mixdev[mixnum].lines)
321             goto eclose;
322
323         /* Master control */
324         mline = &mixdev[mixnum].lines[0];
325         MultiByteToWideChar(CP_UNIXCP, 0, snd_mixer_selem_get_name(mastelem), -1, mline->name, sizeof(mline->name)/sizeof(WCHAR));
326         mline->component = getcomponenttype("Master");
327         mline->dst = 0;
328         mline->capt = 0;
329         mline->elem = mastelem;
330         mline->chans = chans(&mixdev[mixnum], mastelem, 0);
331
332         /* Capture control
333          * Note: since mmixer->dests = 1, it means only playback control is visible
334          * This makes sense, because if there are no capture sources capture control
335          * can't do anything and should be invisible */
336
337         mline++;
338         MultiByteToWideChar(CP_UNIXCP, 0, snd_mixer_selem_get_name(captelem), -1, mline->name, sizeof(mline->name)/sizeof(WCHAR));
339         mline->component = getcomponenttype("Capture");
340         mline->dst = 1;
341         mline->capt = 1;
342         mline->elem = captelem;
343         mline->chans = chans(&mixdev[mixnum], captelem, 1);
344
345         snd_mixer_elem_set_callback(mastelem, &elem_callback);
346         snd_mixer_elem_set_callback_private(mastelem, &mixdev[mixnum]);
347
348         if (mixdev[mixnum].dests == 2)
349         {
350             snd_mixer_elem_set_callback(captelem, &elem_callback);
351             snd_mixer_elem_set_callback_private(captelem, &mixdev[mixnum]);
352         }
353
354         y=2;
355         for (elem = snd_mixer_first_elem(mixdev[mixnum].mix); elem; elem = snd_mixer_elem_next(elem))
356             if (elem != mastelem && elem != captelem && !blacklisted(elem))
357             {
358                 const char * name = snd_mixer_selem_get_name(elem);
359                 DWORD comp = getcomponenttype(name);
360                 snd_mixer_elem_set_callback(elem, &elem_callback);
361                 snd_mixer_elem_set_callback_private(elem, &mixdev[mixnum]);
362
363                 if (snd_mixer_selem_has_playback_volume(elem))
364                 {
365                     mline = &mixdev[mixnum].lines[y++];
366                     mline->component = comp;
367                     MultiByteToWideChar(CP_UNIXCP, 0, name, -1, mline->name, MAXPNAMELEN);
368                     mline->capt = mline->dst = 0;
369                     mline->elem = elem;
370                     mline->chans = chans(&mixdev[mixnum], elem, 0);
371                 }
372                 if (snd_mixer_selem_has_capture_switch(elem))
373                 {
374                     mline = &mixdev[mixnum].lines[y++];
375                     mline->component = comp;
376                     MultiByteToWideChar(CP_UNIXCP, 0, name, -1, mline->name, MAXPNAMELEN);
377                     mline->capt = mline->dst = 1;
378                     mline->elem = elem;
379                     mline->chans = chans(&mixdev[mixnum], elem, 1);
380                 }
381             }
382
383         TRACE("%s: Amount of controls: %i/%i, name: %s\n", cardname, mixdev[mixnum].dests, mixdev[mixnum].chans, debugstr_w(mixdev[mixnum].mixername));
384         mixnum++;
385         continue;
386
387         eclose:
388         WARN("Error occured initialising mixer: %s\n", snd_strerror(err));
389         if (mixdev[mixnum].lines)
390             free(mixdev[mixnum].lines);
391         snd_mixer_close(mixdev[mixnum].mix);
392     }
393     cards = mixnum;
394
395     InitializeCriticalSection(&elem_crst);
396     elem_crst.DebugInfo->Spare[0] = (DWORD_PTR)(__FILE__ ": ALSA_MIXER.elem_crst");
397     TRACE("\n");
398 }
399
400 static void ALSA_MixerExit(void)
401 {
402     int x;
403
404     if (refcnt)
405     {
406         WARN("Callback thread still alive, terminating uncleanly, refcnt: %d\n", refcnt);
407         /* Least we can do is making sure we're not in 'foreign' code */
408         EnterCriticalSection(&elem_crst);
409         TerminateThread(thread, 1);
410         refcnt = 0;
411     }
412
413     TRACE("Cleaning up\n");
414
415     elem_crst.DebugInfo->Spare[0] = 0;
416     DeleteCriticalSection(&elem_crst);
417     for (x = 0; x < cards; ++x)
418     {
419         snd_mixer_close(mixdev[x].mix);
420         free(mixdev[x].lines);
421     }
422     cards = 0;
423 }
424
425 static mixer* MIX_GetMix(UINT wDevID)
426 {
427     mixer *mmixer;
428
429     if (wDevID < 0 || wDevID >= cards)
430     {
431         WARN("Invalid mixer id: %d\n", wDevID);
432         return NULL;
433     }
434
435     mmixer = &mixdev[wDevID];
436     return mmixer;
437 }
438
439 /* Since alsa doesn't tell what exactly changed, just assume all affected controls changed */
440 static int elem_callback(snd_mixer_elem_t *elem, unsigned int type)
441 {
442     mixer *mmixer = snd_mixer_elem_get_callback_private(elem);
443     int x;
444
445     if (type != SND_CTL_EVENT_MASK_VALUE)
446         return 0;
447
448     assert(mmixer);
449
450     EnterCriticalSection(&elem_crst);
451
452     if (!mmixer->callback)
453         goto out;
454
455     for (x=0; x<mmixer->chans; ++x)
456     {
457         if (elem != mmixer->lines[x].elem)
458             continue;
459
460         TRACE("Found changed control %s\n", debugstr_w(mmixer->lines[x].name));
461         mmixer->callback(mmixer->hmx, MM_MIXM_LINE_CHANGE, mmixer->callbackpriv, x, 0);
462     }
463
464     out:
465     LeaveCriticalSection(&elem_crst);
466
467     return 0;
468 }
469
470 static DWORD WINAPI ALSA_MixerPollThread(LPVOID lParam)
471 {
472     struct pollfd *pfds = NULL;
473     int x, y, err, mcnt, count = 1;
474
475     TRACE("%p\n", lParam);
476
477     for (x = 0; x < cards; ++x)
478         count += snd_mixer_poll_descriptors_count(mixdev[x].mix);
479
480     TRACE("Counted %d descriptors\n", count);
481     pfds = HeapAlloc(GetProcessHeap(), 0, count * sizeof(struct pollfd));
482
483     if (!pfds)
484     {
485         WARN("Out of memory\n");
486         goto die;
487     }
488
489     pfds[0].fd = msg_pipe[0];
490     pfds[0].events = POLLIN;
491
492     y = 1;
493     for (x = 0; x < cards; ++x)
494         y += snd_mixer_poll_descriptors(mixdev[x].mix, &pfds[y], count - y);
495
496     while ((err = poll(pfds, (unsigned int) count, -1)) >= 0 || errno == EINTR || errno == EAGAIN)
497     {
498         if (pfds[0].revents & POLLIN)
499             break;
500
501         mcnt = 1;
502         for (x = y = 0; x < cards; ++x)
503         {
504             int j, max = snd_mixer_poll_descriptors_count(mixdev[x].mix);
505             for (j = 0; j < max; ++j)
506                 if (pfds[mcnt+j].revents)
507                 {
508                     y += snd_mixer_handle_events(mixdev[x].mix);
509                     break;
510                 }
511             mcnt += max;
512         }
513         if (y)
514             TRACE("Handled %d events\n", y);
515     }
516
517     die:
518     TRACE("Shutting down\n");
519     if (pfds)
520         HeapFree(GetProcessHeap(), 0, pfds);
521
522     y = read(msg_pipe[0], &x, sizeof(x));
523     close(msg_pipe[1]);
524     close(msg_pipe[0]);
525     return 0;
526 }
527
528 static DWORD MIX_Open(UINT wDevID, LPMIXEROPENDESC desc, DWORD_PTR flags)
529 {
530     mixer *mmixer = MIX_GetMix(wDevID);
531     if (!mmixer)
532         return MMSYSERR_BADDEVICEID;
533
534     flags &= CALLBACK_TYPEMASK;
535     switch (flags)
536     {
537     case CALLBACK_NULL:
538         goto done;
539
540     case CALLBACK_FUNCTION:
541         break;
542
543     default:
544         FIXME("Unhandled callback type: %08lx\n", flags & CALLBACK_TYPEMASK);
545         return MIXERR_INVALVALUE;
546     }
547
548     mmixer->callback = (LPDRVCALLBACK)desc->dwCallback;
549     mmixer->callbackpriv = desc->dwInstance;
550     mmixer->hmx = (HDRVR)desc->hmx;
551
552     done:
553     if (InterlockedIncrement(&refcnt) == 1)
554     {
555         if (pipe(msg_pipe) >= 0)
556         {
557             thread = CreateThread(NULL, 0, ALSA_MixerPollThread, NULL, 0, NULL);
558             if (!thread)
559             {
560                 close(msg_pipe[0]);
561                 close(msg_pipe[1]);
562                 msg_pipe[0] = msg_pipe[1] = -1;
563             }
564         }
565         else
566             msg_pipe[0] = msg_pipe[1] = -1;
567     }
568
569     return MMSYSERR_NOERROR;
570 }
571
572 static DWORD MIX_Close(UINT wDevID)
573 {
574     int x;
575     mixer *mmixer = MIX_GetMix(wDevID);
576     if (!mmixer)
577         return MMSYSERR_BADDEVICEID;
578
579     EnterCriticalSection(&elem_crst);
580     mmixer->callback = 0;
581     LeaveCriticalSection(&elem_crst);
582
583     if (!InterlockedDecrement(&refcnt))
584     {
585         if (write(msg_pipe[1], &x, sizeof(x)) > 0)
586         {
587             TRACE("Shutting down thread...\n");
588             WaitForSingleObject(thread, INFINITE);
589             TRACE("Done\n");
590         }
591     }
592
593     return MMSYSERR_NOERROR;
594 }
595
596 static DWORD MIX_GetDevCaps(UINT wDevID, LPMIXERCAPS2W caps, DWORD_PTR parm2)
597 {
598     mixer *mmixer = MIX_GetMix(wDevID);
599     MIXERCAPS2W capsW;
600
601     if (!caps)
602         return MMSYSERR_INVALPARAM;
603
604     if (!mmixer)
605         return MMSYSERR_BADDEVICEID;
606
607     memset(&capsW, 0, sizeof(MIXERCAPS2W));
608
609     capsW.wMid = WINE_MIXER_MANUF_ID;
610     capsW.wPid = WINE_MIXER_PRODUCT_ID;
611     capsW.vDriverVersion = WINE_MIXER_VERSION;
612
613     lstrcpynW(capsW.szPname, mmixer->mixername, sizeof(capsW.szPname)/sizeof(WCHAR));
614     capsW.cDestinations = mmixer->dests;
615     memcpy(caps, &capsW, min(parm2, sizeof(capsW)));
616     return MMSYSERR_NOERROR;
617 }
618
619 #endif /*HAVE_ALSA*/
620
621 /**************************************************************************
622  *                        mxdMessage (WINEALSA.3)
623  */
624 DWORD WINAPI ALSA_mxdMessage(UINT wDevID, UINT wMsg, DWORD_PTR dwUser,
625                              DWORD_PTR dwParam1, DWORD_PTR dwParam2)
626 {
627 #ifdef HAVE_ALSA
628     DWORD ret;
629     TRACE("(%04X, %s, %08lX, %08lX, %08lX);\n", wDevID, getMessage(wMsg),
630           dwUser, dwParam1, dwParam2);
631
632     switch (wMsg)
633     {
634     case DRVM_INIT: ALSA_MixerInit(); ret = MMSYSERR_NOERROR; break;
635     case DRVM_EXIT: ALSA_MixerExit(); ret = MMSYSERR_NOERROR; break;
636     /* All taken care of by driver initialisation */
637     /* Unimplemented, and not needed */
638     case DRVM_ENABLE:
639     case DRVM_DISABLE:
640         ret = MMSYSERR_NOERROR; break;
641
642     case MXDM_OPEN:
643         ret = MIX_Open(wDevID, (LPMIXEROPENDESC) dwParam1, dwParam2); break;
644
645     case MXDM_CLOSE:
646         ret = MIX_Close(wDevID); break;
647
648     case MXDM_GETDEVCAPS:
649         ret = MIX_GetDevCaps(wDevID, (LPMIXERCAPS2W)dwParam1, dwParam2); break;
650
651     case MXDM_GETNUMDEVS:
652         ret = cards; break;
653
654     default:
655         WARN("unknown message %s!\n", getMessage(wMsg));
656         return MMSYSERR_NOTSUPPORTED;
657     }
658
659     TRACE("Returning %08X\n", ret);
660     return ret;
661 #else /*HAVE_ALSA*/
662     TRACE("(%04X, %04X, %08lX, %08lX, %08lX);\n", wDevID, wMsg, dwUser, dwParam1, dwParam2);
663
664     return MMSYSERR_NOTENABLED;
665 #endif /*HAVE_ALSA*/
666 }