2 * Wine Driver for PulseAudio - DSound Output Functionality
3 * http://pulseaudio.org/
5 * Copyright 2009 Arthur Talyor <theycallhimart@gmail.com>
7 * Conatins code from other wine multimedia drivers.
9 * This library is free software; you can redistribute it and/or
10 * modify it under the terms of the GNU Lesser General Public
11 * License as published by the Free Software Foundation; either
12 * version 2.1 of the License, or (at your option) any later version.
14 * This library is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17 * Lesser General Public License for more details.
19 * You should have received a copy of the GNU Lesser General Public
20 * License along with this library; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
38 #include <winepulse.h>
39 #include "wine/debug.h"
41 WINE_DEFAULT_DEBUG_CHANNEL(dspulse
);
45 /*======================================================================*
46 * Low level DSOUND implementation *
47 *======================================================================*/
49 /* A buffer is allocated with a pointer indicating the read position in the
50 * buffer. The pulse write callback reads data from the buffer, updating the
51 * read pointer to the location at the end of the read. Upon reaching the end
52 * or attempting to read past the end of buffer the read pointer wraps around
53 * to the beginning again. DirectSound applications can write anywhere in the
54 * buffer at anytime without locking and can know the location of the read
55 * pointer. The position of the read pointer cannot be changed by the
56 * application and access to it uses a locking scheme. A fake pointer
57 * indicating estimated playback position is also available to the application.
58 * Applications can potentially write to the same area of memory which is also
59 * being read by the pulse thread. However, this is uncommon as directsound
60 * applications know where pulse should be reading from via the pointer
61 * locations and MSDN says that such an operation should be avoided with the
62 * results being undefined.
65 /* Fragment lengths try to be a power of two close to 10ms worth of data. See
68 static int fragment_length(pa_sample_spec
*s
) {
70 return 128 * pa_frame_size(s
);
73 return 256 * pa_frame_size(s
);
76 return 512 * pa_frame_size(s
);
78 return 1024 * pa_frame_size(s
);
81 /* Callback from the pulse thread for stream data */
82 static void DSPULSE_BufferReadCallback(pa_stream
*s
, size_t nbytes
, void *userdata
) {
83 IDsDriverBufferImpl
*This
= (IDsDriverBufferImpl
*)userdata
;
88 /* Fraglens are always powers of 2 */
89 nbytes
+= This
->fraglen
- 1;
90 nbytes
&= ~(This
->fraglen
- 1);
92 /* If we advance more than 10 fragments at a time it appears that the buffer
93 * pointer is never advancing because of wrap-around. Evil magic numbers. */
94 if (nbytes
> This
->fraglen
* 5)
95 nbytes
= This
->fraglen
* 5;
97 TRACE("Reading %u bytes.\n", nbytes
);
99 if (This
->buffer_read_offset
+ nbytes
<= This
->buffer_length
) {
100 pa_stream_write(s
, This
->buffer
+ This
->buffer_read_offset
, nbytes
, NULL
, 0, PA_SEEK_RELATIVE
);
101 This
->buffer_play_offset
= This
->buffer_read_offset
;
102 This
->buffer_read_offset
+= nbytes
;
104 size_t write_length
= This
->buffer_length
- This
->buffer_read_offset
;
105 nbytes
-= write_length
;
106 pa_stream_write(s
, This
->buffer
+ This
->buffer_read_offset
, write_length
, NULL
, 0, PA_SEEK_RELATIVE
);
107 pa_stream_write(s
, This
->buffer
, nbytes
, NULL
, 0, PA_SEEK_RELATIVE
);
108 This
->buffer_play_offset
= This
->buffer_read_offset
;
109 This
->buffer_read_offset
= nbytes
;
112 This
->buffer_read_offset
%= This
->buffer_length
;
115 /* Called when the stream underruns. Just for information */
116 static void DSPULSE_BufferUnderflowCallback(pa_stream
*s
, void *userdata
) {
117 WARN("(%p) underrun.\n", userdata
);
120 /* Connects a stream to the server. Does not update
121 * IDsDriverBufferImpl->fraglen. Does not lock the pulse mainloop or free
122 * objects in case of failure. This should be handled by the calling function.
124 static HRESULT
DSPULSE_ConnectStream(IDsDriverBufferImpl
* This
) {
125 pa_buffer_attr ba_request
;
126 const pa_buffer_attr
*ba_obtained
;
127 char c
[PA_SAMPLE_SPEC_SNPRINT_MAX
];
128 pa_stream_flags_t stream_flags
= PA_STREAM_START_CORKED
;
130 #if PA_PROTOCOL_VERSION >= 14
131 /* We are a "fragment wait based" application, so this flag should be
133 stream_flags
|= PA_STREAM_EARLY_REQUESTS
;
134 #elif PA_PROTOCOL_VERSION >= 13 && PA_API_VERSION >= 12
135 stream_flags
|= PA_STREAM_ADJUST_LATENCY
;
138 pa_sample_spec_snprint(c
, PA_SAMPLE_SPEC_SNPRINT_MAX
, &This
->sample_spec
);
139 TRACE("Sample spec %s fragment size %u.\n", c
, This
->fraglen
);
141 ba_request
.tlength
= This
->fraglen
* 4; // ~40ms
142 ba_request
.minreq
= This
->fraglen
; // ~10ms
143 ba_request
.prebuf
= (uint32_t)-1; // same as tlength
144 ba_request
.maxlength
= This
->buffer_length
; // 2^x = ~3s
146 TRACE("Asking for buffer tlength:%u (%llums) minreq:%u (%llums)\n",
147 ba_request
.tlength
, pa_bytes_to_usec(ba_request
.tlength
, &This
->sample_spec
)/1000,
148 ba_request
.minreq
, pa_bytes_to_usec(ba_request
.minreq
, &This
->sample_spec
)/1000);
150 This
->stream
= pa_stream_new(PULSE_context
, "DirectSound Buffer", &This
->sample_spec
, NULL
);
151 if (!This
->stream
) return DSERR_BADFORMAT
;
153 pa_stream_set_state_callback(This
->stream
, PULSE_StreamStateCallback
, This
);
154 pa_stream_set_write_callback(This
->stream
, DSPULSE_BufferReadCallback
, This
);
155 pa_stream_set_underflow_callback(This
->stream
, DSPULSE_BufferUnderflowCallback
, This
);
157 TRACE("Attempting to connect (%p)->stream for playback on %s\n", This
, WOutDev
[This
->drv
->wDevID
].device_name
);
158 pa_stream_connect_playback(This
->stream
, WOutDev
[This
->drv
->wDevID
].device_name
, &ba_request
, stream_flags
, NULL
, NULL
);
160 pa_context_state_t cstate
= pa_context_get_state(PULSE_context
);
161 pa_stream_state_t sstate
= pa_stream_get_state(This
->stream
);
163 if (cstate
== PA_CONTEXT_FAILED
|| cstate
== PA_CONTEXT_TERMINATED
||
164 sstate
== PA_STREAM_FAILED
|| sstate
== PA_STREAM_TERMINATED
) {
165 ERR("Failed to connect stream context object: %s\n", pa_strerror(pa_context_errno(PULSE_context
)));
166 return DSERR_BUFFERLOST
;
169 if (sstate
== PA_STREAM_READY
)
172 pa_threaded_mainloop_wait(PULSE_ml
);
174 TRACE("(%p)->stream connected for playback.\n", This
);
175 ba_obtained
= pa_stream_get_buffer_attr(This
->stream
);
177 TRACE("Obtained buffer tlength:%u (%llums) minreq:%u (%llums)\n",
178 ba_obtained
->tlength
, pa_bytes_to_usec(ba_obtained
->tlength
, &This
->sample_spec
)/1000,
179 ba_obtained
->minreq
, pa_bytes_to_usec(ba_obtained
->minreq
, &This
->sample_spec
)/1000);
184 static HRESULT WINAPI
IDsDriverBufferImpl_QueryInterface(PIDSDRIVERBUFFER iface
, REFIID riid
, LPVOID
*ppobj
) {
185 /* IDsDriverBufferImpl *This = (IDsDriverBufferImpl *)iface; */
186 FIXME("(): stub!\n");
187 return DSERR_UNSUPPORTED
;
190 static ULONG WINAPI
IDsDriverBufferImpl_AddRef(PIDSDRIVERBUFFER iface
) {
191 IDsDriverBufferImpl
*This
= (IDsDriverBufferImpl
*)iface
;
192 ULONG refCount
= InterlockedIncrement(&This
->ref
);
194 TRACE("(%p)->(ref before=%u)\n",This
, refCount
- 1);
199 static ULONG WINAPI
IDsDriverBufferImpl_Release(PIDSDRIVERBUFFER iface
) {
200 IDsDriverBufferImpl
*This
= (IDsDriverBufferImpl
*)iface
;
201 ULONG refCount
= InterlockedDecrement(&This
->ref
);
203 TRACE("(%p)->(ref before=%u)\n",This
, refCount
+ 1);
208 TRACE("mmap buffer %p destroyed\n", This
->buffer
);
209 pa_threaded_mainloop_lock(PULSE_ml
);
210 PULSE_WaitForOperation(pa_stream_cork(This
->stream
, 1, PULSE_StreamSuccessCallback
, This
));
211 pa_stream_disconnect(This
->stream
);
212 if (This
== This
->drv
->primary
)
213 This
->drv
->primary
= NULL
;
214 HeapFree(GetProcessHeap(), 0, This
->buffer
);
216 pa_stream_unref(This
->stream
);
218 HeapFree(GetProcessHeap(), 0, This
);
219 pa_threaded_mainloop_unlock(PULSE_ml
);
224 static HRESULT WINAPI
IDsDriverBufferImpl_Lock(PIDSDRIVERBUFFER iface
,
225 LPVOID
*ppvAudio1
,LPDWORD pdwLen1
,
226 LPVOID
*ppvAudio2
,LPDWORD pdwLen2
,
227 DWORD dwWritePosition
,DWORD dwWriteLen
,
230 /* We set DSDDESC_DONTNEEDPRIMARYLOCK so this should never be called */
231 TRACE("(%p): stub", iface
);
232 return DSERR_UNSUPPORTED
;
235 static HRESULT WINAPI
IDsDriverBufferImpl_Unlock(PIDSDRIVERBUFFER iface
,
236 LPVOID pvAudio1
,DWORD dwLen1
,
237 LPVOID pvAudio2
,DWORD dwLen2
)
239 /* We set DSDDESC_DONTNEEDPRIMARYLOCK so this should never be called */
240 TRACE("(%p): stub", iface
);
241 return DSERR_UNSUPPORTED
;
244 /* You cannot change the sample format of a connected stream, so we need to
245 * destroy and re-create the stream if the sample spec is different */
246 static HRESULT WINAPI
IDsDriverBufferImpl_SetFormat(PIDSDRIVERBUFFER iface
, LPWAVEFORMATEX pwfx
) {
247 IDsDriverBufferImpl
*This
= (IDsDriverBufferImpl
*)iface
;
248 pa_sample_spec old_spec
;
250 TRACE("(%p, %p)\n", iface
, pwfx
);
252 old_spec
.rate
= This
->sample_spec
.rate
;
253 old_spec
.format
= This
->sample_spec
.format
;
254 old_spec
.channels
= This
->sample_spec
.channels
;
256 if (!PULSE_SetupFormat(pwfx
, &This
->sample_spec
))
257 return DSERR_BADFORMAT
;
259 if (old_spec
.rate
== This
->sample_spec
.rate
&&
260 old_spec
.format
== This
->sample_spec
.format
&&
261 old_spec
.channels
== This
->sample_spec
.channels
) {
262 TRACE("same as original sample spec, exiting.\n");
263 PULSE_WaitForOperation(pa_stream_flush(This
->stream
, PULSE_StreamSuccessCallback
, This
));
267 /* If the format doesn't match, return an error and the buffer will be remade */
268 TRACE("Formats don't match, failing causing re-creation.\n");
269 return DSERR_BUFFERLOST
;
272 static HRESULT WINAPI
IDsDriverBufferImpl_SetFrequency(PIDSDRIVERBUFFER iface
, DWORD dwFreq
)
274 /* IDsDriverBufferImpl *This = (IDsDriverBufferImpl *)iface; */
275 /* You can't do for primary buffers anyways */
279 static HRESULT WINAPI
IDsDriverBufferImpl_SetVolumePan(PIDSDRIVERBUFFER iface
, PDSVOLUMEPAN pVolPan
) {
280 IDsDriverBufferImpl
*This
= (IDsDriverBufferImpl
*)iface
;
282 if (This
->sample_spec
.channels
== 2) {
283 This
->volume
.channels
= 2;
284 if (pVolPan
->lPan
< 0) {
285 This
->volume
.values
[0] = pa_sw_volume_from_dB(pVolPan
->lVolume
/-10.0 + pVolPan
->lPan
/10.0);
286 This
->volume
.values
[1] = pa_sw_volume_from_dB(pVolPan
->lVolume
/-10.0);
288 This
->volume
.values
[0] = pa_sw_volume_from_dB(pVolPan
->lVolume
/-10.0);
289 This
->volume
.values
[1] = pa_sw_volume_from_dB(pVolPan
->lVolume
/-10.0 - pVolPan
->lPan
/10.0);
292 WARN("Panning non-stereo streams not supported yet!\n");
293 pa_cvolume_set(&This
->volume
, This
->sample_spec
.channels
, pa_sw_volume_from_dB(pVolPan
->lVolume
/-10.0));
294 /* Would be nice to return DSERR_CONTROLUNAVAIL, but that isn't up to us
298 pa_threaded_mainloop_lock(PULSE_ml
);
299 if (This
->stream
&& PULSE_context
&& pa_context_get_state(PULSE_context
) == PA_CONTEXT_READY
&&
300 pa_stream_get_state(This
->stream
) == PA_STREAM_READY
&& pa_cvolume_valid(&This
->volume
))
301 PULSE_WaitForOperation(
302 pa_context_set_sink_input_volume(
303 PULSE_context
, pa_stream_get_index(This
->stream
),
304 &This
->volume
, PULSE_ContextSuccessCallback
, This
));
306 pa_threaded_mainloop_unlock(PULSE_ml
);
311 static HRESULT WINAPI
IDsDriverBufferImpl_SetPosition(PIDSDRIVERBUFFER iface
, DWORD dwNewPos
)
313 /* IDsDriverBufferImpl *This = (IDsDriverBufferImpl *)iface; */
314 /* Not allowed on primary buffers */
315 return DSERR_UNSUPPORTED
;
318 static HRESULT WINAPI
IDsDriverBufferImpl_GetPosition(PIDSDRIVERBUFFER iface
,
319 LPDWORD lpdwPlay
, LPDWORD lpdwWrite
)
321 IDsDriverBufferImpl
*This
= (IDsDriverBufferImpl
*)iface
;
323 pa_threaded_mainloop_lock(PULSE_ml
);
324 if (!This
->buffer
|| pa_stream_get_state(This
->stream
) != PA_STREAM_READY
) {
325 pa_threaded_mainloop_unlock(PULSE_ml
);
326 return DSERR_UNINITIALIZED
;
330 *lpdwPlay
= This
->buffer_play_offset
;
332 *lpdwWrite
= This
->buffer_read_offset
;
333 pa_threaded_mainloop_unlock(PULSE_ml
);
338 static HRESULT WINAPI
IDsDriverBufferImpl_Play(PIDSDRIVERBUFFER iface
, DWORD dwRes1
, DWORD dwRes2
, DWORD dwFlags
) {
339 IDsDriverBufferImpl
*This
= (IDsDriverBufferImpl
*)iface
;
340 TRACE("(%p,%x,%x,%x)\n",iface
,dwRes1
,dwRes2
,dwFlags
);
342 pa_threaded_mainloop_lock(PULSE_ml
);
343 PULSE_WaitForOperation(pa_stream_cork(This
->stream
, 0, PULSE_StreamSuccessCallback
, This
));
344 pa_threaded_mainloop_unlock(PULSE_ml
);
349 static HRESULT WINAPI
IDsDriverBufferImpl_Stop(PIDSDRIVERBUFFER iface
) {
350 IDsDriverBufferImpl
*This
= (IDsDriverBufferImpl
*)iface
;
351 TRACE("(%p)\n",iface
);
353 pa_threaded_mainloop_lock(PULSE_ml
);
354 PULSE_WaitForOperation(pa_stream_cork(This
->stream
, 1, PULSE_StreamSuccessCallback
, This
));
355 pa_threaded_mainloop_unlock(PULSE_ml
);
359 /*****************************************************************************/
361 static const IDsDriverBufferVtbl dsdbvt
=
363 IDsDriverBufferImpl_QueryInterface
,
364 IDsDriverBufferImpl_AddRef
,
365 IDsDriverBufferImpl_Release
,
366 IDsDriverBufferImpl_Lock
,
367 IDsDriverBufferImpl_Unlock
,
368 IDsDriverBufferImpl_SetFormat
,
369 IDsDriverBufferImpl_SetFrequency
,
370 IDsDriverBufferImpl_SetVolumePan
,
371 IDsDriverBufferImpl_SetPosition
,
372 IDsDriverBufferImpl_GetPosition
,
373 IDsDriverBufferImpl_Play
,
374 IDsDriverBufferImpl_Stop
377 static HRESULT WINAPI
IDsDriverImpl_CreateSoundBuffer(PIDSDRIVER iface
,
379 DWORD dwFlags
, DWORD dwCardAddress
,
380 LPDWORD pdwcbBufferSize
,
383 IDsDriverImpl
*This
= (IDsDriverImpl
*)iface
;
384 IDsDriverBufferImpl
** ippdsdb
= (IDsDriverBufferImpl
**)ppvObj
;
385 IDsDriverBufferImpl
*That
;
388 TRACE("(%p,%p,%x,%x)\n",iface
,pwfx
,dwFlags
,dwCardAddress
);
389 /* we only support primary buffers */
391 if (!(dwFlags
& DSBCAPS_PRIMARYBUFFER
))
392 return DSERR_UNSUPPORTED
;
394 return DSERR_ALLOCATED
;
395 This
->primary
= *ippdsdb
;
397 *ippdsdb
= HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY
, sizeof(IDsDriverBufferImpl
));
400 ERR("Out of memory\n");
401 return DSERR_OUTOFMEMORY
;
405 That
->lpVtbl
= &dsdbvt
;
409 TRACE("IdsDriverBufferImpl %p created.\n", That
);
411 if (!PULSE_SetupFormat(pwfx
, &That
->sample_spec
)) {
412 WARN("Bad audio format.\n");
413 ret
= DSERR_BADFORMAT
;
417 /* The buffer length has to be greater than fraglen * 20 or else logic in
418 * dlls/dsound/mixer.c fails to correctly understand buffer wrap around. */
419 That
->fraglen
= fragment_length(&That
->sample_spec
);
420 That
->buffer_length
= That
->fraglen
* 32;
421 That
->buffer
= HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY
, That
->buffer_length
);
423 ret
= DSERR_OUTOFMEMORY
;
426 That
->buffer_read_offset
= 0;
427 That
->buffer_play_offset
= 0;
429 pa_threaded_mainloop_lock(PULSE_ml
);
430 ret
= DSPULSE_ConnectStream(That
);
431 pa_threaded_mainloop_unlock(PULSE_ml
);
436 *pdwcbBufferSize
= That
->buffer_length
;
437 *ppbBuffer
= That
->buffer
;
439 TRACE("Exiting with success. Have buffer %p of length %u\n", That
->buffer
, That
->buffer_length
);
443 pa_threaded_mainloop_lock(PULSE_ml
);
445 if (pa_stream_get_state(That
->stream
) == PA_STREAM_READY
)
446 pa_stream_disconnect(That
->stream
);
447 pa_stream_unref(That
->stream
);
450 pa_threaded_mainloop_unlock(PULSE_ml
);
451 HeapFree(GetProcessHeap(), 0, That
->buffer
);
452 HeapFree(GetProcessHeap(), 0, *ippdsdb
);
453 WARN("exiting with failure.\n");
457 static HRESULT WINAPI
IDsDriverImpl_QueryInterface(PIDSDRIVER iface
, REFIID riid
, LPVOID
*ppobj
) {
458 /* IDsDriverImpl *This = (IDsDriverImpl *)iface; */
459 FIXME("(%p): stub!\n",iface
);
460 return DSERR_UNSUPPORTED
;
463 static ULONG WINAPI
IDsDriverImpl_AddRef(PIDSDRIVER iface
) {
464 IDsDriverImpl
*This
= (IDsDriverImpl
*)iface
;
465 ULONG refCount
= InterlockedIncrement(&This
->ref
);
467 TRACE("(%p)->(ref before=%u)\n",This
, refCount
- 1);
472 static ULONG WINAPI
IDsDriverImpl_Release(PIDSDRIVER iface
) {
473 IDsDriverImpl
*This
= (IDsDriverImpl
*)iface
;
474 ULONG refCount
= InterlockedDecrement(&This
->ref
);
476 TRACE("(%p)->(ref before=%u)\n",This
, refCount
+ 1);
481 HeapFree(GetProcessHeap(), 0, This
);
485 static HRESULT WINAPI
IDsDriverImpl_GetDriverDesc(PIDSDRIVER iface
, PDSDRIVERDESC pDesc
) {
486 IDsDriverImpl
*This
= (IDsDriverImpl
*)iface
;
487 TRACE("(%p,%p)\n",iface
,pDesc
);
488 *pDesc
= WOutDev
[This
->wDevID
].ds_desc
;
489 pDesc
->dwFlags
= DSDDESC_DONTNEEDSECONDARYLOCK
| DSDDESC_DONTNEEDPRIMARYLOCK
;
490 pDesc
->dnDevNode
= 0; /*TODO: Bwah? */
492 pDesc
->wReserved
= 0;
493 pDesc
->ulDeviceNum
= This
->wDevID
;
494 pDesc
->dwHeapType
= DSDHEAP_NOHEAP
;
495 pDesc
->pvDirectDrawHeap
= NULL
;
496 pDesc
->dwMemStartAddress
= 0xDEAD0000;
497 pDesc
->dwMemEndAddress
= 0xDEAF0000;
498 pDesc
->dwMemAllocExtra
= 0;
499 pDesc
->pvReserved1
= NULL
;
500 pDesc
->pvReserved2
= NULL
;
504 static HRESULT WINAPI
IDsDriverImpl_Close(PIDSDRIVER iface
) {
505 IDsDriverImpl
*This
= (IDsDriverImpl
*)iface
;
506 TRACE("(%p) stub, harmless\n",This
);
510 static HRESULT WINAPI
IDsDriverImpl_Open(PIDSDRIVER iface
) {
511 IDsDriverImpl
*This
= (IDsDriverImpl
*)iface
;
512 TRACE("(%p) stub, harmless\n",This
);
516 static HRESULT WINAPI
IDsDriverImpl_GetCaps(PIDSDRIVER iface
, PDSDRIVERCAPS pCaps
) {
517 IDsDriverImpl
*This
= (IDsDriverImpl
*)iface
;
519 if (pa_context_get_state(PULSE_context
) != PA_CONTEXT_READY
) {
520 ERR("Context failure.\n");
521 return DSERR_GENERIC
;
524 TRACE("(%p,%p)\n",iface
,pCaps
);
525 *pCaps
= WOutDev
[This
->wDevID
].ds_caps
;
529 static HRESULT WINAPI
IDsDriverImpl_DuplicateSoundBuffer(PIDSDRIVER iface
,
530 PIDSDRIVERBUFFER pBuffer
,
533 IDsDriverImpl
*This
= (IDsDriverImpl
*)iface
;
534 FIXME("(%p,%p): stub\n",This
,pBuffer
);
535 return DSERR_INVALIDCALL
;
538 static const IDsDriverVtbl dsdvt
=
540 IDsDriverImpl_QueryInterface
,
541 IDsDriverImpl_AddRef
,
542 IDsDriverImpl_Release
,
543 IDsDriverImpl_GetDriverDesc
,
546 IDsDriverImpl_GetCaps
,
547 IDsDriverImpl_CreateSoundBuffer
,
548 IDsDriverImpl_DuplicateSoundBuffer
551 DWORD
wodDsCreate(UINT wDevID
, PIDSDRIVER
* drv
) {
552 IDsDriverImpl
** idrv
= (IDsDriverImpl
**)drv
;
554 if (pa_context_get_state(PULSE_context
) != PA_CONTEXT_READY
|| pa_context_is_local(PULSE_context
) != 1) {
555 WARN("Connection failure or server is not local, falling back to WaveOut HEL.\n");
556 return MMSYSERR_NOTSUPPORTED
;
559 *idrv
= HeapAlloc(GetProcessHeap(), 0, sizeof(IDsDriverImpl
));
561 return MMSYSERR_NOMEM
;
563 TRACE("IDsDriverImpl %p created.\n", *idrv
);
565 (*idrv
)->lpVtbl
= &dsdvt
;
567 (*idrv
)->wDevID
= wDevID
;
568 (*idrv
)->primary
= NULL
;
570 return MMSYSERR_NOERROR
;
573 DWORD
wodDsDesc(UINT wDevID
, PDSDRIVERDESC desc
) {
574 TRACE("(%u, %p)\n", wDevID
, desc
);
575 *desc
= WOutDev
[wDevID
].ds_desc
;
576 return MMSYSERR_NOERROR
;
578 #endif /* HAVE_PULSEAUDIO */