diff options
Diffstat (limited to 'src/VBox/Devices/Audio/DrvHostPulseAudio.cpp')
-rw-r--r-- | src/VBox/Devices/Audio/DrvHostPulseAudio.cpp | 1744 |
1 files changed, 1744 insertions, 0 deletions
diff --git a/src/VBox/Devices/Audio/DrvHostPulseAudio.cpp b/src/VBox/Devices/Audio/DrvHostPulseAudio.cpp new file mode 100644 index 00000000..82de1ffb --- /dev/null +++ b/src/VBox/Devices/Audio/DrvHostPulseAudio.cpp @@ -0,0 +1,1744 @@ +/* $Id: DrvHostPulseAudio.cpp $ */ +/** @file + * VBox audio devices: Pulse Audio audio driver. + */ + +/* + * Copyright (C) 2006-2019 Oracle Corporation + * + * This file is part of VirtualBox Open Source Edition (OSE), as + * available from http://www.virtualbox.org. This file is free software; + * you can redistribute it and/or modify it under the terms of the GNU + * General Public License (GPL) as published by the Free Software + * Foundation, in version 2 as it comes in the "COPYING" file of the + * VirtualBox OSE distribution. VirtualBox OSE is distributed in the + * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_DRV_HOST_AUDIO +#include <VBox/log.h> +#include <VBox/vmm/pdmaudioifs.h> + +#include <stdio.h> + +#include <iprt/alloc.h> +#include <iprt/mem.h> +#include <iprt/uuid.h> + +RT_C_DECLS_BEGIN + #include "pulse_mangling.h" + #include "pulse_stubs.h" +RT_C_DECLS_END + +#include <pulse/pulseaudio.h> + +#include "DrvAudio.h" +#include "VBoxDD.h" + + +/********************************************************************************************************************************* +* Defines * +*********************************************************************************************************************************/ +#define VBOX_PULSEAUDIO_MAX_LOG_REL_ERRORS 32 /** @todo Make this configurable thru driver options. */ + +#ifndef PA_STREAM_NOFLAGS +# define PA_STREAM_NOFLAGS (pa_context_flags_t)0x0000U /* since 0.9.19 */ +#endif + +#ifndef PA_CONTEXT_NOFLAGS +# define PA_CONTEXT_NOFLAGS (pa_context_flags_t)0x0000U /* since 0.9.19 */ +#endif + +/** No flags specified. */ +#define PULSEAUDIOENUMCBFLAGS_NONE 0 +/** (Release) log found devices. */ +#define PULSEAUDIOENUMCBFLAGS_LOG RT_BIT(0) + +/** Makes DRVHOSTPULSEAUDIO out of PDMIHOSTAUDIO. */ +#define PDMIHOSTAUDIO_2_DRVHOSTPULSEAUDIO(pInterface) \ + ( (PDRVHOSTPULSEAUDIO)((uintptr_t)pInterface - RT_UOFFSETOF(DRVHOSTPULSEAUDIO, IHostAudio)) ) + + +/********************************************************************************************************************************* +* Structures * +*********************************************************************************************************************************/ + +/** + * Host Pulse audio driver instance data. + * @implements PDMIAUDIOCONNECTOR + */ +typedef struct DRVHOSTPULSEAUDIO +{ + /** Pointer to the driver instance structure. */ + PPDMDRVINS pDrvIns; + /** Pointer to PulseAudio's threaded main loop. */ + pa_threaded_mainloop *pMainLoop; + /** + * Pointer to our PulseAudio context. + * Note: We use a pMainLoop in a separate thread (pContext). + * So either use callback functions or protect these functions + * by pa_threaded_mainloop_lock() / pa_threaded_mainloop_unlock(). + */ + pa_context *pContext; + /** Shutdown indicator. */ + volatile bool fAbortLoop; + /** Enumeration operation successful? */ + volatile bool fEnumOpSuccess; + /** Pointer to host audio interface. */ + PDMIHOSTAUDIO IHostAudio; + /** Error count for not flooding the release log. + * Specify UINT32_MAX for unlimited logging. */ + uint32_t cLogErrors; + /** The stream (base) name; needed for distinguishing + * streams in the PulseAudio mixer controls if multiple + * VMs are running at the same time. */ + char szStreamName[64]; +} DRVHOSTPULSEAUDIO, *PDRVHOSTPULSEAUDIO; + +typedef struct PULSEAUDIOSTREAM +{ + /** The stream's acquired configuration. */ + PPDMAUDIOSTREAMCFG pCfg; + /** Pointer to driver instance. */ + PDRVHOSTPULSEAUDIO pDrv; + /** Pointer to opaque PulseAudio stream. */ + pa_stream *pStream; + /** Pulse sample format and attribute specification. */ + pa_sample_spec SampleSpec; + /** Pulse playback and buffer metrics. */ + pa_buffer_attr BufAttr; + int fOpSuccess; + /** Pointer to Pulse sample peeking buffer. */ + const uint8_t *pu8PeekBuf; + /** Current size (in bytes) of peeking data in + * buffer. */ + size_t cbPeekBuf; + /** Our offset (in bytes) in peeking buffer. */ + size_t offPeekBuf; + pa_operation *pDrainOp; + /** Number of occurred audio data underflows. */ + uint32_t cUnderflows; + /** Current latency (in us). */ + uint64_t curLatencyUs; +#ifdef LOG_ENABLED + /** Start time stamp (in us) of stream playback / recording. */ + pa_usec_t tsStartUs; + /** Time stamp (in us) when last read from / written to the stream. */ + pa_usec_t tsLastReadWrittenUs; +#endif +} PULSEAUDIOSTREAM, *PPULSEAUDIOSTREAM; + +/** + * Callback context for server enumeration callbacks. + */ +typedef struct PULSEAUDIOENUMCBCTX +{ + /** Pointer to host backend driver. */ + PDRVHOSTPULSEAUDIO pDrv; + /** Enumeration flags. */ + uint32_t fFlags; + /** Number of found input devices. */ + uint8_t cDevIn; + /** Number of found output devices. */ + uint8_t cDevOut; + /** Name of default sink being used. Must be free'd using RTStrFree(). */ + char *pszDefaultSink; + /** Name of default source being used. Must be free'd using RTStrFree(). */ + char *pszDefaultSource; +} PULSEAUDIOENUMCBCTX, *PPULSEAUDIOENUMCBCTX; + +#ifndef PA_CONTEXT_IS_GOOD /* To allow running on systems with PulseAudio < 0.9.11. */ +static inline int PA_CONTEXT_IS_GOOD(pa_context_state_t x) { + return + x == PA_CONTEXT_CONNECTING || + x == PA_CONTEXT_AUTHORIZING || + x == PA_CONTEXT_SETTING_NAME || + x == PA_CONTEXT_READY; +} +#endif /* !PA_CONTEXT_IS_GOOD */ + +#ifndef PA_STREAM_IS_GOOD /* To allow running on systems with PulseAudio < 0.9.11. */ +static inline int PA_STREAM_IS_GOOD(pa_stream_state_t x) { + return + x == PA_STREAM_CREATING || + x == PA_STREAM_READY; +} +#endif /* !PA_STREAM_IS_GOOD */ + + +/********************************************************************************************************************************* +* Prototypes * +*********************************************************************************************************************************/ + +static int paEnumerate(PDRVHOSTPULSEAUDIO pThis, PPDMAUDIOBACKENDCFG pCfg, uint32_t fEnum); +static int paError(PDRVHOSTPULSEAUDIO pThis, const char *szMsg); +#ifdef DEBUG +static void paStreamCbUnderflow(pa_stream *pStream, void *pvContext); +static void paStreamCbReqWrite(pa_stream *pStream, size_t cbLen, void *pvContext); +#endif +static void paStreamCbSuccess(pa_stream *pStream, int fSuccess, void *pvContext); + + +/** + * Signal the main loop to abort. Just signalling isn't sufficient as the + * mainloop might not have been entered yet. + */ +static void paSignalWaiter(PDRVHOSTPULSEAUDIO pThis) +{ + if (!pThis) + return; + + pThis->fAbortLoop = true; + pa_threaded_mainloop_signal(pThis->pMainLoop, 0); +} + + +static pa_sample_format_t paAudioPropsToPulse(PPDMAUDIOPCMPROPS pProps) +{ + switch (pProps->cBytes) + { + case 1: + if (!pProps->fSigned) + return PA_SAMPLE_U8; + break; + + case 2: + if (pProps->fSigned) + return PA_SAMPLE_S16LE; + break; + +#ifdef PA_SAMPLE_S32LE + case 4: + if (pProps->fSigned) + return PA_SAMPLE_S32LE; + break; +#endif + + default: + break; + } + + AssertMsgFailed(("%RU8%s not supported\n", pProps->cBytes, pProps->fSigned ? "S" : "U")); + return PA_SAMPLE_INVALID; +} + + +static int paPulseToAudioProps(pa_sample_format_t pulsefmt, PPDMAUDIOPCMPROPS pProps) +{ + switch (pulsefmt) + { + case PA_SAMPLE_U8: + pProps->cBytes = 1; + pProps->fSigned = false; + break; + + case PA_SAMPLE_S16LE: + pProps->cBytes = 2; + pProps->fSigned = true; + break; + + case PA_SAMPLE_S16BE: + pProps->cBytes = 2; + pProps->fSigned = true; + /** @todo Handle Endianess. */ + break; + +#ifdef PA_SAMPLE_S32LE + case PA_SAMPLE_S32LE: + pProps->cBytes = 4; + pProps->fSigned = true; + break; +#endif + +#ifdef PA_SAMPLE_S32BE + case PA_SAMPLE_S32BE: + pProps->cBytes = 4; + pProps->fSigned = true; + /** @todo Handle Endianess. */ + break; +#endif + + default: + AssertLogRelMsgFailed(("PulseAudio: Format (%ld) not supported\n", pulsefmt)); + return VERR_NOT_SUPPORTED; + } + + return VINF_SUCCESS; +} + + +/** + * Synchronously wait until an operation completed. + */ +static int paWaitForEx(PDRVHOSTPULSEAUDIO pThis, pa_operation *pOP, RTMSINTERVAL cMsTimeout) +{ + AssertPtrReturn(pThis, VERR_INVALID_POINTER); + AssertPtrReturn(pOP, VERR_INVALID_POINTER); + + int rc = VINF_SUCCESS; + + uint64_t u64StartMs = RTTimeMilliTS(); + while (pa_operation_get_state(pOP) == PA_OPERATION_RUNNING) + { + if (!pThis->fAbortLoop) + { + AssertPtr(pThis->pMainLoop); + pa_threaded_mainloop_wait(pThis->pMainLoop); + if ( !pThis->pContext + || pa_context_get_state(pThis->pContext) != PA_CONTEXT_READY) + { + LogRel(("PulseAudio: pa_context_get_state context not ready\n")); + break; + } + } + pThis->fAbortLoop = false; + + uint64_t u64ElapsedMs = RTTimeMilliTS() - u64StartMs; + if (u64ElapsedMs >= cMsTimeout) + { + rc = VERR_TIMEOUT; + break; + } + } + + pa_operation_unref(pOP); + + return rc; +} + + +static int paWaitFor(PDRVHOSTPULSEAUDIO pThis, pa_operation *pOP) +{ + return paWaitForEx(pThis, pOP, 10 * 1000 /* 10s timeout */); +} + + +/** + * Context status changed. + */ +static void paContextCbStateChanged(pa_context *pCtx, void *pvUser) +{ + AssertPtrReturnVoid(pCtx); + + PDRVHOSTPULSEAUDIO pThis = (PDRVHOSTPULSEAUDIO)pvUser; + AssertPtrReturnVoid(pThis); + + switch (pa_context_get_state(pCtx)) + { + case PA_CONTEXT_READY: + case PA_CONTEXT_TERMINATED: + case PA_CONTEXT_FAILED: + paSignalWaiter(pThis); + break; + + default: + break; + } +} + + +/** + * Callback called when our pa_stream_drain operation was completed. + */ +static void paStreamCbDrain(pa_stream *pStream, int fSuccess, void *pvUser) +{ + AssertPtrReturnVoid(pStream); + + PPULSEAUDIOSTREAM pStreamPA = (PPULSEAUDIOSTREAM)pvUser; + AssertPtrReturnVoid(pStreamPA); + + pStreamPA->fOpSuccess = fSuccess; + if (fSuccess) + { + pa_operation_unref(pa_stream_cork(pStream, 1, + paStreamCbSuccess, pvUser)); + } + else + paError(pStreamPA->pDrv, "Failed to drain stream"); + + if (pStreamPA->pDrainOp) + { + pa_operation_unref(pStreamPA->pDrainOp); + pStreamPA->pDrainOp = NULL; + } +} + + +/** + * Stream status changed. + */ +static void paStreamCbStateChanged(pa_stream *pStream, void *pvUser) +{ + AssertPtrReturnVoid(pStream); + + PDRVHOSTPULSEAUDIO pThis = (PDRVHOSTPULSEAUDIO)pvUser; + AssertPtrReturnVoid(pThis); + + switch (pa_stream_get_state(pStream)) + { + case PA_STREAM_READY: + case PA_STREAM_FAILED: + case PA_STREAM_TERMINATED: + paSignalWaiter(pThis); + break; + + default: + break; + } +} + + +#ifdef DEBUG +static void paStreamCbReqWrite(pa_stream *pStream, size_t cbLen, void *pvContext) +{ + RT_NOREF(cbLen, pvContext); + + PPULSEAUDIOSTREAM pStrm = (PPULSEAUDIOSTREAM)pvContext; + AssertPtrReturnVoid(pStrm); + + pa_usec_t usec = 0; + int neg = 0; + pa_stream_get_latency(pStream, &usec, &neg); + + Log2Func(("Requested %zu bytes -- Current latency is %RU64ms\n", cbLen, usec / 1000)); +} + + +static void paStreamCbUnderflow(pa_stream *pStream, void *pvContext) +{ + PPULSEAUDIOSTREAM pStrm = (PPULSEAUDIOSTREAM)pvContext; + AssertPtrReturnVoid(pStrm); + + pStrm->cUnderflows++; + + LogRel2(("PulseAudio: Warning: Hit underflow #%RU32\n", pStrm->cUnderflows)); + + if ( pStrm->cUnderflows >= 6 /** @todo Make this check configurable. */ + && pStrm->curLatencyUs < 2000000 /* 2s */) + { + pStrm->curLatencyUs = (pStrm->curLatencyUs * 3) / 2; + + LogRel2(("PulseAudio: Output latency increased to %RU64ms\n", pStrm->curLatencyUs / 1000 /* ms */)); + + pStrm->BufAttr.maxlength = pa_usec_to_bytes(pStrm->curLatencyUs, &pStrm->SampleSpec); + pStrm->BufAttr.tlength = pa_usec_to_bytes(pStrm->curLatencyUs, &pStrm->SampleSpec); + + pa_stream_set_buffer_attr(pStream, &pStrm->BufAttr, NULL, NULL); + + pStrm->cUnderflows = 0; + } + + pa_usec_t curLatencyUs = 0; + pa_stream_get_latency(pStream, &curLatencyUs, NULL /* Neg */); + + LogRel2(("PulseAudio: Latency now is %RU64ms\n", curLatencyUs / 1000 /* ms */)); + +# ifdef LOG_ENABLED + const pa_timing_info *pTInfo = pa_stream_get_timing_info(pStream); + const pa_sample_spec *pSpec = pa_stream_get_sample_spec(pStream); + + pa_usec_t curPosWritesUs = pa_bytes_to_usec(pTInfo->write_index, pSpec); + pa_usec_t curPosReadsUs = pa_bytes_to_usec(pTInfo->read_index, pSpec); + pa_usec_t curTsUs = pa_rtclock_now() - pStrm->tsStartUs; + + Log2Func(("curPosWrite=%RU64ms, curPosRead=%RU64ms, curTs=%RU64ms, curLatency=%RU64ms (%RU32Hz, %RU8 channels)\n", + curPosWritesUs / RT_US_1MS_64, curPosReadsUs / RT_US_1MS_64, + curTsUs / RT_US_1MS_64, curLatencyUs / RT_US_1MS_64, pSpec->rate, pSpec->channels)); +# endif +} + + +static void paStreamCbOverflow(pa_stream *pStream, void *pvContext) +{ + RT_NOREF(pStream, pvContext); + + Log2Func(("Warning: Hit overflow\n")); +} +#endif /* DEBUG */ + + +static void paStreamCbSuccess(pa_stream *pStream, int fSuccess, void *pvUser) +{ + AssertPtrReturnVoid(pStream); + + PPULSEAUDIOSTREAM pStrm = (PPULSEAUDIOSTREAM)pvUser; + AssertPtrReturnVoid(pStrm); + + pStrm->fOpSuccess = fSuccess; + + if (fSuccess) + paSignalWaiter(pStrm->pDrv); + else + paError(pStrm->pDrv, "Failed to finish stream operation"); +} + + +static int paStreamOpen(PDRVHOSTPULSEAUDIO pThis, PPULSEAUDIOSTREAM pStreamPA, bool fIn, const char *pszName) +{ + AssertPtrReturn(pThis, VERR_INVALID_POINTER); + AssertPtrReturn(pStreamPA, VERR_INVALID_POINTER); + AssertPtrReturn(pszName, VERR_INVALID_POINTER); + + int rc = VERR_AUDIO_STREAM_COULD_NOT_CREATE; + + pa_stream *pStream = NULL; + uint32_t flags = PA_STREAM_NOFLAGS; + + pa_threaded_mainloop_lock(pThis->pMainLoop); + + do + { + pa_sample_spec *pSampleSpec = &pStreamPA->SampleSpec; + + LogFunc(("Opening '%s', rate=%dHz, channels=%d, format=%s\n", + pszName, pSampleSpec->rate, pSampleSpec->channels, + pa_sample_format_to_string(pSampleSpec->format))); + + if (!pa_sample_spec_valid(pSampleSpec)) + { + LogRel(("PulseAudio: Unsupported sample specification for stream '%s'\n", pszName)); + break; + } + + pa_buffer_attr *pBufAttr = &pStreamPA->BufAttr; + + /** @todo r=andy Use pa_stream_new_with_proplist instead. */ + if (!(pStream = pa_stream_new(pThis->pContext, pszName, pSampleSpec, NULL /* pa_channel_map */))) + { + LogRel(("PulseAudio: Could not create stream '%s'\n", pszName)); + rc = VERR_NO_MEMORY; + break; + } + +#ifdef DEBUG + pa_stream_set_write_callback (pStream, paStreamCbReqWrite, pStreamPA); + pa_stream_set_underflow_callback (pStream, paStreamCbUnderflow, pStreamPA); + if (!fIn) /* Only for output streams. */ + pa_stream_set_overflow_callback(pStream, paStreamCbOverflow, pStreamPA); +#endif + pa_stream_set_state_callback (pStream, paStreamCbStateChanged, pThis); + +#if PA_API_VERSION >= 12 + /* XXX */ + flags |= PA_STREAM_ADJUST_LATENCY; +#endif + /* For using pa_stream_get_latency() and pa_stream_get_time(). */ + flags |= PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_AUTO_TIMING_UPDATE; + + /* No input/output right away after the stream was started. */ + flags |= PA_STREAM_START_CORKED; + + if (fIn) + { + LogFunc(("Input stream attributes: maxlength=%d fragsize=%d\n", + pBufAttr->maxlength, pBufAttr->fragsize)); + + if (pa_stream_connect_record(pStream, /*dev=*/NULL, pBufAttr, (pa_stream_flags_t)flags) < 0) + { + LogRel(("PulseAudio: Could not connect input stream '%s': %s\n", + pszName, pa_strerror(pa_context_errno(pThis->pContext)))); + break; + } + } + else + { + LogFunc(("Output buffer attributes: maxlength=%d tlength=%d prebuf=%d minreq=%d\n", + pBufAttr->maxlength, pBufAttr->tlength, pBufAttr->prebuf, pBufAttr->minreq)); + + if (pa_stream_connect_playback(pStream, /*dev=*/NULL, pBufAttr, (pa_stream_flags_t)flags, + /*cvolume=*/NULL, /*sync_stream=*/NULL) < 0) + { + LogRel(("PulseAudio: Could not connect playback stream '%s': %s\n", + pszName, pa_strerror(pa_context_errno(pThis->pContext)))); + break; + } + } + + /* Wait until the stream is ready. */ + for (;;) + { + if (!pThis->fAbortLoop) + pa_threaded_mainloop_wait(pThis->pMainLoop); + pThis->fAbortLoop = false; + + pa_stream_state_t streamSt = pa_stream_get_state(pStream); + if (streamSt == PA_STREAM_READY) + break; + else if ( streamSt == PA_STREAM_FAILED + || streamSt == PA_STREAM_TERMINATED) + { + LogRel(("PulseAudio: Failed to initialize stream '%s' (state %ld)\n", pszName, streamSt)); + break; + } + } + +#ifdef LOG_ENABLED + pStreamPA->tsStartUs = pa_rtclock_now(); +#endif + const pa_buffer_attr *pBufAttrObtained = pa_stream_get_buffer_attr(pStream); + AssertPtr(pBufAttrObtained); + memcpy(pBufAttr, pBufAttrObtained, sizeof(pa_buffer_attr)); + + LogFunc(("Obtained %s buffer attributes: tLength=%RU32, maxLength=%RU32, minReq=%RU32, fragSize=%RU32, preBuf=%RU32\n", + fIn ? "capture" : "playback", + pBufAttr->tlength, pBufAttr->maxlength, pBufAttr->minreq, pBufAttr->fragsize, pBufAttr->prebuf)); + + pStreamPA->pStream = pStream; + + rc = VINF_SUCCESS; + + } while (0); + + if ( RT_FAILURE(rc) + && pStream) + pa_stream_disconnect(pStream); + + pa_threaded_mainloop_unlock(pThis->pMainLoop); + + if (RT_FAILURE(rc)) + { + if (pStream) + pa_stream_unref(pStream); + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnInit} + */ +static DECLCALLBACK(int) drvHostPulseAudioInit(PPDMIHOSTAUDIO pInterface) +{ + AssertPtrReturn(pInterface, VERR_INVALID_POINTER); + + PDRVHOSTPULSEAUDIO pThis = PDMIHOSTAUDIO_2_DRVHOSTPULSEAUDIO(pInterface); + + LogFlowFuncEnter(); + + int rc = audioLoadPulseLib(); + if (RT_FAILURE(rc)) + { + LogRel(("PulseAudio: Failed to load the PulseAudio shared library! Error %Rrc\n", rc)); + return rc; + } + + LogRel(("PulseAudio: Using v%s\n", pa_get_library_version())); + + pThis->fAbortLoop = false; + pThis->pMainLoop = NULL; + + bool fLocked = false; + + do + { + if (!(pThis->pMainLoop = pa_threaded_mainloop_new())) + { + LogRel(("PulseAudio: Failed to allocate main loop: %s\n", + pa_strerror(pa_context_errno(pThis->pContext)))); + rc = VERR_NO_MEMORY; + break; + } + + if (!(pThis->pContext = pa_context_new(pa_threaded_mainloop_get_api(pThis->pMainLoop), "VirtualBox"))) + { + LogRel(("PulseAudio: Failed to allocate context: %s\n", + pa_strerror(pa_context_errno(pThis->pContext)))); + rc = VERR_NO_MEMORY; + break; + } + + if (pa_threaded_mainloop_start(pThis->pMainLoop) < 0) + { + LogRel(("PulseAudio: Failed to start threaded mainloop: %s\n", + pa_strerror(pa_context_errno(pThis->pContext)))); + break; + } + + /* Install a global callback to known if something happens to our acquired context. */ + pa_context_set_state_callback(pThis->pContext, paContextCbStateChanged, pThis /* pvUserData */); + + pa_threaded_mainloop_lock(pThis->pMainLoop); + fLocked = true; + + if (pa_context_connect(pThis->pContext, NULL /* pszServer */, + PA_CONTEXT_NOFLAGS, NULL) < 0) + { + LogRel(("PulseAudio: Failed to connect to server: %s\n", + pa_strerror(pa_context_errno(pThis->pContext)))); + break; + } + + /* Wait until the pThis->pContext is ready. */ + for (;;) + { + if (!pThis->fAbortLoop) + pa_threaded_mainloop_wait(pThis->pMainLoop); + pThis->fAbortLoop = false; + + pa_context_state_t cstate = pa_context_get_state(pThis->pContext); + if (cstate == PA_CONTEXT_READY) + break; + else if ( cstate == PA_CONTEXT_TERMINATED + || cstate == PA_CONTEXT_FAILED) + { + LogRel(("PulseAudio: Failed to initialize context (state %d)\n", cstate)); + break; + } + } + } + while (0); + + if (fLocked) + pa_threaded_mainloop_unlock(pThis->pMainLoop); + + if (RT_FAILURE(rc)) + { + if (pThis->pMainLoop) + pa_threaded_mainloop_stop(pThis->pMainLoop); + + if (pThis->pContext) + { + pa_context_disconnect(pThis->pContext); + pa_context_unref(pThis->pContext); + pThis->pContext = NULL; + } + + if (pThis->pMainLoop) + { + pa_threaded_mainloop_free(pThis->pMainLoop); + pThis->pMainLoop = NULL; + } + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + + +static int paCreateStreamOut(PDRVHOSTPULSEAUDIO pThis, PPULSEAUDIOSTREAM pStreamPA, + PPDMAUDIOSTREAMCFG pCfgReq, PPDMAUDIOSTREAMCFG pCfgAcq) +{ + pStreamPA->pDrainOp = NULL; + + pStreamPA->SampleSpec.format = paAudioPropsToPulse(&pCfgReq->Props); + pStreamPA->SampleSpec.rate = pCfgReq->Props.uHz; + pStreamPA->SampleSpec.channels = pCfgReq->Props.cChannels; + + pStreamPA->curLatencyUs = DrvAudioHlpFramesToMilli(pCfgReq->Backend.cfBufferSize, &pCfgReq->Props) * RT_US_1MS; + + const uint32_t cbLatency = pa_usec_to_bytes(pStreamPA->curLatencyUs, &pStreamPA->SampleSpec); + + LogRel2(("PulseAudio: Initial output latency is %RU64ms (%RU32 bytes)\n", pStreamPA->curLatencyUs / RT_US_1MS, cbLatency)); + + pStreamPA->BufAttr.tlength = cbLatency; + pStreamPA->BufAttr.maxlength = -1; /* Let the PulseAudio server choose the biggest size it can handle. */ + pStreamPA->BufAttr.prebuf = cbLatency; + pStreamPA->BufAttr.minreq = DrvAudioHlpFramesToBytes(pCfgReq->Backend.cfPeriod, &pCfgReq->Props); + + LogFunc(("Requested: BufAttr tlength=%RU32, maxLength=%RU32, minReq=%RU32\n", + pStreamPA->BufAttr.tlength, pStreamPA->BufAttr.maxlength, pStreamPA->BufAttr.minreq)); + + Assert(pCfgReq->enmDir == PDMAUDIODIR_OUT); + + char szName[256]; + RTStrPrintf2(szName, sizeof(szName), "VirtualBox %s [%s]", + DrvAudioHlpPlaybackDstToStr(pCfgReq->DestSource.Dest), pThis->szStreamName); + + /* Note that the struct BufAttr is updated to the obtained values after this call! */ + int rc = paStreamOpen(pThis, pStreamPA, false /* fIn */, szName); + if (RT_FAILURE(rc)) + return rc; + + rc = paPulseToAudioProps(pStreamPA->SampleSpec.format, &pCfgAcq->Props); + if (RT_FAILURE(rc)) + { + LogRel(("PulseAudio: Cannot find audio output format %ld\n", pStreamPA->SampleSpec.format)); + return rc; + } + + pCfgAcq->Props.uHz = pStreamPA->SampleSpec.rate; + pCfgAcq->Props.cChannels = pStreamPA->SampleSpec.channels; + pCfgAcq->Props.cShift = PDMAUDIOPCMPROPS_MAKE_SHIFT_PARMS(pCfgAcq->Props.cBytes, pCfgAcq->Props.cChannels); + + LogFunc(("Acquired: BufAttr tlength=%RU32, maxLength=%RU32, minReq=%RU32\n", + pStreamPA->BufAttr.tlength, pStreamPA->BufAttr.maxlength, pStreamPA->BufAttr.minreq)); + + pCfgAcq->Backend.cfPeriod = PDMAUDIOSTREAMCFG_B2F(pCfgAcq, pStreamPA->BufAttr.minreq); + pCfgAcq->Backend.cfBufferSize = PDMAUDIOSTREAMCFG_B2F(pCfgAcq, pStreamPA->BufAttr.tlength); + pCfgAcq->Backend.cfPreBuf = PDMAUDIOSTREAMCFG_B2F(pCfgAcq, pStreamPA->BufAttr.prebuf); + + pStreamPA->pDrv = pThis; + + return rc; +} + + +static int paCreateStreamIn(PDRVHOSTPULSEAUDIO pThis, PPULSEAUDIOSTREAM pStreamPA, + PPDMAUDIOSTREAMCFG pCfgReq, PPDMAUDIOSTREAMCFG pCfgAcq) +{ + pStreamPA->SampleSpec.format = paAudioPropsToPulse(&pCfgReq->Props); + pStreamPA->SampleSpec.rate = pCfgReq->Props.uHz; + pStreamPA->SampleSpec.channels = pCfgReq->Props.cChannels; + + pStreamPA->BufAttr.fragsize = DrvAudioHlpFramesToBytes(pCfgReq->Backend.cfPeriod, &pCfgReq->Props); + pStreamPA->BufAttr.maxlength = -1; /* Let the PulseAudio server choose the biggest size it can handle. */ + + Assert(pCfgReq->enmDir == PDMAUDIODIR_IN); + + char szName[256]; + RTStrPrintf2(szName, sizeof(szName), "VirtualBox %s [%s]", + DrvAudioHlpRecSrcToStr(pCfgReq->DestSource.Source), pThis->szStreamName); + + /* Note: Other members of BufAttr are ignored for record streams. */ + int rc = paStreamOpen(pThis, pStreamPA, true /* fIn */, szName); + if (RT_FAILURE(rc)) + return rc; + + rc = paPulseToAudioProps(pStreamPA->SampleSpec.format, &pCfgAcq->Props); + if (RT_FAILURE(rc)) + { + LogRel(("PulseAudio: Cannot find audio capture format %ld\n", pStreamPA->SampleSpec.format)); + return rc; + } + + pStreamPA->pDrv = pThis; + pStreamPA->pu8PeekBuf = NULL; + + pCfgAcq->Props.uHz = pStreamPA->SampleSpec.rate; + pCfgAcq->Props.cChannels = pStreamPA->SampleSpec.channels; + + pCfgAcq->Backend.cfPeriod = PDMAUDIOSTREAMCFG_B2F(pCfgAcq, pStreamPA->BufAttr.fragsize); + pCfgAcq->Backend.cfBufferSize = pCfgAcq->Backend.cfBufferSize; + pCfgAcq->Backend.cfPreBuf = pCfgAcq->Backend.cfPeriod; + + LogFlowFuncLeaveRC(rc); + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamCapture} + */ +static DECLCALLBACK(int) drvHostPulseAudioStreamCapture(PPDMIHOSTAUDIO pInterface, + PPDMAUDIOBACKENDSTREAM pStream, void *pvBuf, uint32_t cxBuf, uint32_t *pcxRead) +{ + RT_NOREF(pvBuf, cxBuf); + AssertPtrReturn(pInterface, VERR_INVALID_POINTER); + AssertPtrReturn(pStream, VERR_INVALID_POINTER); + AssertPtrReturn(pvBuf, VERR_INVALID_POINTER); + AssertReturn(cxBuf, VERR_INVALID_PARAMETER); + /* pcbRead is optional. */ + + PDRVHOSTPULSEAUDIO pThis = PDMIHOSTAUDIO_2_DRVHOSTPULSEAUDIO(pInterface); + PPULSEAUDIOSTREAM pStreamPA = (PPULSEAUDIOSTREAM)pStream; + + /* We should only call pa_stream_readable_size() once and trust the first value. */ + pa_threaded_mainloop_lock(pThis->pMainLoop); + size_t cbAvail = pa_stream_readable_size(pStreamPA->pStream); + pa_threaded_mainloop_unlock(pThis->pMainLoop); + + if (cbAvail == (size_t)-1) + return paError(pStreamPA->pDrv, "Failed to determine input data size"); + + /* If the buffer was not dropped last call, add what remains. */ + if (pStreamPA->pu8PeekBuf) + { + Assert(pStreamPA->cbPeekBuf >= pStreamPA->offPeekBuf); + cbAvail += (pStreamPA->cbPeekBuf - pStreamPA->offPeekBuf); + } + + Log3Func(("cbAvail=%zu\n", cbAvail)); + + if (!cbAvail) /* No data? Bail out. */ + { + if (pcxRead) + *pcxRead = 0; + return VINF_SUCCESS; + } + + int rc = VINF_SUCCESS; + + size_t cbToRead = RT_MIN(cbAvail, cxBuf); + + Log3Func(("cbToRead=%zu, cbAvail=%zu, offPeekBuf=%zu, cbPeekBuf=%zu\n", + cbToRead, cbAvail, pStreamPA->offPeekBuf, pStreamPA->cbPeekBuf)); + + uint32_t cbReadTotal = 0; + + while (cbToRead) + { + /* If there is no data, do another peek. */ + if (!pStreamPA->pu8PeekBuf) + { + pa_threaded_mainloop_lock(pThis->pMainLoop); + pa_stream_peek(pStreamPA->pStream, + (const void**)&pStreamPA->pu8PeekBuf, &pStreamPA->cbPeekBuf); + pa_threaded_mainloop_unlock(pThis->pMainLoop); + + pStreamPA->offPeekBuf = 0; + + /* No data anymore? + * Note: If there's a data hole (cbPeekBuf then contains the length of the hole) + * we need to drop the stream lateron. */ + if ( !pStreamPA->pu8PeekBuf + && !pStreamPA->cbPeekBuf) + { + break; + } + } + + Assert(pStreamPA->cbPeekBuf >= pStreamPA->offPeekBuf); + size_t cbToWrite = RT_MIN(pStreamPA->cbPeekBuf - pStreamPA->offPeekBuf, cbToRead); + + Log3Func(("cbToRead=%zu, cbToWrite=%zu, offPeekBuf=%zu, cbPeekBuf=%zu, pu8PeekBuf=%p\n", + cbToRead, cbToWrite, + pStreamPA->offPeekBuf, pStreamPA->cbPeekBuf, pStreamPA->pu8PeekBuf)); + + if ( cbToWrite + /* Only copy data if it's not a data hole (see above). */ + && pStreamPA->pu8PeekBuf + && pStreamPA->cbPeekBuf) + { + memcpy((uint8_t *)pvBuf + cbReadTotal, pStreamPA->pu8PeekBuf + pStreamPA->offPeekBuf, cbToWrite); + + Assert(cbToRead >= cbToWrite); + cbToRead -= cbToWrite; + cbReadTotal += cbToWrite; + + pStreamPA->offPeekBuf += cbToWrite; + Assert(pStreamPA->offPeekBuf <= pStreamPA->cbPeekBuf); + } + + if (/* Nothing to write anymore? Drop the buffer. */ + !cbToWrite + /* Was there a hole in the peeking buffer? Drop it. */ + || !pStreamPA->pu8PeekBuf + /* If the buffer is done, drop it. */ + || pStreamPA->offPeekBuf == pStreamPA->cbPeekBuf) + { + pa_threaded_mainloop_lock(pThis->pMainLoop); + pa_stream_drop(pStreamPA->pStream); + pa_threaded_mainloop_unlock(pThis->pMainLoop); + + pStreamPA->pu8PeekBuf = NULL; + } + } + + if (RT_SUCCESS(rc)) + { + if (pcxRead) + *pcxRead = cbReadTotal; + } + + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamPlay} + */ +static DECLCALLBACK(int) drvHostPulseAudioStreamPlay(PPDMIHOSTAUDIO pInterface, + PPDMAUDIOBACKENDSTREAM pStream, const void *pvBuf, uint32_t cxBuf, + uint32_t *pcxWritten) +{ + AssertPtrReturn(pInterface, VERR_INVALID_POINTER); + AssertPtrReturn(pStream, VERR_INVALID_POINTER); + AssertPtrReturn(pvBuf, VERR_INVALID_POINTER); + AssertReturn(cxBuf, VERR_INVALID_PARAMETER); + /* pcxWritten is optional. */ + + PDRVHOSTPULSEAUDIO pThis = PDMIHOSTAUDIO_2_DRVHOSTPULSEAUDIO(pInterface); + PPULSEAUDIOSTREAM pPAStream = (PPULSEAUDIOSTREAM)pStream; + + int rc = VINF_SUCCESS; + + uint32_t cbWrittenTotal = 0; + + pa_threaded_mainloop_lock(pThis->pMainLoop); + +#ifdef LOG_ENABLED + const pa_usec_t tsNowUs = pa_rtclock_now(); + const pa_usec_t tsDeltaPlayedUs = tsNowUs - pPAStream->tsLastReadWrittenUs; + + Log3Func(("tsDeltaPlayedMs=%RU64\n", tsDeltaPlayedUs / 1000 /* ms */)); + + pPAStream->tsLastReadWrittenUs = tsNowUs; +#endif + + do + { + size_t cbWriteable = pa_stream_writable_size(pPAStream->pStream); + if (cbWriteable == (size_t)-1) + { + rc = paError(pPAStream->pDrv, "Failed to determine output data size"); + break; + } + + size_t cbLeft = RT_MIN(cbWriteable, cxBuf); + Assert(cbLeft); /* At this point we better have *something* to write. */ + + while (cbLeft) + { + uint32_t cbChunk = cbLeft; /* Write all at once for now. */ + + if (pa_stream_write(pPAStream->pStream, (uint8_t *)pvBuf + cbWrittenTotal, cbChunk, NULL /* Cleanup callback */, + 0, PA_SEEK_RELATIVE) < 0) + { + rc = paError(pPAStream->pDrv, "Failed to write to output stream"); + break; + } + + Assert(cbLeft >= cbChunk); + cbLeft -= cbChunk; + cbWrittenTotal += cbChunk; + } + + } while (0); + + pa_threaded_mainloop_unlock(pThis->pMainLoop); + + if (RT_SUCCESS(rc)) + { + if (pcxWritten) + *pcxWritten = cbWrittenTotal; + } + + return rc; +} + + +/** @todo Implement va handling. */ +static int paError(PDRVHOSTPULSEAUDIO pThis, const char *szMsg) +{ + AssertPtrReturn(pThis, VERR_INVALID_POINTER); + AssertPtrReturn(szMsg, VERR_INVALID_POINTER); + + if (pThis->cLogErrors++ < VBOX_PULSEAUDIO_MAX_LOG_REL_ERRORS) + { + int rc2 = pa_context_errno(pThis->pContext); + LogRel2(("PulseAudio: %s: %s\n", szMsg, pa_strerror(rc2))); + } + + /** @todo Implement some PulseAudio -> IPRT mapping here. */ + return VERR_GENERAL_FAILURE; +} + + +static void paEnumSinkCb(pa_context *pCtx, const pa_sink_info *pInfo, int eol, void *pvUserData) +{ + if (eol > 0) + return; + + PPULSEAUDIOENUMCBCTX pCbCtx = (PPULSEAUDIOENUMCBCTX)pvUserData; + AssertPtrReturnVoid(pCbCtx); + PDRVHOSTPULSEAUDIO pThis = pCbCtx->pDrv; + AssertPtrReturnVoid(pThis); + if (eol < 0) + { + pThis->fEnumOpSuccess = false; + pa_threaded_mainloop_signal(pCbCtx->pDrv->pMainLoop, 0); + return; + } + + AssertPtrReturnVoid(pCtx); + AssertPtrReturnVoid(pInfo); + + LogRel2(("PulseAudio: Using output sink '%s'\n", pInfo->name)); + + /** @todo Store sinks + channel mapping in callback context as soon as we have surround support. */ + pCbCtx->cDevOut++; + + pThis->fEnumOpSuccess = true; + pa_threaded_mainloop_signal(pCbCtx->pDrv->pMainLoop, 0); +} + + +static void paEnumSourceCb(pa_context *pCtx, const pa_source_info *pInfo, int eol, void *pvUserData) +{ + if (eol > 0) + return; + + PPULSEAUDIOENUMCBCTX pCbCtx = (PPULSEAUDIOENUMCBCTX)pvUserData; + AssertPtrReturnVoid(pCbCtx); + PDRVHOSTPULSEAUDIO pThis = pCbCtx->pDrv; + AssertPtrReturnVoid(pThis); + if (eol < 0) + { + pThis->fEnumOpSuccess = false; + pa_threaded_mainloop_signal(pCbCtx->pDrv->pMainLoop, 0); + return; + } + + AssertPtrReturnVoid(pCtx); + AssertPtrReturnVoid(pInfo); + + LogRel2(("PulseAudio: Using input source '%s'\n", pInfo->name)); + + /** @todo Store sources + channel mapping in callback context as soon as we have surround support. */ + pCbCtx->cDevIn++; + + pThis->fEnumOpSuccess = true; + pa_threaded_mainloop_signal(pCbCtx->pDrv->pMainLoop, 0); +} + + +static void paEnumServerCb(pa_context *pCtx, const pa_server_info *pInfo, void *pvUserData) +{ + AssertPtrReturnVoid(pCtx); + PPULSEAUDIOENUMCBCTX pCbCtx = (PPULSEAUDIOENUMCBCTX)pvUserData; + AssertPtrReturnVoid(pCbCtx); + PDRVHOSTPULSEAUDIO pThis = pCbCtx->pDrv; + AssertPtrReturnVoid(pThis); + + if (!pInfo) + { + pThis->fEnumOpSuccess = false; + pa_threaded_mainloop_signal(pCbCtx->pDrv->pMainLoop, 0); + return; + } + + if (pInfo->default_sink_name) + { + Assert(RTStrIsValidEncoding(pInfo->default_sink_name)); + pCbCtx->pszDefaultSink = RTStrDup(pInfo->default_sink_name); + } + + if (pInfo->default_sink_name) + { + Assert(RTStrIsValidEncoding(pInfo->default_source_name)); + pCbCtx->pszDefaultSource = RTStrDup(pInfo->default_source_name); + } + + pThis->fEnumOpSuccess = true; + pa_threaded_mainloop_signal(pThis->pMainLoop, 0); +} + + +static int paEnumerate(PDRVHOSTPULSEAUDIO pThis, PPDMAUDIOBACKENDCFG pCfg, uint32_t fEnum) +{ + AssertPtrReturn(pThis, VERR_INVALID_POINTER); + AssertPtrReturn(pCfg, VERR_INVALID_POINTER); + + PDMAUDIOBACKENDCFG Cfg; + RT_ZERO(Cfg); + + RTStrPrintf2(Cfg.szName, sizeof(Cfg.szName), "PulseAudio driver"); + + Cfg.cbStreamOut = sizeof(PULSEAUDIOSTREAM); + Cfg.cbStreamIn = sizeof(PULSEAUDIOSTREAM); + Cfg.cMaxStreamsOut = UINT32_MAX; + Cfg.cMaxStreamsIn = UINT32_MAX; + + PULSEAUDIOENUMCBCTX CbCtx; + RT_ZERO(CbCtx); + + CbCtx.pDrv = pThis; + CbCtx.fFlags = fEnum; + + bool fLog = (fEnum & PULSEAUDIOENUMCBFLAGS_LOG); + + pa_threaded_mainloop_lock(pThis->pMainLoop); + + pThis->fEnumOpSuccess = false; + + LogRel(("PulseAudio: Retrieving server information ...\n")); + + /* Check if server information is available and bail out early if it isn't. */ + pa_operation *paOpServerInfo = pa_context_get_server_info(pThis->pContext, paEnumServerCb, &CbCtx); + if (!paOpServerInfo) + { + pa_threaded_mainloop_unlock(pThis->pMainLoop); + + LogRel(("PulseAudio: Server information not available, skipping enumeration\n")); + return VINF_SUCCESS; + } + + int rc = paWaitFor(pThis, paOpServerInfo); + if (RT_SUCCESS(rc) && !pThis->fEnumOpSuccess) + rc = VERR_AUDIO_BACKEND_INIT_FAILED; /* error code does not matter */ + if (RT_SUCCESS(rc)) + { + if (CbCtx.pszDefaultSink) + { + if (fLog) + LogRel2(("PulseAudio: Default output sink is '%s'\n", CbCtx.pszDefaultSink)); + + pThis->fEnumOpSuccess = false; + rc = paWaitFor(pThis, pa_context_get_sink_info_by_name(pThis->pContext, CbCtx.pszDefaultSink, + paEnumSinkCb, &CbCtx)); + if (RT_SUCCESS(rc) && !pThis->fEnumOpSuccess) + rc = VERR_AUDIO_BACKEND_INIT_FAILED; /* error code does not matter */ + if ( RT_FAILURE(rc) + && fLog) + { + LogRel(("PulseAudio: Error enumerating properties for default output sink '%s'\n", CbCtx.pszDefaultSink)); + } + } + else if (fLog) + LogRel2(("PulseAudio: No default output sink found\n")); + + if (RT_SUCCESS(rc)) + { + if (CbCtx.pszDefaultSource) + { + if (fLog) + LogRel2(("PulseAudio: Default input source is '%s'\n", CbCtx.pszDefaultSource)); + + pThis->fEnumOpSuccess = false; + rc = paWaitFor(pThis, pa_context_get_source_info_by_name(pThis->pContext, CbCtx.pszDefaultSource, + paEnumSourceCb, &CbCtx)); + if ( (RT_FAILURE(rc) || !pThis->fEnumOpSuccess) + && fLog) + { + LogRel(("PulseAudio: Error enumerating properties for default input source '%s'\n", CbCtx.pszDefaultSource)); + } + } + else if (fLog) + LogRel2(("PulseAudio: No default input source found\n")); + } + + if (RT_SUCCESS(rc)) + { + if (fLog) + { + LogRel2(("PulseAudio: Found %RU8 host playback device(s)\n", CbCtx.cDevOut)); + LogRel2(("PulseAudio: Found %RU8 host capturing device(s)\n", CbCtx.cDevIn)); + } + + if (pCfg) + memcpy(pCfg, &Cfg, sizeof(PDMAUDIOBACKENDCFG)); + } + + if (CbCtx.pszDefaultSink) + { + RTStrFree(CbCtx.pszDefaultSink); + CbCtx.pszDefaultSink = NULL; + } + + if (CbCtx.pszDefaultSource) + { + RTStrFree(CbCtx.pszDefaultSource); + CbCtx.pszDefaultSource = NULL; + } + } + else if (fLog) + LogRel(("PulseAudio: Error enumerating PulseAudio server properties\n")); + + pa_threaded_mainloop_unlock(pThis->pMainLoop); + + LogFlowFuncLeaveRC(rc); + return rc; +} + + +static int paDestroyStreamIn(PDRVHOSTPULSEAUDIO pThis, PPULSEAUDIOSTREAM pStreamPA) +{ + LogFlowFuncEnter(); + + if (pStreamPA->pStream) + { + pa_threaded_mainloop_lock(pThis->pMainLoop); + + pa_stream_disconnect(pStreamPA->pStream); + pa_stream_unref(pStreamPA->pStream); + + pStreamPA->pStream = NULL; + + pa_threaded_mainloop_unlock(pThis->pMainLoop); + } + + return VINF_SUCCESS; +} + + +static int paDestroyStreamOut(PDRVHOSTPULSEAUDIO pThis, PPULSEAUDIOSTREAM pStreamPA) +{ + if (pStreamPA->pStream) + { + pa_threaded_mainloop_lock(pThis->pMainLoop); + + /* Make sure to cancel a pending draining operation, if any. */ + if (pStreamPA->pDrainOp) + { + pa_operation_cancel(pStreamPA->pDrainOp); + pStreamPA->pDrainOp = NULL; + } + + pa_stream_disconnect(pStreamPA->pStream); + pa_stream_unref(pStreamPA->pStream); + + pStreamPA->pStream = NULL; + + pa_threaded_mainloop_unlock(pThis->pMainLoop); + } + + return VINF_SUCCESS; +} + + +static int paControlStreamOut(PDRVHOSTPULSEAUDIO pThis, PPULSEAUDIOSTREAM pStreamPA, PDMAUDIOSTREAMCMD enmStreamCmd) +{ + int rc = VINF_SUCCESS; + + switch (enmStreamCmd) + { + case PDMAUDIOSTREAMCMD_ENABLE: + case PDMAUDIOSTREAMCMD_RESUME: + { + pa_threaded_mainloop_lock(pThis->pMainLoop); + + if ( pStreamPA->pDrainOp + && pa_operation_get_state(pStreamPA->pDrainOp) != PA_OPERATION_DONE) + { + pa_operation_cancel(pStreamPA->pDrainOp); + pa_operation_unref(pStreamPA->pDrainOp); + + pStreamPA->pDrainOp = NULL; + } + else + { + /* Uncork (resume) stream. */ + rc = paWaitFor(pThis, pa_stream_cork(pStreamPA->pStream, 0 /* Uncork */, paStreamCbSuccess, pStreamPA)); + } + + pa_threaded_mainloop_unlock(pThis->pMainLoop); + break; + } + + case PDMAUDIOSTREAMCMD_DISABLE: + case PDMAUDIOSTREAMCMD_PAUSE: + { + /* Pause audio output (the Pause bit of the AC97 x_CR register is set). + * Note that we must return immediately from here! */ + pa_threaded_mainloop_lock(pThis->pMainLoop); + if (!pStreamPA->pDrainOp) + { + rc = paWaitFor(pThis, pa_stream_trigger(pStreamPA->pStream, paStreamCbSuccess, pStreamPA)); + if (RT_SUCCESS(rc)) + pStreamPA->pDrainOp = pa_stream_drain(pStreamPA->pStream, paStreamCbDrain, pStreamPA); + } + pa_threaded_mainloop_unlock(pThis->pMainLoop); + break; + } + + default: + rc = VERR_NOT_SUPPORTED; + break; + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + + +static int paControlStreamIn(PDRVHOSTPULSEAUDIO pThis, PPULSEAUDIOSTREAM pStreamPA, PDMAUDIOSTREAMCMD enmStreamCmd) +{ + int rc = VINF_SUCCESS; + + LogFlowFunc(("enmStreamCmd=%ld\n", enmStreamCmd)); + + switch (enmStreamCmd) + { + case PDMAUDIOSTREAMCMD_ENABLE: + case PDMAUDIOSTREAMCMD_RESUME: + { + pa_threaded_mainloop_lock(pThis->pMainLoop); + rc = paWaitFor(pThis, pa_stream_cork(pStreamPA->pStream, 0 /* Play / resume */, paStreamCbSuccess, pStreamPA)); + pa_threaded_mainloop_unlock(pThis->pMainLoop); + break; + } + + case PDMAUDIOSTREAMCMD_DISABLE: + case PDMAUDIOSTREAMCMD_PAUSE: + { + pa_threaded_mainloop_lock(pThis->pMainLoop); + if (pStreamPA->pu8PeekBuf) /* Do we need to drop the peek buffer?*/ + { + pa_stream_drop(pStreamPA->pStream); + pStreamPA->pu8PeekBuf = NULL; + } + + rc = paWaitFor(pThis, pa_stream_cork(pStreamPA->pStream, 1 /* Stop / pause */, paStreamCbSuccess, pStreamPA)); + pa_threaded_mainloop_unlock(pThis->pMainLoop); + break; + } + + default: + rc = VERR_NOT_SUPPORTED; + break; + } + + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnShutdown} + */ +static DECLCALLBACK(void) drvHostPulseAudioShutdown(PPDMIHOSTAUDIO pInterface) +{ + AssertPtrReturnVoid(pInterface); + + PDRVHOSTPULSEAUDIO pThis = PDMIHOSTAUDIO_2_DRVHOSTPULSEAUDIO(pInterface); + + LogFlowFuncEnter(); + + if (pThis->pMainLoop) + pa_threaded_mainloop_stop(pThis->pMainLoop); + + if (pThis->pContext) + { + pa_context_disconnect(pThis->pContext); + pa_context_unref(pThis->pContext); + pThis->pContext = NULL; + } + + if (pThis->pMainLoop) + { + pa_threaded_mainloop_free(pThis->pMainLoop); + pThis->pMainLoop = NULL; + } + + LogFlowFuncLeave(); +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnGetConfig} + */ +static DECLCALLBACK(int) drvHostPulseAudioGetConfig(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDCFG pBackendCfg) +{ + AssertPtrReturn(pInterface, VERR_INVALID_POINTER); + AssertPtrReturn(pBackendCfg, VERR_INVALID_POINTER); + + PDRVHOSTPULSEAUDIO pThis = PDMIHOSTAUDIO_2_DRVHOSTPULSEAUDIO(pInterface); + + return paEnumerate(pThis, pBackendCfg, PULSEAUDIOENUMCBFLAGS_LOG /* fEnum */); +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnGetStatus} + */ +static DECLCALLBACK(PDMAUDIOBACKENDSTS) drvHostPulseAudioGetStatus(PPDMIHOSTAUDIO pInterface, PDMAUDIODIR enmDir) +{ + RT_NOREF(enmDir); + AssertPtrReturn(pInterface, PDMAUDIOBACKENDSTS_UNKNOWN); + + return PDMAUDIOBACKENDSTS_RUNNING; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamCreate} + */ +static DECLCALLBACK(int) drvHostPulseAudioStreamCreate(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, + PPDMAUDIOSTREAMCFG pCfgReq, PPDMAUDIOSTREAMCFG pCfgAcq) +{ + AssertPtrReturn(pInterface, VERR_INVALID_POINTER); + AssertPtrReturn(pStream, VERR_INVALID_POINTER); + AssertPtrReturn(pCfgReq, VERR_INVALID_POINTER); + AssertPtrReturn(pCfgAcq, VERR_INVALID_POINTER); + + PDRVHOSTPULSEAUDIO pThis = PDMIHOSTAUDIO_2_DRVHOSTPULSEAUDIO(pInterface); + PPULSEAUDIOSTREAM pStreamPA = (PPULSEAUDIOSTREAM)pStream; + + int rc; + if (pCfgReq->enmDir == PDMAUDIODIR_IN) + rc = paCreateStreamIn (pThis, pStreamPA, pCfgReq, pCfgAcq); + else if (pCfgReq->enmDir == PDMAUDIODIR_OUT) + rc = paCreateStreamOut(pThis, pStreamPA, pCfgReq, pCfgAcq); + else + AssertFailedReturn(VERR_NOT_IMPLEMENTED); + + if (RT_SUCCESS(rc)) + { + pStreamPA->pCfg = DrvAudioHlpStreamCfgDup(pCfgAcq); + if (!pStreamPA->pCfg) + rc = VERR_NO_MEMORY; + } + + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamDestroy} + */ +static DECLCALLBACK(int) drvHostPulseAudioStreamDestroy(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + AssertPtrReturn(pInterface, VERR_INVALID_POINTER); + AssertPtrReturn(pStream, VERR_INVALID_POINTER); + + PDRVHOSTPULSEAUDIO pThis = PDMIHOSTAUDIO_2_DRVHOSTPULSEAUDIO(pInterface); + PPULSEAUDIOSTREAM pStreamPA = (PPULSEAUDIOSTREAM)pStream; + + if (!pStreamPA->pCfg) /* Not (yet) configured? Skip. */ + return VINF_SUCCESS; + + int rc; + if (pStreamPA->pCfg->enmDir == PDMAUDIODIR_IN) + rc = paDestroyStreamIn (pThis, pStreamPA); + else if (pStreamPA->pCfg->enmDir == PDMAUDIODIR_OUT) + rc = paDestroyStreamOut(pThis, pStreamPA); + else + AssertFailedStmt(rc = VERR_NOT_IMPLEMENTED); + + if (RT_SUCCESS(rc)) + { + DrvAudioHlpStreamCfgFree(pStreamPA->pCfg); + pStreamPA->pCfg = NULL; + } + + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamControl} + */ +static DECLCALLBACK(int) drvHostPulseAudioStreamControl(PPDMIHOSTAUDIO pInterface, + PPDMAUDIOBACKENDSTREAM pStream, PDMAUDIOSTREAMCMD enmStreamCmd) +{ + AssertPtrReturn(pInterface, VERR_INVALID_POINTER); + AssertPtrReturn(pStream, VERR_INVALID_POINTER); + + PDRVHOSTPULSEAUDIO pThis = PDMIHOSTAUDIO_2_DRVHOSTPULSEAUDIO(pInterface); + PPULSEAUDIOSTREAM pStreamPA = (PPULSEAUDIOSTREAM)pStream; + + if (!pStreamPA->pCfg) /* Not (yet) configured? Skip. */ + return VINF_SUCCESS; + + int rc; + if (pStreamPA->pCfg->enmDir == PDMAUDIODIR_IN) + rc = paControlStreamIn (pThis, pStreamPA, enmStreamCmd); + else if (pStreamPA->pCfg->enmDir == PDMAUDIODIR_OUT) + rc = paControlStreamOut(pThis, pStreamPA, enmStreamCmd); + else + AssertFailedStmt(rc = VERR_NOT_IMPLEMENTED); + + return rc; +} + + +static uint32_t paStreamGetAvail(PDRVHOSTPULSEAUDIO pThis, PPULSEAUDIOSTREAM pStreamPA) +{ + pa_threaded_mainloop_lock(pThis->pMainLoop); + + uint32_t cbAvail = 0; + + if (PA_STREAM_IS_GOOD(pa_stream_get_state(pStreamPA->pStream))) + { + if (pStreamPA->pCfg->enmDir == PDMAUDIODIR_IN) + { + cbAvail = (uint32_t)pa_stream_readable_size(pStreamPA->pStream); + Log3Func(("cbReadable=%RU32\n", cbAvail)); + } + else if (pStreamPA->pCfg->enmDir == PDMAUDIODIR_OUT) + { + size_t cbWritable = pa_stream_writable_size(pStreamPA->pStream); + + Log3Func(("cbWritable=%zu, maxLength=%RU32, minReq=%RU32\n", + cbWritable, pStreamPA->BufAttr.maxlength, pStreamPA->BufAttr.minreq)); + + /* Don't report more writable than the PA server can handle. */ + if (cbWritable > pStreamPA->BufAttr.maxlength) + cbWritable = pStreamPA->BufAttr.maxlength; + + cbAvail = (uint32_t)cbWritable; + } + else + AssertFailed(); + } + + pa_threaded_mainloop_unlock(pThis->pMainLoop); + + return cbAvail; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamGetReadable} + */ +static DECLCALLBACK(uint32_t) drvHostPulseAudioStreamGetReadable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + PDRVHOSTPULSEAUDIO pThis = PDMIHOSTAUDIO_2_DRVHOSTPULSEAUDIO(pInterface); + PPULSEAUDIOSTREAM pStreamPA = (PPULSEAUDIOSTREAM)pStream; + + return paStreamGetAvail(pThis, pStreamPA); +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamGetWritable} + */ +static DECLCALLBACK(uint32_t) drvHostPulseAudioStreamGetWritable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + PDRVHOSTPULSEAUDIO pThis = PDMIHOSTAUDIO_2_DRVHOSTPULSEAUDIO(pInterface); + PPULSEAUDIOSTREAM pStreamPA = (PPULSEAUDIOSTREAM)pStream; + + return paStreamGetAvail(pThis, pStreamPA); +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamGetStatus} + */ +static DECLCALLBACK(PDMAUDIOSTREAMSTS) drvHostPulseAudioStreamGetStatus(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + AssertPtrReturn(pInterface, VERR_INVALID_POINTER); + RT_NOREF(pStream); + + PDRVHOSTPULSEAUDIO pThis = PDMIHOSTAUDIO_2_DRVHOSTPULSEAUDIO(pInterface); + + PDMAUDIOSTREAMSTS strmSts = PDMAUDIOSTREAMSTS_FLAG_NONE; + + /* Check PulseAudio's general status. */ + if ( pThis->pContext + && PA_CONTEXT_IS_GOOD(pa_context_get_state(pThis->pContext))) + { + strmSts = PDMAUDIOSTREAMSTS_FLAG_INITIALIZED | PDMAUDIOSTREAMSTS_FLAG_ENABLED; + } + + return strmSts; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamIterate} + */ +static DECLCALLBACK(int) drvHostPulseAudioStreamIterate(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + AssertPtrReturn(pInterface, VERR_INVALID_POINTER); + AssertPtrReturn(pStream, VERR_INVALID_POINTER); + + LogFlowFuncEnter(); + + /* Nothing to do here for PulseAudio. */ + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{PDMIBASE,pfnQueryInterface} + */ +static DECLCALLBACK(void *) drvHostPulseAudioQueryInterface(PPDMIBASE pInterface, const char *pszIID) +{ + AssertPtrReturn(pInterface, NULL); + AssertPtrReturn(pszIID, NULL); + + PPDMDRVINS pDrvIns = PDMIBASE_2_PDMDRV(pInterface); + PDRVHOSTPULSEAUDIO pThis = PDMINS_2_DATA(pDrvIns, PDRVHOSTPULSEAUDIO); + PDMIBASE_RETURN_INTERFACE(pszIID, PDMIBASE, &pDrvIns->IBase); + PDMIBASE_RETURN_INTERFACE(pszIID, PDMIHOSTAUDIO, &pThis->IHostAudio); + + return NULL; +} + + +/** + * Destructs a PulseAudio Audio driver instance. + * + * @copydoc FNPDMDRVDESTRUCT + */ +static DECLCALLBACK(void) drvHostPulseAudioDestruct(PPDMDRVINS pDrvIns) +{ + PDMDRV_CHECK_VERSIONS_RETURN_VOID(pDrvIns); + LogFlowFuncEnter(); +} + + +/** + * Constructs a PulseAudio Audio driver instance. + * + * @copydoc FNPDMDRVCONSTRUCT + */ +static DECLCALLBACK(int) drvHostPulseAudioConstruct(PPDMDRVINS pDrvIns, PCFGMNODE pCfg, uint32_t fFlags) +{ + RT_NOREF(pCfg, fFlags); + PDMDRV_CHECK_VERSIONS_RETURN(pDrvIns); + AssertPtrReturn(pDrvIns, VERR_INVALID_POINTER); + + PDRVHOSTPULSEAUDIO pThis = PDMINS_2_DATA(pDrvIns, PDRVHOSTPULSEAUDIO); + LogRel(("Audio: Initializing PulseAudio driver\n")); + + pThis->pDrvIns = pDrvIns; + /* IBase */ + pDrvIns->IBase.pfnQueryInterface = drvHostPulseAudioQueryInterface; + /* IHostAudio */ + PDMAUDIO_IHOSTAUDIO_CALLBACKS(drvHostPulseAudio); + + int rc2 = CFGMR3QueryString(pCfg, "StreamName", pThis->szStreamName, sizeof(pThis->szStreamName)); + AssertMsgRCReturn(rc2, ("Confguration error: No/bad \"StreamName\" value, rc=%Rrc\n", rc2), rc2); + + return VINF_SUCCESS; +} + + +/** + * Pulse audio driver registration record. + */ +const PDMDRVREG g_DrvHostPulseAudio = +{ + /* u32Version */ + PDM_DRVREG_VERSION, + /* szName */ + "PulseAudio", + /* szRCMod */ + "", + /* szR0Mod */ + "", + /* pszDescription */ + "Pulse Audio host driver", + /* fFlags */ + PDM_DRVREG_FLAGS_HOST_BITS_DEFAULT, + /* fClass. */ + PDM_DRVREG_CLASS_AUDIO, + /* cMaxInstances */ + ~0U, + /* cbInstance */ + sizeof(DRVHOSTPULSEAUDIO), + /* pfnConstruct */ + drvHostPulseAudioConstruct, + /* pfnDestruct */ + drvHostPulseAudioDestruct, + /* pfnRelocate */ + NULL, + /* pfnIOCtl */ + NULL, + /* pfnPowerOn */ + NULL, + /* pfnReset */ + NULL, + /* pfnSuspend */ + NULL, + /* pfnResume */ + NULL, + /* pfnAttach */ + NULL, + /* pfnDetach */ + NULL, + /* pfnPowerOff */ + NULL, + /* pfnSoftReset */ + NULL, + /* u32EndVersion */ + PDM_DRVREG_VERSION +}; + +#if 0 /* unused */ +static struct audio_option pulse_options[] = +{ + {"DAC_MS", AUD_OPT_INT, &s_pulseCfg.buffer_msecs_out, + "DAC period size in milliseconds", NULL, 0}, + {"ADC_MS", AUD_OPT_INT, &s_pulseCfg.buffer_msecs_in, + "ADC period size in milliseconds", NULL, 0} +}; +#endif + |