summaryrefslogtreecommitdiffstats
path: root/src/VBox/Devices/Audio/DrvHostAudioPulseAudio.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/VBox/Devices/Audio/DrvHostAudioPulseAudio.cpp')
-rw-r--r--src/VBox/Devices/Audio/DrvHostAudioPulseAudio.cpp2432
1 files changed, 2432 insertions, 0 deletions
diff --git a/src/VBox/Devices/Audio/DrvHostAudioPulseAudio.cpp b/src/VBox/Devices/Audio/DrvHostAudioPulseAudio.cpp
new file mode 100644
index 00000000..c60e17cd
--- /dev/null
+++ b/src/VBox/Devices/Audio/DrvHostAudioPulseAudio.cpp
@@ -0,0 +1,2432 @@
+/* $Id: DrvHostAudioPulseAudio.cpp $ */
+/** @file
+ * Host audio driver - Pulse Audio.
+ */
+
+/*
+ * Copyright (C) 2006-2023 Oracle and/or its affiliates.
+ *
+ * This file is part of VirtualBox base platform packages, as
+ * available from https://www.virtualbox.org.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation, in version 3 of the
+ * License.
+ *
+ * This program 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+
+/*********************************************************************************************************************************
+* Header Files *
+*********************************************************************************************************************************/
+#define LOG_GROUP LOG_GROUP_DRV_HOST_AUDIO
+#include <VBox/log.h>
+#include <VBox/vmm/pdmaudioifs.h>
+#include <VBox/vmm/pdmaudioinline.h>
+#include <VBox/vmm/pdmaudiohostenuminline.h>
+
+#include <stdio.h>
+
+#include <iprt/alloc.h>
+#include <iprt/mem.h>
+#include <iprt/uuid.h>
+#include <iprt/semaphore.h>
+
+#include "DrvHostAudioPulseAudioStubsMangling.h"
+#include "DrvHostAudioPulseAudioStubs.h"
+
+#include <pulse/pulseaudio.h>
+#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
+
+#include "VBoxDD.h"
+
+
+/*********************************************************************************************************************************
+* Defines *
+*********************************************************************************************************************************/
+/** Max number of errors reported by drvHstAudPaError per instance.
+ * @todo Make this configurable thru driver config. */
+#define VBOX_PULSEAUDIO_MAX_LOG_REL_ERRORS 99
+
+
+/** @name DRVHSTAUDPAENUMCB_F_XXX
+ * @{ */
+/** No flags specified. */
+#define DRVHSTAUDPAENUMCB_F_NONE 0
+/** (Release) log found devices. */
+#define DRVHSTAUDPAENUMCB_F_LOG RT_BIT(0)
+/** Only do default devices. */
+#define DRVHSTAUDPAENUMCB_F_DEFAULT_ONLY RT_BIT(1)
+/** @} */
+
+
+/*********************************************************************************************************************************
+* Structures *
+*********************************************************************************************************************************/
+/** Pointer to the instance data for a pulse audio host audio driver. */
+typedef struct DRVHSTAUDPA *PDRVHSTAUDPA;
+
+
+/**
+ * Callback context for the server init context state changed callback.
+ */
+typedef struct DRVHSTAUDPASTATECHGCTX
+{
+ /** The event semaphore. */
+ RTSEMEVENT hEvtInit;
+ /** The returned context state. */
+ pa_context_state_t volatile enmCtxState;
+} DRVHSTAUDPASTATECHGCTX;
+/** Pointer to a server init context state changed callback context. */
+typedef DRVHSTAUDPASTATECHGCTX *PDRVHSTAUDPASTATECHGCTX;
+
+
+/**
+ * Enumeration callback context used by the pfnGetConfig code.
+ */
+typedef struct DRVHSTAUDPAENUMCBCTX
+{
+ /** Pointer to PulseAudio's threaded main loop. */
+ pa_threaded_mainloop *pMainLoop;
+ /** Enumeration flags, DRVHSTAUDPAENUMCB_F_XXX. */
+ uint32_t fFlags;
+ /** VBox status code for the operation.
+ * The caller sets this to VERR_AUDIO_ENUMERATION_FAILED, the callback never
+ * uses that status code. */
+ int32_t rcEnum;
+ /** 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;
+ /** The device enumeration to fill, NULL if pfnGetConfig context. */
+ PPDMAUDIOHOSTENUM pDeviceEnum;
+} DRVHSTAUDPAENUMCBCTX;
+/** Pointer to an enumeration callback context. */
+typedef DRVHSTAUDPAENUMCBCTX *PDRVHSTAUDPAENUMCBCTX;
+
+
+/**
+ * Pulse audio device enumeration entry.
+ */
+typedef struct DRVHSTAUDPADEVENTRY
+{
+ /** The part we share with others. */
+ PDMAUDIOHOSTDEV Core;
+} DRVHSTAUDPADEVENTRY;
+/** Pointer to a pulse audio device enumeration entry. */
+typedef DRVHSTAUDPADEVENTRY *PDRVHSTAUDPADEVENTRY;
+
+
+/**
+ * Pulse audio stream data.
+ */
+typedef struct DRVHSTAUDPASTREAM
+{
+ /** Common part. */
+ PDMAUDIOBACKENDSTREAM Core;
+ /** The stream's acquired configuration. */
+ PDMAUDIOSTREAMCFG Cfg;
+ /** Pointer to driver instance. */
+ PDRVHSTAUDPA pDrv;
+ /** Pointer to opaque PulseAudio stream. */
+ pa_stream *pStream;
+ /** Input: Pointer to Pulse sample peek buffer. */
+ const uint8_t *pbPeekBuf;
+ /** Input: Current size (in bytes) of peeked data in buffer. */
+ size_t cbPeekBuf;
+ /** Input: Our offset (in bytes) in peek data buffer. */
+ size_t offPeekBuf;
+ /** Output: Asynchronous drain operation. This is used as an indicator of
+ * whether we're currently draining the stream (will be cleaned up before
+ * resume/re-enable). */
+ pa_operation *pDrainOp;
+ /** Asynchronous cork/uncork operation.
+ * (This solely for cancelling before destroying the stream, so the callback
+ * won't do any after-freed accesses.) */
+ pa_operation *pCorkOp;
+ /** Asynchronous trigger operation.
+ * (This solely for cancelling before destroying the stream, so the callback
+ * won't do any after-freed accesses.) */
+ pa_operation *pTriggerOp;
+ /** Internal byte offset. */
+ uint64_t offInternal;
+#ifdef LOG_ENABLED
+ /** Creation timestamp (in microsecs) of stream playback / recording. */
+ pa_usec_t tsStartUs;
+ /** Timestamp (in microsecs) when last read from / written to the stream. */
+ pa_usec_t tsLastReadWrittenUs;
+#endif
+ /** Number of occurred audio data underflows. */
+ uint32_t cUnderflows;
+ /** Pulse sample format and attribute specification. */
+ pa_sample_spec SampleSpec;
+ /** Channel map. */
+ pa_channel_map ChannelMap;
+ /** Pulse playback and buffer metrics. */
+ pa_buffer_attr BufAttr;
+} DRVHSTAUDPASTREAM;
+/** Pointer to pulse audio stream data. */
+typedef DRVHSTAUDPASTREAM *PDRVHSTAUDPASTREAM;
+
+
+/**
+ * Pulse audio host audio driver instance data.
+ * @implements PDMIAUDIOCONNECTOR
+ */
+typedef struct DRVHSTAUDPA
+{
+ /** 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;
+ /** Error count for not flooding the release log.
+ * Specify UINT32_MAX for unlimited logging. */
+ uint32_t cLogErrors;
+ /** Don't want to put this on the stack... */
+ DRVHSTAUDPASTATECHGCTX InitStateChgCtx;
+ /** Pointer to host audio interface. */
+ PDMIHOSTAUDIO IHostAudio;
+ /** Upwards notification interface. */
+ PPDMIHOSTAUDIOPORT pIHostAudioPort;
+
+ /** The stream (base) name.
+ * This is needed for distinguishing streams in the PulseAudio mixer controls if
+ * multiple VMs are running at the same time. */
+ char szStreamName[64];
+ /** The name of the input device to use. Empty string for default. */
+ char szInputDev[256];
+ /** The name of the output device to use. Empty string for default. */
+ char szOutputDev[256];
+
+ /** Number of buffer underruns (for all streams). */
+ STAMCOUNTER StatUnderruns;
+ /** Number of buffer overruns (for all streams). */
+ STAMCOUNTER StatOverruns;
+} DRVHSTAUDPA;
+
+
+
+/*
+ * Glue to make the code work systems with PulseAudio < 0.9.11.
+ */
+#if !defined(PA_CONTEXT_IS_GOOD) && PA_API_VERSION < 12 /* 12 = 0.9.11 where PA_STREAM_IS_GOOD was added */
+DECLINLINE(bool) PA_CONTEXT_IS_GOOD(pa_context_state_t enmState)
+{
+ return enmState == PA_CONTEXT_CONNECTING
+ || enmState == PA_CONTEXT_AUTHORIZING
+ || enmState == PA_CONTEXT_SETTING_NAME
+ || enmState == PA_CONTEXT_READY;
+}
+#endif
+
+#if !defined(PA_STREAM_IS_GOOD) && PA_API_VERSION < 12 /* 12 = 0.9.11 where PA_STREAM_IS_GOOD was added */
+DECLINLINE(bool) PA_STREAM_IS_GOOD(pa_stream_state_t enmState)
+{
+ return enmState == PA_STREAM_CREATING
+ || enmState == PA_STREAM_READY;
+}
+#endif
+
+
+/**
+ * Converts a pulse audio error to a VBox status.
+ *
+ * @returns VBox status code.
+ * @param rcPa The error code to convert.
+ */
+static int drvHstAudPaErrorToVBox(int rcPa)
+{
+ /** @todo Implement some PulseAudio -> VBox mapping here. */
+ RT_NOREF(rcPa);
+ return VERR_GENERAL_FAILURE;
+}
+
+
+/**
+ * Logs a pulse audio (from context) and converts it to VBox status.
+ *
+ * @returns VBox status code.
+ * @param pThis Our instance data.
+ * @param pszFormat The format string for the release log (no newline) .
+ * @param ... Format string arguments.
+ */
+static int drvHstAudPaError(PDRVHSTAUDPA pThis, const char *pszFormat, ...)
+{
+ AssertPtrReturn(pThis, VERR_INVALID_POINTER);
+ AssertPtr(pszFormat);
+
+ int const rcPa = pa_context_errno(pThis->pContext);
+ int const rcVBox = drvHstAudPaErrorToVBox(rcPa);
+
+ if ( pThis->cLogErrors < VBOX_PULSEAUDIO_MAX_LOG_REL_ERRORS
+ && LogRelIs2Enabled())
+ {
+ va_list va;
+ va_start(va, pszFormat);
+ LogRel(("PulseAudio: %N: %s (%d, %Rrc)\n", pszFormat, &va, pa_strerror(rcPa), rcPa, rcVBox));
+ va_end(va);
+
+ if (++pThis->cLogErrors == VBOX_PULSEAUDIO_MAX_LOG_REL_ERRORS)
+ LogRel(("PulseAudio: muting errors (max %u)\n", VBOX_PULSEAUDIO_MAX_LOG_REL_ERRORS));
+ }
+
+ return rcVBox;
+}
+
+
+/**
+ * Signal the main loop to abort. Just signalling isn't sufficient as the
+ * mainloop might not have been entered yet.
+ */
+static void drvHstAudPaSignalWaiter(PDRVHSTAUDPA pThis)
+{
+ if (pThis)
+ {
+ pThis->fAbortLoop = true;
+ pa_threaded_mainloop_signal(pThis->pMainLoop, 0);
+ }
+}
+
+
+/**
+ * Wrapper around pa_threaded_mainloop_wait().
+ */
+static void drvHstAudPaMainloopWait(PDRVHSTAUDPA pThis)
+{
+ /** @todo r=bird: explain this logic. */
+ if (!pThis->fAbortLoop)
+ pa_threaded_mainloop_wait(pThis->pMainLoop);
+ pThis->fAbortLoop = false;
+}
+
+
+/**
+ * Pulse audio callback for context status changes, init variant.
+ */
+static void drvHstAudPaCtxCallbackStateChanged(pa_context *pCtx, void *pvUser)
+{
+ AssertPtrReturnVoid(pCtx);
+
+ PDRVHSTAUDPA pThis = (PDRVHSTAUDPA)pvUser;
+ AssertPtrReturnVoid(pThis);
+
+ switch (pa_context_get_state(pCtx))
+ {
+ case PA_CONTEXT_READY:
+ case PA_CONTEXT_TERMINATED:
+ case PA_CONTEXT_FAILED:
+ drvHstAudPaSignalWaiter(pThis);
+ break;
+
+ default:
+ break;
+ }
+}
+
+
+/**
+ * Synchronously wait until an operation completed.
+ *
+ * This will consume the pOperation reference.
+ */
+static int drvHstAudPaWaitForEx(PDRVHSTAUDPA pThis, pa_operation *pOperation, RTMSINTERVAL cMsTimeout)
+{
+ AssertPtrReturn(pOperation, VERR_INVALID_POINTER);
+
+ uint64_t const msStart = RTTimeMilliTS();
+ pa_operation_state_t enmOpState;
+ while ((enmOpState = pa_operation_get_state(pOperation)) == PA_OPERATION_RUNNING)
+ {
+ if (!pThis->fAbortLoop) /** @todo r=bird: I do _not_ get the logic behind this fAbortLoop mechanism, it looks more
+ * than a little mixed up and too much generalized see drvHstAudPaSignalWaiter. */
+ {
+ AssertPtr(pThis->pMainLoop);
+ pa_threaded_mainloop_wait(pThis->pMainLoop);
+ if ( !pThis->pContext
+ || pa_context_get_state(pThis->pContext) != PA_CONTEXT_READY)
+ {
+ pa_operation_cancel(pOperation);
+ pa_operation_unref(pOperation);
+ LogRel(("PulseAudio: pa_context_get_state context not ready\n"));
+ return VERR_INVALID_STATE;
+ }
+ }
+ pThis->fAbortLoop = false;
+
+ /*
+ * Note! This timeout business is a bit bogus as pa_threaded_mainloop_wait is indefinite.
+ */
+ if (RTTimeMilliTS() - msStart >= cMsTimeout)
+ {
+ enmOpState = pa_operation_get_state(pOperation);
+ if (enmOpState != PA_OPERATION_RUNNING)
+ break;
+ pa_operation_cancel(pOperation);
+ pa_operation_unref(pOperation);
+ return VERR_TIMEOUT;
+ }
+ }
+
+ pa_operation_unref(pOperation);
+ if (enmOpState == PA_OPERATION_DONE)
+ return VINF_SUCCESS;
+ return VERR_CANCELLED;
+}
+
+
+static int drvHstAudPaWaitFor(PDRVHSTAUDPA pThis, pa_operation *pOP)
+{
+ return drvHstAudPaWaitForEx(pThis, pOP, 10 * RT_MS_1SEC);
+}
+
+
+
+/*********************************************************************************************************************************
+* PDMIHOSTAUDIO *
+*********************************************************************************************************************************/
+
+/**
+ * Worker for drvHstAudPaEnumSourceCallback() and
+ * drvHstAudPaEnumSinkCallback() that adds an entry to the enumeration
+ * result.
+ */
+static void drvHstAudPaEnumAddDevice(PDRVHSTAUDPAENUMCBCTX pCbCtx, PDMAUDIODIR enmDir, const char *pszName,
+ const char *pszDesc, uint8_t cChannelsInput, uint8_t cChannelsOutput,
+ const char *pszDefaultName)
+{
+ size_t const cbId = strlen(pszName) + 1;
+ size_t const cbName = pszDesc && *pszDesc ? strlen(pszDesc) + 1 : cbId;
+ PDRVHSTAUDPADEVENTRY pDev = (PDRVHSTAUDPADEVENTRY)PDMAudioHostDevAlloc(sizeof(*pDev), cbName, cbId);
+ if (pDev != NULL)
+ {
+ pDev->Core.enmUsage = enmDir;
+ pDev->Core.enmType = RTStrIStr(pszDesc, "built-in") != NULL
+ ? PDMAUDIODEVICETYPE_BUILTIN : PDMAUDIODEVICETYPE_UNKNOWN;
+ if (RTStrCmp(pszName, pszDefaultName) != 0)
+ pDev->Core.fFlags = PDMAUDIOHOSTDEV_F_NONE;
+ else
+ pDev->Core.fFlags = enmDir == PDMAUDIODIR_IN ? PDMAUDIOHOSTDEV_F_DEFAULT_IN : PDMAUDIOHOSTDEV_F_DEFAULT_OUT;
+ pDev->Core.cMaxInputChannels = cChannelsInput;
+ pDev->Core.cMaxOutputChannels = cChannelsOutput;
+
+ int rc = RTStrCopy(pDev->Core.pszId, cbId, pszName);
+ AssertRC(rc);
+
+ rc = RTStrCopy(pDev->Core.pszName, cbName, pszDesc && *pszDesc ? pszDesc : pszName);
+ AssertRC(rc);
+
+ PDMAudioHostEnumAppend(pCbCtx->pDeviceEnum, &pDev->Core);
+ }
+ else
+ pCbCtx->rcEnum = VERR_NO_MEMORY;
+}
+
+
+/**
+ * Enumeration callback - source info.
+ *
+ * @param pCtx The context (DRVHSTAUDPA::pContext).
+ * @param pInfo The info. NULL when @a eol is not zero.
+ * @param eol Error-or-last indicator or something like that:
+ * - 0: Normal call with info.
+ * - 1: End of list, no info.
+ * - -1: Error callback, no info.
+ * @param pvUserData Pointer to our DRVHSTAUDPAENUMCBCTX structure.
+ */
+static void drvHstAudPaEnumSourceCallback(pa_context *pCtx, const pa_source_info *pInfo, int eol, void *pvUserData)
+{
+ LogFlowFunc(("pCtx=%p pInfo=%p eol=%d pvUserData=%p\n", pCtx, pInfo, eol, pvUserData));
+ PDRVHSTAUDPAENUMCBCTX pCbCtx = (PDRVHSTAUDPAENUMCBCTX)pvUserData;
+ AssertPtrReturnVoid(pCbCtx);
+ Assert((pInfo == NULL) == (eol != 0));
+ RT_NOREF(pCtx);
+
+ if (eol == 0 && pInfo != NULL)
+ {
+ LogRel2(("PulseAudio: Source #%u: %u Hz %uch format=%u name='%s' desc='%s' driver='%s' flags=%#x\n",
+ pInfo->index, pInfo->sample_spec.rate, pInfo->sample_spec.channels, pInfo->sample_spec.format,
+ pInfo->name, pInfo->description, pInfo->driver, pInfo->flags));
+ drvHstAudPaEnumAddDevice(pCbCtx, PDMAUDIODIR_IN, pInfo->name, pInfo->description,
+ pInfo->sample_spec.channels, 0 /*cChannelsOutput*/, pCbCtx->pszDefaultSource);
+ }
+ else if (eol == 1 && !pInfo && pCbCtx->rcEnum == VERR_AUDIO_ENUMERATION_FAILED)
+ pCbCtx->rcEnum = VINF_SUCCESS;
+
+ /* Wake up the calling thread when done: */
+ if (eol != 0)
+ pa_threaded_mainloop_signal(pCbCtx->pMainLoop, 0);
+}
+
+
+/**
+ * Enumeration callback - sink info.
+ *
+ * @param pCtx The context (DRVHSTAUDPA::pContext).
+ * @param pInfo The info. NULL when @a eol is not zero.
+ * @param eol Error-or-last indicator or something like that:
+ * - 0: Normal call with info.
+ * - 1: End of list, no info.
+ * - -1: Error callback, no info.
+ * @param pvUserData Pointer to our DRVHSTAUDPAENUMCBCTX structure.
+ */
+static void drvHstAudPaEnumSinkCallback(pa_context *pCtx, const pa_sink_info *pInfo, int eol, void *pvUserData)
+{
+ LogFlowFunc(("pCtx=%p pInfo=%p eol=%d pvUserData=%p\n", pCtx, pInfo, eol, pvUserData));
+ PDRVHSTAUDPAENUMCBCTX pCbCtx = (PDRVHSTAUDPAENUMCBCTX)pvUserData;
+ AssertPtrReturnVoid(pCbCtx);
+ Assert((pInfo == NULL) == (eol != 0));
+ RT_NOREF(pCtx);
+
+ if (eol == 0 && pInfo != NULL)
+ {
+ LogRel2(("PulseAudio: Sink #%u: %u Hz %uch format=%u name='%s' desc='%s' driver='%s' flags=%#x\n",
+ pInfo->index, pInfo->sample_spec.rate, pInfo->sample_spec.channels, pInfo->sample_spec.format,
+ pInfo->name, pInfo->description, pInfo->driver, pInfo->flags));
+ drvHstAudPaEnumAddDevice(pCbCtx, PDMAUDIODIR_OUT, pInfo->name, pInfo->description,
+ 0 /*cChannelsInput*/, pInfo->sample_spec.channels, pCbCtx->pszDefaultSink);
+ }
+ else if (eol == 1 && !pInfo && pCbCtx->rcEnum == VERR_AUDIO_ENUMERATION_FAILED)
+ pCbCtx->rcEnum = VINF_SUCCESS;
+
+ /* Wake up the calling thread when done: */
+ if (eol != 0)
+ pa_threaded_mainloop_signal(pCbCtx->pMainLoop, 0);
+}
+
+
+/**
+ * Enumeration callback - service info.
+ *
+ * Copy down the default names.
+ */
+static void drvHstAudPaEnumServerCallback(pa_context *pCtx, const pa_server_info *pInfo, void *pvUserData)
+{
+ LogFlowFunc(("pCtx=%p pInfo=%p pvUserData=%p\n", pCtx, pInfo, pvUserData));
+ PDRVHSTAUDPAENUMCBCTX pCbCtx = (PDRVHSTAUDPAENUMCBCTX)pvUserData;
+ AssertPtrReturnVoid(pCbCtx);
+ RT_NOREF(pCtx);
+
+ if (pInfo)
+ {
+ LogRel2(("PulseAudio: Server info: user=%s host=%s ver=%s name=%s defsink=%s defsrc=%s spec: %d %uHz %uch\n",
+ pInfo->user_name, pInfo->host_name, pInfo->server_version, pInfo->server_name,
+ pInfo->default_sink_name, pInfo->default_source_name,
+ pInfo->sample_spec.format, pInfo->sample_spec.rate, pInfo->sample_spec.channels));
+
+ Assert(!pCbCtx->pszDefaultSink);
+ Assert(!pCbCtx->pszDefaultSource);
+ Assert(pCbCtx->rcEnum == VERR_AUDIO_ENUMERATION_FAILED);
+ pCbCtx->rcEnum = VINF_SUCCESS;
+
+ if (pInfo->default_sink_name)
+ {
+ Assert(RTStrIsValidEncoding(pInfo->default_sink_name));
+ pCbCtx->pszDefaultSink = RTStrDup(pInfo->default_sink_name);
+ AssertStmt(pCbCtx->pszDefaultSink, pCbCtx->rcEnum = VERR_NO_STR_MEMORY);
+ }
+
+ if (pInfo->default_source_name)
+ {
+ Assert(RTStrIsValidEncoding(pInfo->default_source_name));
+ pCbCtx->pszDefaultSource = RTStrDup(pInfo->default_source_name);
+ AssertStmt(pCbCtx->pszDefaultSource, pCbCtx->rcEnum = VERR_NO_STR_MEMORY);
+ }
+ }
+ else
+ pCbCtx->rcEnum = VERR_INVALID_POINTER;
+
+ pa_threaded_mainloop_signal(pCbCtx->pMainLoop, 0);
+}
+
+
+/**
+ * @note Called with the PA main loop locked.
+ */
+static int drvHstAudPaEnumerate(PDRVHSTAUDPA pThis, uint32_t fEnum, PPDMAUDIOHOSTENUM pDeviceEnum)
+{
+ DRVHSTAUDPAENUMCBCTX CbCtx = { pThis->pMainLoop, fEnum, VERR_AUDIO_ENUMERATION_FAILED, NULL, NULL, pDeviceEnum };
+ bool const fLog = (fEnum & DRVHSTAUDPAENUMCB_F_LOG);
+ bool const fOnlyDefault = (fEnum & DRVHSTAUDPAENUMCB_F_DEFAULT_ONLY);
+ int rc;
+
+ /*
+ * Check if server information is available and bail out early if it isn't.
+ * This should give us a default (playback) sink and (recording) source.
+ */
+ LogRel(("PulseAudio: Retrieving server information ...\n"));
+ CbCtx.rcEnum = VERR_AUDIO_ENUMERATION_FAILED;
+ pa_operation *paOpServerInfo = pa_context_get_server_info(pThis->pContext, drvHstAudPaEnumServerCallback, &CbCtx);
+ if (paOpServerInfo)
+ rc = drvHstAudPaWaitFor(pThis, paOpServerInfo);
+ else
+ {
+ LogRel(("PulseAudio: Server information not available, skipping enumeration.\n"));
+ return VINF_SUCCESS;
+ }
+ if (RT_SUCCESS(rc))
+ rc = CbCtx.rcEnum;
+ if (RT_FAILURE(rc))
+ {
+ if (fLog)
+ LogRel(("PulseAudio: Error enumerating PulseAudio server properties: %Rrc\n", rc));
+ return rc;
+ }
+
+ /*
+ * Get info about the playback sink.
+ */
+ if (fLog && CbCtx.pszDefaultSink)
+ LogRel2(("PulseAudio: Default output sink is '%s'\n", CbCtx.pszDefaultSink));
+ else if (fLog)
+ LogRel2(("PulseAudio: No default output sink found\n"));
+
+ if (CbCtx.pszDefaultSink || !fOnlyDefault)
+ {
+ CbCtx.rcEnum = VERR_AUDIO_ENUMERATION_FAILED;
+ if (!fOnlyDefault)
+ rc = drvHstAudPaWaitFor(pThis,
+ pa_context_get_sink_info_list(pThis->pContext, drvHstAudPaEnumSinkCallback, &CbCtx));
+ else
+ rc = drvHstAudPaWaitFor(pThis, pa_context_get_sink_info_by_name(pThis->pContext, CbCtx.pszDefaultSink,
+ drvHstAudPaEnumSinkCallback, &CbCtx));
+ if (RT_SUCCESS(rc))
+ rc = CbCtx.rcEnum;
+ if (fLog && RT_FAILURE(rc))
+ LogRel(("PulseAudio: Error enumerating properties for default output sink '%s': %Rrc\n",
+ CbCtx.pszDefaultSink, rc));
+ }
+
+ /*
+ * Get info about the recording source.
+ */
+ if (fLog && CbCtx.pszDefaultSource)
+ LogRel2(("PulseAudio: Default input source is '%s'\n", CbCtx.pszDefaultSource));
+ else if (fLog)
+ LogRel2(("PulseAudio: No default input source found\n"));
+ if (CbCtx.pszDefaultSource || !fOnlyDefault)
+ {
+ CbCtx.rcEnum = VERR_AUDIO_ENUMERATION_FAILED;
+ int rc2;
+ if (!fOnlyDefault)
+ rc2 = drvHstAudPaWaitFor(pThis, pa_context_get_source_info_list(pThis->pContext,
+ drvHstAudPaEnumSourceCallback, &CbCtx));
+ else
+ rc2 = drvHstAudPaWaitFor(pThis, pa_context_get_source_info_by_name(pThis->pContext, CbCtx.pszDefaultSource,
+ drvHstAudPaEnumSourceCallback, &CbCtx));
+ if (RT_SUCCESS(rc2))
+ rc2 = CbCtx.rcEnum;
+ if (fLog && RT_FAILURE(rc2))
+ LogRel(("PulseAudio: Error enumerating properties for default input source '%s': %Rrc\n",
+ CbCtx.pszDefaultSource, rc));
+ if (RT_SUCCESS(rc))
+ rc = rc2;
+ }
+
+ /* clean up */
+ RTStrFree(CbCtx.pszDefaultSink);
+ RTStrFree(CbCtx.pszDefaultSource);
+
+ LogFlowFuncLeaveRC(rc);
+ return rc;
+}
+
+
+/**
+ * @interface_method_impl{PDMIHOSTAUDIO,pfnGetConfig}
+ */
+static DECLCALLBACK(int) drvHstAudPaHA_GetConfig(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDCFG pBackendCfg)
+{
+ PDRVHSTAUDPA pThis = RT_FROM_MEMBER(pInterface, DRVHSTAUDPA, IHostAudio);
+ AssertPtrReturn(pBackendCfg, VERR_INVALID_POINTER);
+
+ /*
+ * The configuration.
+ */
+ RTStrCopy(pBackendCfg->szName, sizeof(pBackendCfg->szName), "PulseAudio");
+ pBackendCfg->cbStream = sizeof(DRVHSTAUDPASTREAM);
+ pBackendCfg->fFlags = 0;
+ pBackendCfg->cMaxStreamsOut = UINT32_MAX;
+ pBackendCfg->cMaxStreamsIn = UINT32_MAX;
+
+#if 0
+ /*
+ * In case we want to gather info about default devices, we can do this:
+ */
+ PDMAUDIOHOSTENUM DeviceEnum;
+ PDMAudioHostEnumInit(&DeviceEnum);
+ pa_threaded_mainloop_lock(pThis->pMainLoop);
+ int rc = drvHstAudPaEnumerate(pThis, DRVHSTAUDPAENUMCB_F_DEFAULT_ONLY | DRVHSTAUDPAENUMCB_F_LOG, &DeviceEnum);
+ pa_threaded_mainloop_unlock(pThis->pMainLoop);
+ AssertRCReturn(rc, rc);
+ /** @todo do stuff with DeviceEnum. */
+ PDMAudioHostEnumDelete(&DeviceEnum);
+#else
+ RT_NOREF(pThis);
+#endif
+ return VINF_SUCCESS;
+}
+
+
+/**
+ * @interface_method_impl{PDMIHOSTAUDIO,pfnGetDevices}
+ */
+static DECLCALLBACK(int) drvHstAudPaHA_GetDevices(PPDMIHOSTAUDIO pInterface, PPDMAUDIOHOSTENUM pDeviceEnum)
+{
+ PDRVHSTAUDPA pThis = RT_FROM_MEMBER(pInterface, DRVHSTAUDPA, IHostAudio);
+ AssertPtrReturn(pDeviceEnum, VERR_INVALID_POINTER);
+ PDMAudioHostEnumInit(pDeviceEnum);
+
+ /* Refine it or something (currently only some LogRel2 stuff): */
+ pa_threaded_mainloop_lock(pThis->pMainLoop);
+ int rc = drvHstAudPaEnumerate(pThis, DRVHSTAUDPAENUMCB_F_NONE, pDeviceEnum);
+ pa_threaded_mainloop_unlock(pThis->pMainLoop);
+ return rc;
+}
+
+
+/**
+ * @interface_method_impl{PDMIHOSTAUDIO,pfnSetDevice}
+ */
+static DECLCALLBACK(int) drvHstAudPaHA_SetDevice(PPDMIHOSTAUDIO pInterface, PDMAUDIODIR enmDir, const char *pszId)
+{
+ PDRVHSTAUDPA pThis = RT_FROM_MEMBER(pInterface, DRVHSTAUDPA, IHostAudio);
+
+ /*
+ * Validate and normalize input.
+ */
+ AssertReturn(enmDir == PDMAUDIODIR_IN || enmDir == PDMAUDIODIR_OUT || enmDir == PDMAUDIODIR_DUPLEX, VERR_INVALID_PARAMETER);
+ AssertPtrNullReturn(pszId, VERR_INVALID_POINTER);
+ if (!pszId || !*pszId)
+ pszId = "";
+ else
+ {
+ size_t cch = strlen(pszId);
+ AssertReturn(cch < sizeof(pThis->szInputDev), VERR_INVALID_NAME);
+ }
+ LogFunc(("enmDir=%d pszId=%s\n", enmDir, pszId));
+
+ /*
+ * Update input.
+ */
+ if (enmDir == PDMAUDIODIR_IN || enmDir == PDMAUDIODIR_DUPLEX)
+ {
+ pa_threaded_mainloop_lock(pThis->pMainLoop);
+ if (strcmp(pThis->szInputDev, pszId) == 0)
+ pa_threaded_mainloop_unlock(pThis->pMainLoop);
+ else
+ {
+ LogRel(("PulseAudio: Changing input device: '%s' -> '%s'\n", pThis->szInputDev, pszId));
+ RTStrCopy(pThis->szInputDev, sizeof(pThis->szInputDev), pszId);
+ PPDMIHOSTAUDIOPORT pIHostAudioPort = pThis->pIHostAudioPort;
+ pa_threaded_mainloop_unlock(pThis->pMainLoop);
+ if (pIHostAudioPort)
+ {
+ LogFlowFunc(("Notifying parent driver about input device change...\n"));
+ pIHostAudioPort->pfnNotifyDeviceChanged(pIHostAudioPort, PDMAUDIODIR_IN, NULL /*pvUser*/);
+ }
+ }
+ }
+
+ /*
+ * Update output.
+ */
+ if (enmDir == PDMAUDIODIR_OUT || enmDir == PDMAUDIODIR_DUPLEX)
+ {
+ pa_threaded_mainloop_lock(pThis->pMainLoop);
+ if (strcmp(pThis->szOutputDev, pszId) == 0)
+ pa_threaded_mainloop_unlock(pThis->pMainLoop);
+ else
+ {
+ LogRel(("PulseAudio: Changing output device: '%s' -> '%s'\n", pThis->szOutputDev, pszId));
+ RTStrCopy(pThis->szOutputDev, sizeof(pThis->szOutputDev), pszId);
+ PPDMIHOSTAUDIOPORT pIHostAudioPort = pThis->pIHostAudioPort;
+ pa_threaded_mainloop_unlock(pThis->pMainLoop);
+ if (pIHostAudioPort)
+ {
+ LogFlowFunc(("Notifying parent driver about output device change...\n"));
+ pIHostAudioPort->pfnNotifyDeviceChanged(pIHostAudioPort, PDMAUDIODIR_OUT, NULL /*pvUser*/);
+ }
+ }
+ }
+
+ return VINF_SUCCESS;
+}
+
+
+
+
+/**
+ * @interface_method_impl{PDMIHOSTAUDIO,pfnGetStatus}
+ */
+static DECLCALLBACK(PDMAUDIOBACKENDSTS) drvHstAudPaHA_GetStatus(PPDMIHOSTAUDIO pInterface, PDMAUDIODIR enmDir)
+{
+ RT_NOREF(pInterface, enmDir);
+ return PDMAUDIOBACKENDSTS_RUNNING;
+}
+
+
+/**
+ * Stream status changed.
+ */
+static void drvHstAudPaStreamStateChangedCallback(pa_stream *pStream, void *pvUser)
+{
+ AssertPtrReturnVoid(pStream);
+
+ PDRVHSTAUDPA pThis = (PDRVHSTAUDPA)pvUser;
+ AssertPtrReturnVoid(pThis);
+
+ switch (pa_stream_get_state(pStream))
+ {
+ case PA_STREAM_READY:
+ case PA_STREAM_FAILED:
+ case PA_STREAM_TERMINATED:
+ drvHstAudPaSignalWaiter(pThis);
+ break;
+
+ default:
+ break;
+ }
+}
+
+
+/**
+ * Underflow notification.
+ */
+static void drvHstAudPaStreamUnderflowStatsCallback(pa_stream *pStream, void *pvContext)
+{
+ PDRVHSTAUDPASTREAM pStreamPA = (PDRVHSTAUDPASTREAM)pvContext;
+ AssertPtrReturnVoid(pStreamPA);
+ AssertPtrReturnVoid(pStreamPA->pDrv);
+
+ /* This may happen when draining/corking, so don't count those. */
+ if (!pStreamPA->pDrainOp)
+ STAM_REL_COUNTER_INC(&pStreamPA->pDrv->StatUnderruns);
+
+ pStreamPA->cUnderflows++;
+
+ LogRel2(("PulseAudio: Warning: Hit underflow #%RU32%s%s\n", pStreamPA->cUnderflows,
+ pStreamPA->pDrainOp && pa_operation_get_state(pStreamPA->pDrainOp) == PA_OPERATION_RUNNING ? " (draining)" : "",
+ pStreamPA->pCorkOp && pa_operation_get_state(pStreamPA->pCorkOp) == PA_OPERATION_RUNNING ? " (corking)" : "" ));
+
+ if (LogRelIs2Enabled() || LogIs2Enabled())
+ {
+ pa_usec_t cUsLatency = 0;
+ int fNegative = 0;
+ pa_stream_get_latency(pStream, &cUsLatency, &fNegative);
+ LogRel2(("PulseAudio: Latency now is %'RU64 us\n", cUsLatency));
+
+ if (LogRelIs2Enabled())
+ {
+ const pa_timing_info *pTInfo = pa_stream_get_timing_info(pStream);
+ AssertReturnVoid(pTInfo);
+ const pa_sample_spec *pSpec = pa_stream_get_sample_spec(pStream);
+ AssertReturnVoid(pSpec);
+ LogRel2(("PulseAudio: Timing info: writepos=%'RU64 us, readpost=%'RU64 us, latency=%'RU64 us (%RU32Hz %RU8ch)\n",
+ pa_bytes_to_usec(pTInfo->write_index, pSpec), pa_bytes_to_usec(pTInfo->read_index, pSpec),
+ cUsLatency, pSpec->rate, pSpec->channels));
+ }
+
+#ifdef LOG_ENABLED
+ Log2Func(("age=%'RU64 us\n", pa_rtclock_now() - pStreamPA->tsStartUs));
+#endif
+ }
+}
+
+
+/**
+ * Overflow notification.
+ */
+static void drvHstAudPaStreamOverflowStatsCallback(pa_stream *pStream, void *pvContext)
+{
+ PDRVHSTAUDPASTREAM pStreamPA = (PDRVHSTAUDPASTREAM)pvContext;
+ AssertPtrReturnVoid(pStreamPA);
+ AssertPtrReturnVoid(pStreamPA->pDrv);
+
+ STAM_REL_COUNTER_INC(&pStreamPA->pDrv->StatOverruns);
+ LogRel2(("PulseAudio: Warning: Hit overflow.\n"));
+ RT_NOREF(pStream);
+}
+
+
+#ifdef DEBUG
+/**
+ * Debug PA callback: Need data to output.
+ */
+static void drvHstAudPaStreamReqWriteDebugCallback(pa_stream *pStream, size_t cbLen, void *pvContext)
+{
+ RT_NOREF(cbLen, pvContext);
+ pa_usec_t cUsLatency = 0;
+ int fNegative = 0;
+ int rcPa = pa_stream_get_latency(pStream, &cUsLatency, &fNegative);
+ Log2Func(("Requesting %zu bytes; Latency: %'RU64 us (rcPa=%d n=%d)\n", cbLen, cUsLatency, rcPa, fNegative));
+}
+#endif /* DEBUG */
+
+/**
+ * Converts from PDM PCM properties to pulse audio format.
+ *
+ * Worker for the stream creation code.
+ *
+ * @returns PA format.
+ * @retval PA_SAMPLE_INVALID if format not supported.
+ * @param pProps The PDM audio source properties.
+ */
+static pa_sample_format_t drvHstAudPaPropsToPulse(PCPDMAUDIOPCMPROPS pProps)
+{
+ switch (PDMAudioPropsSampleSize(pProps))
+ {
+ case 1:
+ if (!PDMAudioPropsIsSigned(pProps))
+ return PA_SAMPLE_U8;
+ break;
+
+ case 2:
+ if (PDMAudioPropsIsSigned(pProps))
+ return PDMAudioPropsIsLittleEndian(pProps) ? PA_SAMPLE_S16LE : PA_SAMPLE_S16BE;
+ break;
+
+#ifdef PA_SAMPLE_S32LE
+ case 4:
+ if (PDMAudioPropsIsSigned(pProps))
+ return PDMAudioPropsIsLittleEndian(pProps) ? PA_SAMPLE_S32LE : PA_SAMPLE_S32BE;
+ break;
+#endif
+ }
+
+ AssertMsgFailed(("%RU8%s not supported\n", PDMAudioPropsSampleSize(pProps), PDMAudioPropsIsSigned(pProps) ? "S" : "U"));
+ return PA_SAMPLE_INVALID;
+}
+
+
+/**
+ * Converts from pulse audio sample specification to PDM PCM audio properties.
+ *
+ * Worker for the stream creation code.
+ *
+ * @returns VBox status code.
+ * @param pProps The PDM audio source properties.
+ * @param enmPulseFmt The PA format.
+ * @param cChannels The number of channels.
+ * @param uHz The frequency.
+ */
+static int drvHstAudPaToAudioProps(PPDMAUDIOPCMPROPS pProps, pa_sample_format_t enmPulseFmt, uint8_t cChannels, uint32_t uHz)
+{
+ AssertReturn(cChannels > 0, VERR_INVALID_PARAMETER);
+ AssertReturn(cChannels < 16, VERR_INVALID_PARAMETER);
+
+ switch (enmPulseFmt)
+ {
+ case PA_SAMPLE_U8:
+ PDMAudioPropsInit(pProps, 1 /*8-bit*/, false /*signed*/, cChannels, uHz);
+ break;
+
+ case PA_SAMPLE_S16LE:
+ PDMAudioPropsInitEx(pProps, 2 /*16-bit*/, true /*signed*/, cChannels, uHz, true /*fLittleEndian*/, false /*fRaw*/);
+ break;
+
+ case PA_SAMPLE_S16BE:
+ PDMAudioPropsInitEx(pProps, 2 /*16-bit*/, true /*signed*/, cChannels, uHz, false /*fLittleEndian*/, false /*fRaw*/);
+ break;
+
+#ifdef PA_SAMPLE_S32LE
+ case PA_SAMPLE_S32LE:
+ PDMAudioPropsInitEx(pProps, 4 /*32-bit*/, true /*signed*/, cChannels, uHz, true /*fLittleEndian*/, false /*fRaw*/);
+ break;
+#endif
+
+#ifdef PA_SAMPLE_S32BE
+ case PA_SAMPLE_S32BE:
+ PDMAudioPropsInitEx(pProps, 4 /*32-bit*/, true /*signed*/, cChannels, uHz, false /*fLittleEndian*/, false /*fRaw*/);
+ break;
+#endif
+
+ default:
+ AssertLogRelMsgFailed(("PulseAudio: Format (%d) not supported\n", enmPulseFmt));
+ return VERR_NOT_SUPPORTED;
+ }
+
+ return VINF_SUCCESS;
+}
+
+
+#if 0 /* experiment */
+/**
+ * Completion callback used with pa_stream_set_buffer_attr().
+ */
+static void drvHstAudPaStreamSetBufferAttrCompletionCallback(pa_stream *pStream, int fSuccess, void *pvUser)
+{
+ PDRVHSTAUDPA pThis = (PDRVHSTAUDPA)pvUser;
+ LogFlowFunc(("fSuccess=%d\n", fSuccess));
+ pa_threaded_mainloop_signal(pThis->pMainLoop, 0 /*fWaitForAccept*/);
+ RT_NOREF(pStream);
+}
+#endif
+
+
+/**
+ * Worker that does the actual creation of an PA stream.
+ *
+ * @returns VBox status code.
+ * @param pThis Our driver instance data.
+ * @param pStreamPA Our stream data.
+ * @param pszName How we name the stream.
+ * @param pCfgAcq The requested stream properties, the Props member is
+ * updated upon successful return.
+ *
+ * @note Caller owns the mainloop lock.
+ */
+static int drvHstAudPaStreamCreateLocked(PDRVHSTAUDPA pThis, PDRVHSTAUDPASTREAM pStreamPA,
+ const char *pszName, PPDMAUDIOSTREAMCFG pCfgAcq)
+{
+ /*
+ * Create the stream.
+ */
+ pa_stream *pStream = pa_stream_new(pThis->pContext, pszName, &pStreamPA->SampleSpec, &pStreamPA->ChannelMap);
+ if (!pStream)
+ {
+ LogRel(("PulseAudio: Failed to create stream '%s': %s (%d)\n",
+ pszName, pa_strerror(pa_context_errno(pThis->pContext)), pa_context_errno(pThis->pContext)));
+ return VERR_AUDIO_STREAM_COULD_NOT_CREATE;
+ }
+
+ /*
+ * Set the state callback, and in debug builds a few more...
+ */
+ pa_stream_set_state_callback(pStream, drvHstAudPaStreamStateChangedCallback, pThis);
+ pa_stream_set_underflow_callback(pStream, drvHstAudPaStreamUnderflowStatsCallback, pStreamPA);
+ pa_stream_set_overflow_callback(pStream, drvHstAudPaStreamOverflowStatsCallback, pStreamPA);
+#ifdef DEBUG
+ pa_stream_set_write_callback(pStream, drvHstAudPaStreamReqWriteDebugCallback, pStreamPA);
+#endif
+
+ /*
+ * Connect the stream.
+ */
+ int rc;
+ unsigned const fFlags = PA_STREAM_START_CORKED /* Require explicit starting (uncorking). */
+ /* For using pa_stream_get_latency() and pa_stream_get_time(). */
+ | PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_AUTO_TIMING_UPDATE
+#if PA_API_VERSION >= 12
+ | PA_STREAM_ADJUST_LATENCY
+#endif
+ ;
+ if (pCfgAcq->enmDir == PDMAUDIODIR_IN)
+ {
+ LogFunc(("Input stream attributes: maxlength=%d fragsize=%d\n",
+ pStreamPA->BufAttr.maxlength, pStreamPA->BufAttr.fragsize));
+ rc = pa_stream_connect_record(pStream, pThis->szInputDev[0] ? pThis->szInputDev : NULL,
+ &pStreamPA->BufAttr, (pa_stream_flags_t)fFlags);
+ }
+ else
+ {
+ LogFunc(("Output buffer attributes: maxlength=%d tlength=%d prebuf=%d minreq=%d\n",
+ pStreamPA->BufAttr.maxlength, pStreamPA->BufAttr.tlength, pStreamPA->BufAttr.prebuf, pStreamPA->BufAttr.minreq));
+ rc = pa_stream_connect_playback(pStream, pThis->szOutputDev[0] ? pThis->szOutputDev : NULL, &pStreamPA->BufAttr, (pa_stream_flags_t)fFlags,
+ NULL /*volume*/, NULL /*sync_stream*/);
+ }
+ if (rc >= 0)
+ {
+ /*
+ * Wait for the stream to become ready.
+ */
+ uint64_t const nsStart = RTTimeNanoTS();
+ pa_stream_state_t enmStreamState;
+ while ( (enmStreamState = pa_stream_get_state(pStream)) != PA_STREAM_READY
+ && PA_STREAM_IS_GOOD(enmStreamState)
+ && RTTimeNanoTS() - nsStart < RT_NS_10SEC /* not really timed */ )
+ drvHstAudPaMainloopWait(pThis);
+ if (enmStreamState == PA_STREAM_READY)
+ {
+ LogFunc(("Connecting stream took %'RU64 ns\n", RTTimeNanoTS() - nsStart));
+#ifdef LOG_ENABLED
+ pStreamPA->tsStartUs = pa_rtclock_now();
+#endif
+ /*
+ * Update the buffer attributes.
+ */
+ const pa_buffer_attr *pBufAttribs = pa_stream_get_buffer_attr(pStream);
+#if 0 /* Experiment for getting tlength closer to what we requested (ADJUST_LATENCY effect).
+ Will slow down stream creation, so not pursued any further at present. */
+ if ( pCfgAcq->enmDir == PDMAUDIODIR_OUT
+ && pBufAttribs
+ && pBufAttribs->tlength < pStreamPA->BufAttr.tlength)
+ {
+ pStreamPA->BufAttr.maxlength += (pStreamPA->BufAttr.tlength - pBufAttribs->tlength) * 2;
+ pStreamPA->BufAttr.tlength += (pStreamPA->BufAttr.tlength - pBufAttribs->tlength) * 2;
+ LogRel(("Before pa_stream_set_buffer_attr: tlength=%#x (trying =%#x)\n", pBufAttribs->tlength, pStreamPA->BufAttr.tlength));
+ drvHstAudPaWaitFor(pThis, pa_stream_set_buffer_attr(pStream, &pStreamPA->BufAttr,
+ drvHstAudPaStreamSetBufferAttrCompletionCallback, pThis));
+ pBufAttribs = pa_stream_get_buffer_attr(pStream);
+ LogRel(("After pa_stream_set_buffer_attr: tlength=%#x\n", pBufAttribs->tlength));
+ }
+#endif
+ AssertPtr(pBufAttribs);
+ if (pBufAttribs)
+ {
+ pStreamPA->BufAttr = *pBufAttribs;
+ LogFunc(("Obtained %s buffer attributes: maxlength=%RU32 tlength=%RU32 prebuf=%RU32 minreq=%RU32 fragsize=%RU32\n",
+ pCfgAcq->enmDir == PDMAUDIODIR_IN ? "input" : "output", pBufAttribs->maxlength, pBufAttribs->tlength,
+ pBufAttribs->prebuf, pBufAttribs->minreq, pBufAttribs->fragsize));
+
+ /*
+ * Convert the sample spec back to PDM speak.
+ * Note! This isn't strictly speaking needed as SampleSpec has *not* been
+ * modified since the caller converted it from pCfgReq.
+ */
+ rc = drvHstAudPaToAudioProps(&pCfgAcq->Props, pStreamPA->SampleSpec.format,
+ pStreamPA->SampleSpec.channels, pStreamPA->SampleSpec.rate);
+ if (RT_SUCCESS(rc))
+ {
+ pStreamPA->pStream = pStream;
+ LogFlowFunc(("returns VINF_SUCCESS\n"));
+ return VINF_SUCCESS;
+ }
+ }
+ else
+ {
+ LogRelMax(99, ("PulseAudio: Failed to get buffer attribs for stream '%s': %s (%d)\n",
+ pszName, pa_strerror(pa_context_errno(pThis->pContext)), pa_context_errno(pThis->pContext)));
+ rc = VERR_AUDIO_STREAM_COULD_NOT_CREATE;
+ }
+ }
+ else
+ {
+ LogRelMax(99, ("PulseAudio: Failed to initialize stream '%s': state=%d, waited %'RU64 ns\n",
+ pszName, enmStreamState, RTTimeNanoTS() - nsStart));
+ rc = VERR_AUDIO_STREAM_COULD_NOT_CREATE;
+ }
+ pa_stream_disconnect(pStream);
+ }
+ else
+ {
+ LogRelMax(99, ("PulseAudio: Could not connect %s stream '%s': %s (%d/%d)\n",
+ pCfgAcq->enmDir == PDMAUDIODIR_IN ? "input" : "output",
+ pszName, pa_strerror(pa_context_errno(pThis->pContext)), pa_context_errno(pThis->pContext), rc));
+ rc = VERR_AUDIO_STREAM_COULD_NOT_CREATE;
+ }
+
+ pa_stream_unref(pStream);
+ Assert(RT_FAILURE_NP(rc));
+ LogFlowFunc(("returns %Rrc\n", rc));
+ return rc;
+}
+
+
+/**
+ * Translates a PDM channel ID to a PA channel position.
+ *
+ * @returns PA channel position, INVALID if no mapping found.
+ */
+static pa_channel_position_t drvHstAudPaConvertChannelId(uint8_t idChannel)
+{
+ switch (idChannel)
+ {
+ case PDMAUDIOCHANNELID_FRONT_LEFT: return PA_CHANNEL_POSITION_FRONT_LEFT;
+ case PDMAUDIOCHANNELID_FRONT_RIGHT: return PA_CHANNEL_POSITION_FRONT_RIGHT;
+ case PDMAUDIOCHANNELID_FRONT_CENTER: return PA_CHANNEL_POSITION_FRONT_CENTER;
+ case PDMAUDIOCHANNELID_LFE: return PA_CHANNEL_POSITION_LFE;
+ case PDMAUDIOCHANNELID_REAR_LEFT: return PA_CHANNEL_POSITION_REAR_LEFT;
+ case PDMAUDIOCHANNELID_REAR_RIGHT: return PA_CHANNEL_POSITION_REAR_RIGHT;
+ case PDMAUDIOCHANNELID_FRONT_LEFT_OF_CENTER: return PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER;
+ case PDMAUDIOCHANNELID_FRONT_RIGHT_OF_CENTER: return PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER;
+ case PDMAUDIOCHANNELID_REAR_CENTER: return PA_CHANNEL_POSITION_REAR_CENTER;
+ case PDMAUDIOCHANNELID_SIDE_LEFT: return PA_CHANNEL_POSITION_SIDE_LEFT;
+ case PDMAUDIOCHANNELID_SIDE_RIGHT: return PA_CHANNEL_POSITION_SIDE_RIGHT;
+ case PDMAUDIOCHANNELID_TOP_CENTER: return PA_CHANNEL_POSITION_TOP_CENTER;
+ case PDMAUDIOCHANNELID_FRONT_LEFT_HEIGHT: return PA_CHANNEL_POSITION_TOP_FRONT_LEFT;
+ case PDMAUDIOCHANNELID_FRONT_CENTER_HEIGHT: return PA_CHANNEL_POSITION_TOP_FRONT_CENTER;
+ case PDMAUDIOCHANNELID_FRONT_RIGHT_HEIGHT: return PA_CHANNEL_POSITION_TOP_FRONT_RIGHT;
+ case PDMAUDIOCHANNELID_REAR_LEFT_HEIGHT: return PA_CHANNEL_POSITION_TOP_REAR_LEFT;
+ case PDMAUDIOCHANNELID_REAR_CENTER_HEIGHT: return PA_CHANNEL_POSITION_TOP_REAR_CENTER;
+ case PDMAUDIOCHANNELID_REAR_RIGHT_HEIGHT: return PA_CHANNEL_POSITION_TOP_REAR_RIGHT;
+ default: return PA_CHANNEL_POSITION_INVALID;
+ }
+}
+
+
+/**
+ * Translates a PA channel position to a PDM channel ID.
+ *
+ * @returns PDM channel ID, UNKNOWN if no mapping found.
+ */
+static PDMAUDIOCHANNELID drvHstAudPaConvertChannelPos(pa_channel_position_t enmChannelPos)
+{
+ switch (enmChannelPos)
+ {
+ case PA_CHANNEL_POSITION_INVALID: return PDMAUDIOCHANNELID_INVALID;
+ case PA_CHANNEL_POSITION_MONO: return PDMAUDIOCHANNELID_MONO;
+ case PA_CHANNEL_POSITION_FRONT_LEFT: return PDMAUDIOCHANNELID_FRONT_LEFT;
+ case PA_CHANNEL_POSITION_FRONT_RIGHT: return PDMAUDIOCHANNELID_FRONT_RIGHT;
+ case PA_CHANNEL_POSITION_FRONT_CENTER: return PDMAUDIOCHANNELID_FRONT_CENTER;
+ case PA_CHANNEL_POSITION_LFE: return PDMAUDIOCHANNELID_LFE;
+ case PA_CHANNEL_POSITION_REAR_LEFT: return PDMAUDIOCHANNELID_REAR_LEFT;
+ case PA_CHANNEL_POSITION_REAR_RIGHT: return PDMAUDIOCHANNELID_REAR_RIGHT;
+ case PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER: return PDMAUDIOCHANNELID_FRONT_LEFT_OF_CENTER;
+ case PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER: return PDMAUDIOCHANNELID_FRONT_RIGHT_OF_CENTER;
+ case PA_CHANNEL_POSITION_REAR_CENTER: return PDMAUDIOCHANNELID_REAR_CENTER;
+ case PA_CHANNEL_POSITION_SIDE_LEFT: return PDMAUDIOCHANNELID_SIDE_LEFT;
+ case PA_CHANNEL_POSITION_SIDE_RIGHT: return PDMAUDIOCHANNELID_SIDE_RIGHT;
+ case PA_CHANNEL_POSITION_TOP_CENTER: return PDMAUDIOCHANNELID_TOP_CENTER;
+ case PA_CHANNEL_POSITION_TOP_FRONT_LEFT: return PDMAUDIOCHANNELID_FRONT_LEFT_HEIGHT;
+ case PA_CHANNEL_POSITION_TOP_FRONT_CENTER: return PDMAUDIOCHANNELID_FRONT_CENTER_HEIGHT;
+ case PA_CHANNEL_POSITION_TOP_FRONT_RIGHT: return PDMAUDIOCHANNELID_FRONT_RIGHT_HEIGHT;
+ case PA_CHANNEL_POSITION_TOP_REAR_LEFT: return PDMAUDIOCHANNELID_REAR_LEFT_HEIGHT;
+ case PA_CHANNEL_POSITION_TOP_REAR_CENTER: return PDMAUDIOCHANNELID_REAR_CENTER_HEIGHT;
+ case PA_CHANNEL_POSITION_TOP_REAR_RIGHT: return PDMAUDIOCHANNELID_REAR_RIGHT_HEIGHT;
+ default: return PDMAUDIOCHANNELID_UNKNOWN;
+ }
+}
+
+
+
+/**
+ * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamCreate}
+ */
+static DECLCALLBACK(int) drvHstAudPaHA_StreamCreate(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream,
+ PCPDMAUDIOSTREAMCFG pCfgReq, PPDMAUDIOSTREAMCFG pCfgAcq)
+{
+ PDRVHSTAUDPA pThis = RT_FROM_MEMBER(pInterface, DRVHSTAUDPA, IHostAudio);
+ PDRVHSTAUDPASTREAM pStreamPA = (PDRVHSTAUDPASTREAM)pStream;
+ AssertPtrReturn(pStreamPA, VERR_INVALID_POINTER);
+ AssertPtrReturn(pCfgReq, VERR_INVALID_POINTER);
+ AssertPtrReturn(pCfgAcq, VERR_INVALID_POINTER);
+ AssertReturn(pCfgReq->enmDir == PDMAUDIODIR_IN || pCfgReq->enmDir == PDMAUDIODIR_OUT, VERR_INVALID_PARAMETER);
+ Assert(PDMAudioStrmCfgEquals(pCfgReq, pCfgAcq));
+ int rc;
+
+ /*
+ * Prepare name, sample spec and the stream instance data.
+ */
+ char szName[256];
+ RTStrPrintf(szName, sizeof(szName), "VirtualBox %s [%s]", PDMAudioPathGetName(pCfgReq->enmPath), pThis->szStreamName);
+
+ pStreamPA->pDrv = pThis;
+ pStreamPA->pDrainOp = NULL;
+ pStreamPA->pbPeekBuf = NULL;
+ pStreamPA->SampleSpec.rate = PDMAudioPropsHz(&pCfgReq->Props);
+ pStreamPA->SampleSpec.channels = PDMAudioPropsChannels(&pCfgReq->Props);
+ pStreamPA->SampleSpec.format = drvHstAudPaPropsToPulse(&pCfgReq->Props);
+
+ /*
+ * Initialize the channelmap. This may change the channel count.
+ */
+ AssertCompile(RT_ELEMENTS(pStreamPA->ChannelMap.map) >= PDMAUDIO_MAX_CHANNELS);
+ uint8_t const cSrcChannels = pStreamPA->ChannelMap.channels = PDMAudioPropsChannels(&pCfgReq->Props);
+ uintptr_t iDst = 0;
+ if (cSrcChannels == 1 && pCfgReq->Props.aidChannels[0] == PDMAUDIOCHANNELID_MONO)
+ pStreamPA->ChannelMap.map[iDst++] = PA_CHANNEL_POSITION_MONO;
+ else
+ {
+ uintptr_t iSrc;
+ for (iSrc = iDst = 0; iSrc < cSrcChannels; iSrc++)
+ {
+ pStreamPA->ChannelMap.map[iDst] = drvHstAudPaConvertChannelId(pCfgReq->Props.aidChannels[iSrc]);
+ if (pStreamPA->ChannelMap.map[iDst] != PA_CHANNEL_POSITION_INVALID)
+ iDst++;
+ else
+ {
+ LogRel2(("PulseAudio: Dropping channel #%u (%d/%s)\n", iSrc, pCfgReq->Props.aidChannels[iSrc],
+ PDMAudioChannelIdGetName((PDMAUDIOCHANNELID)pCfgReq->Props.aidChannels[iSrc])));
+ pStreamPA->ChannelMap.channels--;
+ pStreamPA->SampleSpec.channels--;
+ PDMAudioPropsSetChannels(&pCfgAcq->Props, pStreamPA->SampleSpec.channels);
+ }
+ }
+ Assert(iDst == pStreamPA->ChannelMap.channels);
+ }
+ while (iDst < RT_ELEMENTS(pStreamPA->ChannelMap.map))
+ pStreamPA->ChannelMap.map[iDst++] = PA_CHANNEL_POSITION_INVALID;
+
+ LogFunc(("Opening '%s', rate=%dHz, channels=%d (%d), format=%s\n", szName, pStreamPA->SampleSpec.rate,
+ pStreamPA->SampleSpec.channels, cSrcChannels, pa_sample_format_to_string(pStreamPA->SampleSpec.format)));
+
+ if (pa_sample_spec_valid(&pStreamPA->SampleSpec))
+ {
+ /*
+ * Convert the requested buffer parameters to PA bytes.
+ */
+ uint32_t const cbBuffer = pa_usec_to_bytes(PDMAudioPropsFramesToMicro(&pCfgAcq->Props,
+ pCfgReq->Backend.cFramesBufferSize),
+ &pStreamPA->SampleSpec);
+ uint32_t const cbPreBuffer = pa_usec_to_bytes(PDMAudioPropsFramesToMicro(&pCfgAcq->Props,
+ pCfgReq->Backend.cFramesPreBuffering),
+ &pStreamPA->SampleSpec);
+ uint32_t const cbSchedHint = pa_usec_to_bytes(pCfgReq->Device.cMsSchedulingHint * RT_US_1MS, &pStreamPA->SampleSpec);
+ RT_NOREF(cbBuffer, cbSchedHint, cbPreBuffer);
+
+ /*
+ * Set up buffer attributes according to the stream type.
+ */
+ if (pCfgReq->enmDir == PDMAUDIODIR_IN)
+ {
+ /* Set maxlength to the requested buffer size. */
+ pStreamPA->BufAttr.maxlength = cbBuffer;
+
+ /* Set the fragment size according to the scheduling hint (forget
+ cFramesPeriod, it's generally rubbish on input). */
+ pStreamPA->BufAttr.fragsize = cbSchedHint;
+
+ /* (tlength, minreq and prebuf are playback only) */
+ LogRel2(("PulseAudio: Requesting: BufAttr: fragsize=%#RX32 maxLength=%#RX32\n",
+ pStreamPA->BufAttr.fragsize, pStreamPA->BufAttr.maxlength));
+ }
+ else
+ {
+ /* Set tlength to the desired buffer size as PA doesn't have any way
+ of telling us if anything beyond tlength is writable or not (see
+ drvHstAudPaStreamGetWritableLocked for more). Because of the
+ ADJUST_LATENCY flag, this value will be adjusted down, so we'll
+ end up with less buffer than what we requested, however it should
+ probably reflect the actual latency a bit closer. Probably not
+ worth trying to adjust this via pa_stream_set_buffer_attr. */
+ pStreamPA->BufAttr.tlength = cbBuffer;
+
+ /* Set maxlength to the same as tlength as we won't ever write more
+ than tlength. */
+ pStreamPA->BufAttr.maxlength = pStreamPA->BufAttr.tlength;
+
+ /* According to vlc, pulseaudio goes berserk if the minreq is not
+ significantly smaller than half of tlength. They use a 1:3 ratio
+ between minreq and tlength. Traditionally, we've used to just
+ pass the period value here, however the quality of the incoming
+ cFramesPeriod value is so variable that just ignore it. This
+ minreq value is mainly about updating the pa_stream_writable_size
+ return value, so it makes sense that it need to be well below
+ half of the buffer length, otherwise we will think the buffer
+ is full for too long when it isn't.
+
+ The DMA scheduling hint is often a much better indicator. Just
+ to avoid generating too much IPC, limit this to 10 ms. */
+ uint32_t const cbMinUpdate = pa_usec_to_bytes(RT_US_10MS, &pStreamPA->SampleSpec);
+ pStreamPA->BufAttr.minreq = RT_MIN(RT_MAX(cbSchedHint, cbMinUpdate),
+ pStreamPA->BufAttr.tlength / 4);
+
+ /* Just pass along the requested pre-buffering size. This seems
+ typically to be unaltered by pa_stream_connect_playback. Not
+ sure if tlength is perhaps adjusted relative to it... Ratio
+ seen here is prebuf=93.75% of tlength. This isn't entirely
+ optimal as we use 50% by default (see DrvAudio) so that there
+ is equal room for the guest to run too fast and too slow. Not
+ much we can do about it w/o slowing down stream creation. */
+ pStreamPA->BufAttr.prebuf = cbPreBuffer;
+
+ /* (fragsize is capture only) */
+ LogRel2(("PulseAudio: Requesting: BufAttr: tlength=%#RX32 minReq=%#RX32 prebuf=%#RX32 maxLength=%#RX32\n",
+ pStreamPA->BufAttr.tlength, pStreamPA->BufAttr.minreq, pStreamPA->BufAttr.prebuf, pStreamPA->BufAttr.maxlength));
+ }
+
+ /*
+ * Do the actual PA stream creation.
+ */
+ pa_threaded_mainloop_lock(pThis->pMainLoop);
+ rc = drvHstAudPaStreamCreateLocked(pThis, pStreamPA, szName, pCfgAcq);
+ pa_threaded_mainloop_unlock(pThis->pMainLoop);
+ if (RT_SUCCESS(rc))
+ {
+ /*
+ * Set the acquired stream config according to the actual buffer
+ * attributes we got and the stream type.
+ *
+ * Note! We use maxlength for input buffer and tlength for the
+ * output buffer size. See above for why.
+ */
+ if (pCfgReq->enmDir == PDMAUDIODIR_IN)
+ {
+ LogRel2(("PulseAudio: Got: BufAttr: fragsize=%#RX32 maxLength=%#RX32\n",
+ pStreamPA->BufAttr.fragsize, pStreamPA->BufAttr.maxlength));
+ pCfgAcq->Backend.cFramesPeriod = PDMAudioPropsBytesToFrames(&pCfgAcq->Props, pStreamPA->BufAttr.fragsize);
+ pCfgAcq->Backend.cFramesBufferSize = pStreamPA->BufAttr.maxlength != UINT32_MAX /* paranoia */
+ ? PDMAudioPropsBytesToFrames(&pCfgAcq->Props, pStreamPA->BufAttr.maxlength)
+ : pCfgAcq->Backend.cFramesPeriod * 3 /* whatever */;
+ pCfgAcq->Backend.cFramesPreBuffering = pCfgReq->Backend.cFramesPreBuffering * pCfgAcq->Backend.cFramesBufferSize
+ / RT_MAX(pCfgReq->Backend.cFramesBufferSize, 1);
+ }
+ else
+ {
+ LogRel2(("PulseAudio: Got: BufAttr: tlength=%#RX32 minReq=%#RX32 prebuf=%#RX32 maxLength=%#RX32\n",
+ pStreamPA->BufAttr.tlength, pStreamPA->BufAttr.minreq, pStreamPA->BufAttr.prebuf, pStreamPA->BufAttr.maxlength));
+ pCfgAcq->Backend.cFramesPeriod = PDMAudioPropsBytesToFrames(&pCfgAcq->Props, pStreamPA->BufAttr.minreq);
+ pCfgAcq->Backend.cFramesBufferSize = PDMAudioPropsBytesToFrames(&pCfgAcq->Props, pStreamPA->BufAttr.tlength);
+ pCfgAcq->Backend.cFramesPreBuffering = PDMAudioPropsBytesToFrames(&pCfgAcq->Props, pStreamPA->BufAttr.prebuf);
+
+ LogRel2(("PulseAudio: Initial output latency is %RU64 us (%RU32 bytes)\n",
+ PDMAudioPropsBytesToMicro(&pCfgAcq->Props, pStreamPA->BufAttr.tlength), pStreamPA->BufAttr.tlength));
+ }
+
+ /*
+ * Translate back the channel mapping.
+ */
+ for (iDst = 0; iDst < pStreamPA->ChannelMap.channels; iDst++)
+ pCfgAcq->Props.aidChannels[iDst] = drvHstAudPaConvertChannelPos(pStreamPA->ChannelMap.map[iDst]);
+ while (iDst < RT_ELEMENTS(pCfgAcq->Props.aidChannels))
+ pCfgAcq->Props.aidChannels[iDst++] = PDMAUDIOCHANNELID_INVALID;
+
+ PDMAudioStrmCfgCopy(&pStreamPA->Cfg, pCfgAcq);
+ }
+ }
+ else
+ {
+ LogRel(("PulseAudio: Unsupported sample specification for stream '%s'\n", szName));
+ rc = VERR_AUDIO_STREAM_COULD_NOT_CREATE;
+ }
+
+ LogFlowFuncLeaveRC(rc);
+ return rc;
+}
+
+/**
+ * Cancel and release any pending stream requests (drain and cork/uncork).
+ *
+ * @note Caller has locked the mainloop.
+ */
+static void drvHstAudPaStreamCancelAndReleaseOperations(PDRVHSTAUDPASTREAM pStreamPA)
+{
+ if (pStreamPA->pDrainOp)
+ {
+ LogFlowFunc(("drain operation (%p) status: %d\n", pStreamPA->pDrainOp, pa_operation_get_state(pStreamPA->pDrainOp)));
+ pa_operation_cancel(pStreamPA->pDrainOp);
+ pa_operation_unref(pStreamPA->pDrainOp);
+ pStreamPA->pDrainOp = NULL;
+ }
+
+ if (pStreamPA->pCorkOp)
+ {
+ LogFlowFunc(("cork operation (%p) status: %d\n", pStreamPA->pCorkOp, pa_operation_get_state(pStreamPA->pCorkOp)));
+ pa_operation_cancel(pStreamPA->pCorkOp);
+ pa_operation_unref(pStreamPA->pCorkOp);
+ pStreamPA->pCorkOp = NULL;
+ }
+
+ if (pStreamPA->pTriggerOp)
+ {
+ LogFlowFunc(("trigger operation (%p) status: %d\n", pStreamPA->pTriggerOp, pa_operation_get_state(pStreamPA->pTriggerOp)));
+ pa_operation_cancel(pStreamPA->pTriggerOp);
+ pa_operation_unref(pStreamPA->pTriggerOp);
+ pStreamPA->pTriggerOp = NULL;
+ }
+}
+
+
+/**
+ * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamDestroy}
+ */
+static DECLCALLBACK(int) drvHstAudPaHA_StreamDestroy(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, bool fImmediate)
+{
+ PDRVHSTAUDPA pThis = RT_FROM_MEMBER(pInterface, DRVHSTAUDPA, IHostAudio);
+ PDRVHSTAUDPASTREAM pStreamPA = (PDRVHSTAUDPASTREAM)pStream;
+ AssertPtrReturn(pStreamPA, VERR_INVALID_POINTER);
+ RT_NOREF(fImmediate);
+
+ if (pStreamPA->pStream)
+ {
+ pa_threaded_mainloop_lock(pThis->pMainLoop);
+
+ drvHstAudPaStreamCancelAndReleaseOperations(pStreamPA);
+ pa_stream_disconnect(pStreamPA->pStream);
+
+ pa_stream_unref(pStreamPA->pStream);
+ pStreamPA->pStream = NULL;
+
+ pa_threaded_mainloop_unlock(pThis->pMainLoop);
+ }
+
+ return VINF_SUCCESS;
+}
+
+
+/**
+ * Common worker for the cork/uncork completion callbacks.
+ * @note This is fully async, so nobody is waiting for this.
+ */
+static void drvHstAudPaStreamCorkUncorkCommon(PDRVHSTAUDPASTREAM pStreamPA, int fSuccess, const char *pszOperation)
+{
+ AssertPtrReturnVoid(pStreamPA);
+ LogFlowFunc(("%s '%s': fSuccess=%RTbool\n", pszOperation, pStreamPA->Cfg.szName, fSuccess));
+
+ if (!fSuccess)
+ drvHstAudPaError(pStreamPA->pDrv, "%s stream '%s' failed", pszOperation, pStreamPA->Cfg.szName);
+
+ if (pStreamPA->pCorkOp)
+ {
+ pa_operation_unref(pStreamPA->pCorkOp);
+ pStreamPA->pCorkOp = NULL;
+ }
+}
+
+
+/**
+ * Completion callback used with pa_stream_cork(,false,).
+ */
+static void drvHstAudPaStreamUncorkCompletionCallback(pa_stream *pStream, int fSuccess, void *pvUser)
+{
+ RT_NOREF(pStream);
+ drvHstAudPaStreamCorkUncorkCommon((PDRVHSTAUDPASTREAM)pvUser, fSuccess, "Uncorking");
+}
+
+
+/**
+ * Completion callback used with pa_stream_cork(,true,).
+ */
+static void drvHstAudPaStreamCorkCompletionCallback(pa_stream *pStream, int fSuccess, void *pvUser)
+{
+ RT_NOREF(pStream);
+ drvHstAudPaStreamCorkUncorkCommon((PDRVHSTAUDPASTREAM)pvUser, fSuccess, "Corking");
+}
+
+
+/**
+ * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamEnable}
+ */
+static DECLCALLBACK(int) drvHstAudPaHA_StreamEnable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream)
+{
+ PDRVHSTAUDPA pThis = RT_FROM_MEMBER(pInterface, DRVHSTAUDPA, IHostAudio);
+ PDRVHSTAUDPASTREAM pStreamPA = (PDRVHSTAUDPASTREAM)pStream;
+ LogFlowFunc(("\n"));
+
+ /*
+ * Uncork (start or resume playback/capture) the stream.
+ */
+ pa_threaded_mainloop_lock(pThis->pMainLoop);
+
+ drvHstAudPaStreamCancelAndReleaseOperations(pStreamPA);
+ pStreamPA->pCorkOp = pa_stream_cork(pStreamPA->pStream, 0 /*uncork it*/,
+ drvHstAudPaStreamUncorkCompletionCallback, pStreamPA);
+ LogFlowFunc(("Uncorking '%s': %p (async)\n", pStreamPA->Cfg.szName, pStreamPA->pCorkOp));
+ int const rc = pStreamPA->pCorkOp ? VINF_SUCCESS
+ : drvHstAudPaError(pThis, "pa_stream_cork('%s', 0 /*uncork it*/,,) failed", pStreamPA->Cfg.szName);
+
+ pStreamPA->offInternal = 0;
+
+ pa_threaded_mainloop_unlock(pThis->pMainLoop);
+
+ LogFlowFunc(("returns %Rrc\n", rc));
+ return rc;
+}
+
+
+/**
+ * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamDisable}
+ */
+static DECLCALLBACK(int) drvHstAudPaHA_StreamDisable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream)
+{
+ PDRVHSTAUDPA pThis = RT_FROM_MEMBER(pInterface, DRVHSTAUDPA, IHostAudio);
+ PDRVHSTAUDPASTREAM pStreamPA = (PDRVHSTAUDPASTREAM)pStream;
+ LogFlowFunc(("\n"));
+
+ pa_threaded_mainloop_lock(pThis->pMainLoop);
+
+ /*
+ * For output streams, we will ignore the request if there is a pending drain
+ * as it will cork the stream in the end.
+ */
+ if (pStreamPA->Cfg.enmDir == PDMAUDIODIR_OUT)
+ {
+ if (pStreamPA->pDrainOp)
+ {
+ pa_operation_state_t const enmOpState = pa_operation_get_state(pStreamPA->pDrainOp);
+ if (enmOpState == PA_OPERATION_RUNNING)
+ {
+/** @todo consider corking it immediately instead, as that's what the caller
+ * wants now... */
+ LogFlowFunc(("Drain (%p) already running on '%s', skipping.\n", pStreamPA->pDrainOp, pStreamPA->Cfg.szName));
+ pa_threaded_mainloop_unlock(pThis->pMainLoop);
+ return VINF_SUCCESS;
+ }
+ LogFlowFunc(("Drain (%p) not running: %d\n", pStreamPA->pDrainOp, enmOpState));
+ }
+ }
+ /*
+ * For input stream we always cork it, but we clean up the peek buffer first.
+ */
+ /** @todo r=bird: It is (probably) not technically be correct to drop the peek buffer
+ * here when we're only pausing the stream (VM paused) as it means we'll
+ * risk underruns when later resuming. */
+ else if (pStreamPA->pbPeekBuf) /** @todo Do we need to drop the peek buffer?*/
+ {
+ pStreamPA->pbPeekBuf = NULL;
+ pStreamPA->cbPeekBuf = 0;
+ pa_stream_drop(pStreamPA->pStream);
+ }
+
+ /*
+ * Cork (pause playback/capture) the stream.
+ */
+ drvHstAudPaStreamCancelAndReleaseOperations(pStreamPA);
+ pStreamPA->pCorkOp = pa_stream_cork(pStreamPA->pStream, 1 /* cork it */,
+ drvHstAudPaStreamCorkCompletionCallback, pStreamPA);
+ LogFlowFunc(("Corking '%s': %p (async)\n", pStreamPA->Cfg.szName, pStreamPA->pCorkOp));
+ int const rc = pStreamPA->pCorkOp ? VINF_SUCCESS
+ : drvHstAudPaError(pThis, "pa_stream_cork('%s', 1 /*cork*/,,) failed", pStreamPA->Cfg.szName);
+
+ pa_threaded_mainloop_unlock(pThis->pMainLoop);
+ LogFlowFunc(("returns %Rrc\n", rc));
+ return rc;
+}
+
+
+/**
+ * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamPause}
+ */
+static DECLCALLBACK(int) drvHstAudPaHA_StreamPause(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream)
+{
+ /* Same as disable. */
+ return drvHstAudPaHA_StreamDisable(pInterface, pStream);
+}
+
+
+/**
+ * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamResume}
+ */
+static DECLCALLBACK(int) drvHstAudPaHA_StreamResume(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream)
+{
+ /* Same as enable. */
+ return drvHstAudPaHA_StreamEnable(pInterface, pStream);
+}
+
+
+/**
+ * Pulse audio pa_stream_drain() completion callback.
+ * @note This is fully async, so nobody is waiting for this.
+ */
+static void drvHstAudPaStreamDrainCompletionCallback(pa_stream *pStream, int fSuccess, void *pvUser)
+{
+ PDRVHSTAUDPASTREAM pStreamPA = (PDRVHSTAUDPASTREAM)pvUser;
+ AssertPtrReturnVoid(pStreamPA);
+ Assert(pStreamPA->pStream == pStream);
+ LogFlowFunc(("'%s': fSuccess=%RTbool\n", pStreamPA->Cfg.szName, fSuccess));
+
+ if (!fSuccess)
+ drvHstAudPaError(pStreamPA->pDrv, "Draining stream '%s' failed", pStreamPA->Cfg.szName);
+
+ /* Now cork the stream (doing it unconditionally atm). */
+ if (pStreamPA->pCorkOp)
+ {
+ LogFlowFunc(("Cancelling & releasing cork/uncork operation %p (state: %d)\n",
+ pStreamPA->pCorkOp, pa_operation_get_state(pStreamPA->pCorkOp)));
+ pa_operation_cancel(pStreamPA->pCorkOp);
+ pa_operation_unref(pStreamPA->pCorkOp);
+ }
+
+ pStreamPA->pCorkOp = pa_stream_cork(pStream, 1 /* cork it*/, drvHstAudPaStreamCorkCompletionCallback, pStreamPA);
+ if (pStreamPA->pCorkOp)
+ LogFlowFunc(("Started cork operation %p of %s (following drain)\n", pStreamPA->pCorkOp, pStreamPA->Cfg.szName));
+ else
+ drvHstAudPaError(pStreamPA->pDrv, "pa_stream_cork failed on '%s' (following drain)", pStreamPA->Cfg.szName);
+}
+
+
+/**
+ * Callback used with pa_stream_tigger(), starts draining.
+ */
+static void drvHstAudPaStreamTriggerCompletionCallback(pa_stream *pStream, int fSuccess, void *pvUser)
+{
+ PDRVHSTAUDPASTREAM pStreamPA = (PDRVHSTAUDPASTREAM)pvUser;
+ AssertPtrReturnVoid(pStreamPA);
+ RT_NOREF(pStream);
+ LogFlowFunc(("'%s': fSuccess=%RTbool\n", pStreamPA->Cfg.szName, fSuccess));
+
+ if (!fSuccess)
+ drvHstAudPaError(pStreamPA->pDrv, "Forcing playback before drainig '%s' failed", pStreamPA->Cfg.szName);
+
+ if (pStreamPA->pTriggerOp)
+ {
+ pa_operation_unref(pStreamPA->pTriggerOp);
+ pStreamPA->pTriggerOp = NULL;
+ }
+}
+
+
+/**
+ * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamDrain}
+ */
+static DECLCALLBACK(int) drvHstAudPaHA_StreamDrain(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream)
+{
+ PDRVHSTAUDPA pThis = RT_FROM_MEMBER(pInterface, DRVHSTAUDPA, IHostAudio);
+ PDRVHSTAUDPASTREAM pStreamPA = (PDRVHSTAUDPASTREAM)pStream;
+ AssertReturn(pStreamPA->Cfg.enmDir == PDMAUDIODIR_OUT, VERR_INVALID_PARAMETER);
+ LogFlowFunc(("\n"));
+
+ pa_threaded_mainloop_lock(pThis->pMainLoop);
+
+ /*
+ * If there is a drain running already, don't try issue another as pulse
+ * doesn't support more than one concurrent drain per stream.
+ */
+ if (pStreamPA->pDrainOp)
+ {
+ if (pa_operation_get_state(pStreamPA->pDrainOp) == PA_OPERATION_RUNNING)
+ {
+ pa_threaded_mainloop_unlock(pThis->pMainLoop);
+ LogFlowFunc(("returns VINF_SUCCESS (drain already running)\n"));
+ return VINF_SUCCESS;
+ }
+ LogFlowFunc(("Releasing drain operation %p (state: %d)\n", pStreamPA->pDrainOp, pa_operation_get_state(pStreamPA->pDrainOp)));
+ pa_operation_unref(pStreamPA->pDrainOp);
+ pStreamPA->pDrainOp = NULL;
+ }
+
+ /*
+ * Make sure pre-buffered data is played before we drain it.
+ *
+ * ASSUMES that the async stream requests are executed in the order they're
+ * issued here, so that we avoid waiting for the trigger request to complete.
+ */
+ int rc = VINF_SUCCESS;
+ if ( pStreamPA->offInternal
+ < PDMAudioPropsFramesToBytes(&pStreamPA->Cfg.Props, pStreamPA->Cfg.Backend.cFramesPreBuffering) * 2)
+ {
+ if (pStreamPA->pTriggerOp)
+ {
+ LogFlowFunc(("Cancelling & releasing trigger operation %p (state: %d)\n",
+ pStreamPA->pTriggerOp, pa_operation_get_state(pStreamPA->pTriggerOp)));
+ pa_operation_cancel(pStreamPA->pTriggerOp);
+ pa_operation_unref(pStreamPA->pTriggerOp);
+ }
+ pStreamPA->pTriggerOp = pa_stream_trigger(pStreamPA->pStream, drvHstAudPaStreamTriggerCompletionCallback, pStreamPA);
+ if (pStreamPA->pTriggerOp)
+ LogFlowFunc(("Started tigger operation %p on %s\n", pStreamPA->pTriggerOp, pStreamPA->Cfg.szName));
+ else
+ rc = drvHstAudPaError(pStreamPA->pDrv, "pa_stream_trigger failed on '%s'", pStreamPA->Cfg.szName);
+ }
+
+ /*
+ * Initiate the draining (async), will cork the stream when it completes.
+ */
+ pStreamPA->pDrainOp = pa_stream_drain(pStreamPA->pStream, drvHstAudPaStreamDrainCompletionCallback, pStreamPA);
+ if (pStreamPA->pDrainOp)
+ LogFlowFunc(("Started drain operation %p of %s\n", pStreamPA->pDrainOp, pStreamPA->Cfg.szName));
+ else
+ rc = drvHstAudPaError(pStreamPA->pDrv, "pa_stream_drain failed on '%s'", pStreamPA->Cfg.szName);
+
+ pa_threaded_mainloop_unlock(pThis->pMainLoop);
+ LogFlowFunc(("returns %Rrc\n", rc));
+ return rc;
+}
+
+
+/**
+ * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamGetState}
+ */
+static DECLCALLBACK(PDMHOSTAUDIOSTREAMSTATE) drvHstAudPaHA_StreamGetState(PPDMIHOSTAUDIO pInterface,
+ PPDMAUDIOBACKENDSTREAM pStream)
+{
+ PDRVHSTAUDPA pThis = RT_FROM_MEMBER(pInterface, DRVHSTAUDPA, IHostAudio);
+ AssertPtrReturn(pStream, PDMHOSTAUDIOSTREAMSTATE_INVALID);
+ PDRVHSTAUDPASTREAM pStreamPA = (PDRVHSTAUDPASTREAM)pStream;
+ AssertPtrReturn(pStreamPA, PDMHOSTAUDIOSTREAMSTATE_INVALID);
+
+ /* Check PulseAudio's general status. */
+ PDMHOSTAUDIOSTREAMSTATE enmBackendStreamState = PDMHOSTAUDIOSTREAMSTATE_NOT_WORKING;
+ if (pThis->pContext)
+ {
+ pa_context_state_t const enmPaCtxState = pa_context_get_state(pThis->pContext);
+ if (PA_CONTEXT_IS_GOOD(enmPaCtxState))
+ {
+ pa_stream_state_t const enmPaStreamState = pa_stream_get_state(pStreamPA->pStream);
+ if (PA_STREAM_IS_GOOD(enmPaStreamState))
+ {
+ if (enmPaStreamState != PA_STREAM_CREATING)
+ {
+ if ( pStreamPA->Cfg.enmDir != PDMAUDIODIR_OUT
+ || pStreamPA->pDrainOp == NULL
+ || pa_operation_get_state(pStreamPA->pDrainOp) != PA_OPERATION_RUNNING)
+ enmBackendStreamState = PDMHOSTAUDIOSTREAMSTATE_OKAY;
+ else
+ enmBackendStreamState = PDMHOSTAUDIOSTREAMSTATE_DRAINING;
+ }
+ else
+ enmBackendStreamState = PDMHOSTAUDIOSTREAMSTATE_INITIALIZING;
+ }
+ else
+ LogFunc(("non-good PA stream state: %d\n", enmPaStreamState));
+ }
+ else
+ LogFunc(("non-good PA context state: %d\n", enmPaCtxState));
+ }
+ else
+ LogFunc(("No context!\n"));
+ LogFlowFunc(("returns %s for stream '%s'\n", PDMHostAudioStreamStateGetName(enmBackendStreamState), pStreamPA->Cfg.szName));
+ return enmBackendStreamState;
+}
+
+
+/**
+ * Gets the number of bytes that can safely be written to a stream.
+ *
+ * @returns Number of writable bytes, ~(size_t)0 on error.
+ * @param pStreamPA The stream.
+ */
+DECLINLINE(uint32_t) drvHstAudPaStreamGetWritableLocked(PDRVHSTAUDPASTREAM pStreamPA)
+{
+ /* pa_stream_writable_size() returns the amount requested currently by the
+ server, we could write more than this if we liked. The documentation says
+ up to maxlength, whoever I'm not sure how that limitation is enforced or
+ what would happen if we exceed it. There seems to be no (simple) way to
+ figure out how much buffer we have left between what pa_stream_writable_size
+ returns and what maxlength indicates.
+
+ An alternative would be to guess the difference using the read and write
+ positions in the timing info, however the read position is only updated
+ when starting and stopping. In the auto update mode it's updated at a
+ sharply decreasing rate starting at 10ms and ending at 1500ms. So, not
+ all that helpful. (As long as pa_stream_writable_size returns a non-zero
+ value, though, we could just add the maxlength-tlength difference. But
+ the problem is after that.)
+
+ So, for now we just use tlength = maxlength for output streams and
+ problem solved. */
+ size_t const cbWritablePa = pa_stream_writable_size(pStreamPA->pStream);
+#if 1
+ return cbWritablePa;
+#else
+ if (cbWritablePa > 0 && cbWritablePa != (size_t)-1)
+ return cbWritablePa + (pStreamPA->BufAttr.maxlength - pStreamPA->BufAttr.tlength);
+ //const pa_timing_info * const pTimingInfo = pa_stream_get_timing_info(pStreamPA->pStream);
+ return 0;
+#endif
+}
+
+
+/**
+ * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamGetWritable}
+ */
+static DECLCALLBACK(uint32_t) drvHstAudPaHA_StreamGetWritable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream)
+{
+ PDRVHSTAUDPA pThis = RT_FROM_MEMBER(pInterface, DRVHSTAUDPA, IHostAudio);
+ PDRVHSTAUDPASTREAM pStreamPA = (PDRVHSTAUDPASTREAM)pStream;
+ uint32_t cbWritable = 0;
+ if (pStreamPA->Cfg.enmDir == PDMAUDIODIR_OUT)
+ {
+ pa_threaded_mainloop_lock(pThis->pMainLoop);
+
+ pa_stream_state_t const enmState = pa_stream_get_state(pStreamPA->pStream);
+ if (PA_STREAM_IS_GOOD(enmState))
+ {
+ size_t cbWritablePa = drvHstAudPaStreamGetWritableLocked(pStreamPA);
+ if (cbWritablePa != (size_t)-1)
+ cbWritable = cbWritablePa <= UINT32_MAX ? (uint32_t)cbWritablePa : UINT32_MAX;
+ else
+ drvHstAudPaError(pThis, "pa_stream_writable_size failed on '%s'", pStreamPA->Cfg.szName);
+ }
+ else
+ drvHstAudPaError(pThis, "Non-good %s stream state for '%s' (%#x)\n",
+ PDMAudioDirGetName(pStreamPA->Cfg.enmDir), pStreamPA->Cfg.szName, enmState);
+
+ pa_threaded_mainloop_unlock(pThis->pMainLoop);
+ }
+ Log3Func(("returns %#x (%u) [max=%#RX32 min=%#RX32]\n",
+ cbWritable, cbWritable, pStreamPA->BufAttr.maxlength, pStreamPA->BufAttr.minreq));
+ return cbWritable;
+}
+
+
+/**
+ * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamPlay}
+ */
+static DECLCALLBACK(int) drvHstAudPaHA_StreamPlay(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream,
+ const void *pvBuf, uint32_t cbBuf, uint32_t *pcbWritten)
+{
+ PDRVHSTAUDPA pThis = RT_FROM_MEMBER(pInterface, DRVHSTAUDPA, IHostAudio);
+ PDRVHSTAUDPASTREAM pStreamPA = (PDRVHSTAUDPASTREAM)pStream;
+ AssertPtrReturn(pStreamPA, VERR_INVALID_POINTER);
+ AssertPtrReturn(pcbWritten, VERR_INVALID_POINTER);
+ if (cbBuf)
+ AssertPtrReturn(pvBuf, VERR_INVALID_POINTER);
+ else
+ {
+ /* Fend off draining calls. */
+ *pcbWritten = 0;
+ return VINF_SUCCESS;
+ }
+
+ pa_threaded_mainloop_lock(pThis->pMainLoop);
+
+#ifdef LOG_ENABLED
+ const pa_usec_t tsNowUs = pa_rtclock_now();
+ Log3Func(("play delta: %'RI64 us; cbBuf=%#x @%#RX64\n",
+ pStreamPA->tsLastReadWrittenUs ? tsNowUs - pStreamPA->tsLastReadWrittenUs : -1, cbBuf, pStreamPA->offInternal));
+ pStreamPA->tsLastReadWrittenUs = tsNowUs;
+#endif
+
+ /*
+ * Using a loop here so we can stuff the buffer as full as it gets.
+ */
+ int rc = VINF_SUCCESS;
+ uint32_t cbTotalWritten = 0;
+ uint32_t iLoop;
+ for (iLoop = 0; ; iLoop++)
+ {
+ size_t const cbWriteable = drvHstAudPaStreamGetWritableLocked(pStreamPA);
+ if ( cbWriteable != (size_t)-1
+ && cbWriteable >= PDMAudioPropsFrameSize(&pStreamPA->Cfg.Props))
+ {
+ uint32_t cbToWrite = (uint32_t)RT_MIN(cbWriteable, cbBuf);
+ cbToWrite = PDMAudioPropsFloorBytesToFrame(&pStreamPA->Cfg.Props, cbToWrite);
+ if (pa_stream_write(pStreamPA->pStream, pvBuf, cbToWrite, NULL /*pfnFree*/, 0 /*offset*/, PA_SEEK_RELATIVE) >= 0)
+ {
+ cbTotalWritten += cbToWrite;
+ cbBuf -= cbToWrite;
+ pStreamPA->offInternal += cbToWrite;
+ if (!cbBuf)
+ break;
+ pvBuf = (uint8_t const *)pvBuf + cbToWrite;
+ Log3Func(("%#x left to write\n", cbBuf));
+ }
+ else
+ {
+ rc = drvHstAudPaError(pStreamPA->pDrv, "Failed to write to output stream");
+ break;
+ }
+ }
+ else
+ {
+ if (cbWriteable == (size_t)-1)
+ rc = drvHstAudPaError(pStreamPA->pDrv, "pa_stream_writable_size failed on '%s'", pStreamPA->Cfg.szName);
+ break;
+ }
+ }
+
+ pa_threaded_mainloop_unlock(pThis->pMainLoop);
+
+ *pcbWritten = cbTotalWritten;
+ if (RT_SUCCESS(rc) || cbTotalWritten == 0)
+ { /* likely */ }
+ else
+ {
+ LogFunc(("Supressing %Rrc because we wrote %#x bytes\n", rc, cbTotalWritten));
+ rc = VINF_SUCCESS;
+ }
+ Log3Func(("returns %Rrc *pcbWritten=%#x iLoop=%u @%#RX64\n", rc, cbTotalWritten, iLoop, pStreamPA->offInternal));
+ return rc;
+}
+
+
+/**
+ * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamGetReadable}
+ */
+static DECLCALLBACK(uint32_t) drvHstAudPaHA_StreamGetReadable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream)
+{
+ PDRVHSTAUDPA pThis = RT_FROM_MEMBER(pInterface, DRVHSTAUDPA, IHostAudio);
+ PDRVHSTAUDPASTREAM pStreamPA = (PDRVHSTAUDPASTREAM)pStream;
+ uint32_t cbReadable = 0;
+ if (pStreamPA->Cfg.enmDir == PDMAUDIODIR_IN)
+ {
+ pa_threaded_mainloop_lock(pThis->pMainLoop);
+
+ pa_stream_state_t const enmState = pa_stream_get_state(pStreamPA->pStream);
+ if (PA_STREAM_IS_GOOD(enmState))
+ {
+ size_t cbReadablePa = pa_stream_readable_size(pStreamPA->pStream);
+ if (cbReadablePa != (size_t)-1)
+ {
+ /* As with WASAPI on Windows, the peek buffer must be subtracted.*/
+ if (cbReadablePa >= pStreamPA->cbPeekBuf)
+ cbReadable = (uint32_t)(cbReadablePa - pStreamPA->cbPeekBuf);
+ else
+ {
+ AssertMsgFailed(("%#zx vs %#zx\n", cbReadablePa, pStreamPA->cbPeekBuf));
+ cbReadable = 0;
+ }
+ }
+ else
+ drvHstAudPaError(pThis, "pa_stream_readable_size failed on '%s'", pStreamPA->Cfg.szName);
+ }
+ else
+ drvHstAudPaError(pThis, "Non-good %s stream state for '%s' (%#x)\n",
+ PDMAudioDirGetName(pStreamPA->Cfg.enmDir), pStreamPA->Cfg.szName, enmState);
+
+ pa_threaded_mainloop_unlock(pThis->pMainLoop);
+ }
+ Log3Func(("returns %#x (%u)\n", cbReadable, cbReadable));
+ return cbReadable;
+}
+
+
+/**
+ * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamCapture}
+ */
+static DECLCALLBACK(int) drvHstAudPaHA_StreamCapture(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream,
+ void *pvBuf, uint32_t cbBuf, uint32_t *pcbRead)
+{
+ PDRVHSTAUDPA pThis = RT_FROM_MEMBER(pInterface, DRVHSTAUDPA, IHostAudio);
+ PDRVHSTAUDPASTREAM pStreamPA = (PDRVHSTAUDPASTREAM)pStream;
+ AssertPtrReturn(pStreamPA, VERR_INVALID_POINTER);
+ AssertPtrReturn(pvBuf, VERR_INVALID_POINTER);
+ AssertReturn(cbBuf, VERR_INVALID_PARAMETER);
+ AssertPtrReturn(pcbRead, VERR_INVALID_POINTER);
+
+#ifdef LOG_ENABLED
+ const pa_usec_t tsNowUs = pa_rtclock_now();
+ Log3Func(("capture delta: %'RI64 us; cbBuf=%#x @%#RX64\n",
+ pStreamPA->tsLastReadWrittenUs ? tsNowUs - pStreamPA->tsLastReadWrittenUs : -1, cbBuf, pStreamPA->offInternal));
+ pStreamPA->tsLastReadWrittenUs = tsNowUs;
+#endif
+
+ /*
+ * If we have left over peek buffer space from the last call,
+ * copy out the data from there.
+ */
+ uint32_t cbTotalRead = 0;
+ if ( pStreamPA->pbPeekBuf
+ && pStreamPA->offPeekBuf < pStreamPA->cbPeekBuf)
+ {
+ uint32_t cbToCopy = pStreamPA->cbPeekBuf - pStreamPA->offPeekBuf;
+ if (cbToCopy >= cbBuf)
+ {
+ memcpy(pvBuf, &pStreamPA->pbPeekBuf[pStreamPA->offPeekBuf], cbBuf);
+ pStreamPA->offPeekBuf += cbBuf;
+ pStreamPA->offInternal += cbBuf;
+ *pcbRead = cbBuf;
+
+ if (cbToCopy == cbBuf)
+ {
+ pa_threaded_mainloop_lock(pThis->pMainLoop);
+ pStreamPA->pbPeekBuf = NULL;
+ pStreamPA->cbPeekBuf = 0;
+ pa_stream_drop(pStreamPA->pStream);
+ pa_threaded_mainloop_unlock(pThis->pMainLoop);
+ }
+ Log3Func(("returns *pcbRead=%#x from prev peek buf (%#x/%#x) @%#RX64\n",
+ cbBuf, pStreamPA->offPeekBuf, pStreamPA->cbPeekBuf, pStreamPA->offInternal));
+ return VINF_SUCCESS;
+ }
+
+ memcpy(pvBuf, &pStreamPA->pbPeekBuf[pStreamPA->offPeekBuf], cbToCopy);
+ cbBuf -= cbToCopy;
+ pvBuf = (uint8_t *)pvBuf + cbToCopy;
+ cbTotalRead += cbToCopy;
+ pStreamPA->offPeekBuf = pStreamPA->cbPeekBuf;
+ }
+
+ /*
+ * Copy out what we can.
+ */
+ int rc = VINF_SUCCESS;
+ pa_threaded_mainloop_lock(pThis->pMainLoop);
+ while (cbBuf > 0)
+ {
+ /*
+ * Drop the old peek buffer first, if we have one.
+ */
+ if (pStreamPA->pbPeekBuf)
+ {
+ Assert(pStreamPA->offPeekBuf >= pStreamPA->cbPeekBuf);
+ pStreamPA->pbPeekBuf = NULL;
+ pStreamPA->cbPeekBuf = 0;
+ pa_stream_drop(pStreamPA->pStream);
+ }
+
+ /*
+ * Check if there is anything to read, the get the peek buffer for it.
+ */
+ size_t cbAvail = pa_stream_readable_size(pStreamPA->pStream);
+ if (cbAvail > 0 && cbAvail != (size_t)-1)
+ {
+ pStreamPA->pbPeekBuf = NULL;
+ pStreamPA->cbPeekBuf = 0;
+ int rcPa = pa_stream_peek(pStreamPA->pStream, (const void **)&pStreamPA->pbPeekBuf, &pStreamPA->cbPeekBuf);
+ if (rcPa == 0)
+ {
+ if (pStreamPA->cbPeekBuf)
+ {
+ if (pStreamPA->pbPeekBuf)
+ {
+ /*
+ * We got data back. Copy it into the return buffer, return if it's full.
+ */
+ if (cbBuf < pStreamPA->cbPeekBuf)
+ {
+ memcpy(pvBuf, pStreamPA->pbPeekBuf, cbBuf);
+ cbTotalRead += cbBuf;
+ pStreamPA->offPeekBuf = cbBuf;
+ pStreamPA->offInternal += cbBuf;
+ cbBuf = 0;
+ break;
+ }
+ memcpy(pvBuf, pStreamPA->pbPeekBuf, pStreamPA->cbPeekBuf);
+ cbBuf -= pStreamPA->cbPeekBuf;
+ pvBuf = (uint8_t *)pvBuf + pStreamPA->cbPeekBuf;
+ cbTotalRead += pStreamPA->cbPeekBuf;
+ pStreamPA->offInternal += cbBuf;
+
+ pStreamPA->pbPeekBuf = NULL;
+ }
+ else
+ {
+ /*
+ * We got a hole (drop needed). We will skip it as we leave it to
+ * the device's DMA engine to fill in buffer gaps with silence.
+ */
+ LogFunc(("pa_stream_peek returned a %#zx (%zu) byte hole - skipping.\n",
+ pStreamPA->cbPeekBuf, pStreamPA->cbPeekBuf));
+ }
+ pStreamPA->cbPeekBuf = 0;
+ pa_stream_drop(pStreamPA->pStream);
+ }
+ else
+ {
+ Assert(!pStreamPA->pbPeekBuf);
+ LogFunc(("pa_stream_peek returned empty buffer\n"));
+ break;
+ }
+ }
+ else
+ {
+ rc = drvHstAudPaError(pStreamPA->pDrv, "pa_stream_peek failed on '%s' (%d)", pStreamPA->Cfg.szName, rcPa);
+ pStreamPA->pbPeekBuf = NULL;
+ pStreamPA->cbPeekBuf = 0;
+ break;
+ }
+ }
+ else
+ {
+ if (cbAvail == (size_t)-1)
+ rc = drvHstAudPaError(pStreamPA->pDrv, "pa_stream_readable_size failed on '%s'", pStreamPA->Cfg.szName);
+ break;
+ }
+ }
+ pa_threaded_mainloop_unlock(pThis->pMainLoop);
+
+ *pcbRead = cbTotalRead;
+ if (RT_SUCCESS(rc) || cbTotalRead == 0)
+ { /* likely */ }
+ else
+ {
+ LogFunc(("Supressing %Rrc because we're returning %#x bytes\n", rc, cbTotalRead));
+ rc = VINF_SUCCESS;
+ }
+ Log3Func(("returns %Rrc *pcbRead=%#x (%#x left, peek %#x/%#x) @%#RX64\n",
+ rc, cbTotalRead, cbBuf, pStreamPA->offPeekBuf, pStreamPA->cbPeekBuf, pStreamPA->offInternal));
+ return rc;
+}
+
+
+/*********************************************************************************************************************************
+* PDMIBASE *
+*********************************************************************************************************************************/
+
+/**
+ * @interface_method_impl{PDMIBASE,pfnQueryInterface}
+ */
+static DECLCALLBACK(void *) drvHstAudPaQueryInterface(PPDMIBASE pInterface, const char *pszIID)
+{
+ AssertPtrReturn(pInterface, NULL);
+ AssertPtrReturn(pszIID, NULL);
+
+ PPDMDRVINS pDrvIns = PDMIBASE_2_PDMDRV(pInterface);
+ PDRVHSTAUDPA pThis = PDMINS_2_DATA(pDrvIns, PDRVHSTAUDPA);
+ PDMIBASE_RETURN_INTERFACE(pszIID, PDMIBASE, &pDrvIns->IBase);
+ PDMIBASE_RETURN_INTERFACE(pszIID, PDMIHOSTAUDIO, &pThis->IHostAudio);
+
+ return NULL;
+}
+
+
+/*********************************************************************************************************************************
+* PDMDRVREG *
+*********************************************************************************************************************************/
+
+/**
+ * Destructs a PulseAudio Audio driver instance.
+ *
+ * @copydoc FNPDMDRVDESTRUCT
+ */
+static DECLCALLBACK(void) drvHstAudPaDestruct(PPDMDRVINS pDrvIns)
+{
+ PDMDRV_CHECK_VERSIONS_RETURN_VOID(pDrvIns);
+ PDRVHSTAUDPA pThis = PDMINS_2_DATA(pDrvIns, PDRVHSTAUDPA);
+ 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();
+}
+
+
+/**
+ * Pulse audio callback for context status changes, init variant.
+ *
+ * Signalls our event semaphore so we can do a timed wait from
+ * drvHstAudPaConstruct().
+ */
+static void drvHstAudPaCtxCallbackStateChangedInit(pa_context *pCtx, void *pvUser)
+{
+ AssertPtrReturnVoid(pCtx);
+ PDRVHSTAUDPASTATECHGCTX pStateChgCtx = (PDRVHSTAUDPASTATECHGCTX)pvUser;
+ pa_context_state_t enmCtxState = pa_context_get_state(pCtx);
+ switch (enmCtxState)
+ {
+ case PA_CONTEXT_READY:
+ case PA_CONTEXT_TERMINATED:
+ case PA_CONTEXT_FAILED:
+ AssertPtrReturnVoid(pStateChgCtx);
+ pStateChgCtx->enmCtxState = enmCtxState;
+ RTSemEventSignal(pStateChgCtx->hEvtInit);
+ break;
+
+ default:
+ break;
+ }
+}
+
+
+/**
+ * Constructs a PulseAudio Audio driver instance.
+ *
+ * @copydoc FNPDMDRVCONSTRUCT
+ */
+static DECLCALLBACK(int) drvHstAudPaConstruct(PPDMDRVINS pDrvIns, PCFGMNODE pCfg, uint32_t fFlags)
+{
+ RT_NOREF(pCfg, fFlags);
+ PDMDRV_CHECK_VERSIONS_RETURN(pDrvIns);
+ PDRVHSTAUDPA pThis = PDMINS_2_DATA(pDrvIns, PDRVHSTAUDPA);
+ PCPDMDRVHLPR3 pHlp = pDrvIns->pHlpR3;
+
+ LogRel(("Audio: Initializing PulseAudio driver\n"));
+
+ /*
+ * Initialize instance data.
+ */
+ pThis->pDrvIns = pDrvIns;
+ /* IBase */
+ pDrvIns->IBase.pfnQueryInterface = drvHstAudPaQueryInterface;
+ /* IHostAudio */
+ pThis->IHostAudio.pfnGetConfig = drvHstAudPaHA_GetConfig;
+ pThis->IHostAudio.pfnGetDevices = drvHstAudPaHA_GetDevices;
+ pThis->IHostAudio.pfnSetDevice = drvHstAudPaHA_SetDevice;
+ pThis->IHostAudio.pfnGetStatus = drvHstAudPaHA_GetStatus;
+ pThis->IHostAudio.pfnDoOnWorkerThread = NULL;
+ pThis->IHostAudio.pfnStreamConfigHint = NULL;
+ pThis->IHostAudio.pfnStreamCreate = drvHstAudPaHA_StreamCreate;
+ pThis->IHostAudio.pfnStreamInitAsync = NULL;
+ pThis->IHostAudio.pfnStreamDestroy = drvHstAudPaHA_StreamDestroy;
+ pThis->IHostAudio.pfnStreamNotifyDeviceChanged = NULL;
+ pThis->IHostAudio.pfnStreamEnable = drvHstAudPaHA_StreamEnable;
+ pThis->IHostAudio.pfnStreamDisable = drvHstAudPaHA_StreamDisable;
+ pThis->IHostAudio.pfnStreamPause = drvHstAudPaHA_StreamPause;
+ pThis->IHostAudio.pfnStreamResume = drvHstAudPaHA_StreamResume;
+ pThis->IHostAudio.pfnStreamDrain = drvHstAudPaHA_StreamDrain;
+ pThis->IHostAudio.pfnStreamGetState = drvHstAudPaHA_StreamGetState;
+ pThis->IHostAudio.pfnStreamGetPending = NULL;
+ pThis->IHostAudio.pfnStreamGetWritable = drvHstAudPaHA_StreamGetWritable;
+ pThis->IHostAudio.pfnStreamPlay = drvHstAudPaHA_StreamPlay;
+ pThis->IHostAudio.pfnStreamGetReadable = drvHstAudPaHA_StreamGetReadable;
+ pThis->IHostAudio.pfnStreamCapture = drvHstAudPaHA_StreamCapture;
+
+ /*
+ * Read configuration.
+ */
+ PDMDRV_VALIDATE_CONFIG_RETURN(pDrvIns, "VmName|InputDeviceID|OutputDeviceID", "");
+ int rc = pHlp->pfnCFGMQueryString(pCfg, "VmName", pThis->szStreamName, sizeof(pThis->szStreamName));
+ AssertMsgRCReturn(rc, ("Confguration error: No/bad \"VmName\" value, rc=%Rrc\n", rc), rc);
+ rc = pHlp->pfnCFGMQueryStringDef(pCfg, "InputDeviceID", pThis->szInputDev, sizeof(pThis->szInputDev), "");
+ AssertMsgRCReturn(rc, ("Confguration error: Failed to read \"InputDeviceID\" as string: rc=%Rrc\n", rc), rc);
+ rc = pHlp->pfnCFGMQueryStringDef(pCfg, "OutputDeviceID", pThis->szOutputDev, sizeof(pThis->szOutputDev), "");
+ AssertMsgRCReturn(rc, ("Confguration error: Failed to read \"OutputDeviceID\" as string: rc=%Rrc\n", rc), rc);
+
+ /*
+ * Query the notification interface from the driver/device above us.
+ */
+ pThis->pIHostAudioPort = PDMIBASE_QUERY_INTERFACE(pDrvIns->pUpBase, PDMIHOSTAUDIOPORT);
+ AssertReturn(pThis->pIHostAudioPort, VERR_PDM_MISSING_INTERFACE_ABOVE);
+
+ /*
+ * Load the pulse audio library.
+ */
+ LogRel2(("PulseAudio: Loading PulseAudio shared library ...\n"));
+ rc = audioLoadPulseLib();
+ if (RT_SUCCESS(rc))
+ {
+ LogRel2(("PulseAudio: PulseAudio shared library loaded\n"));
+ LogRel(("PulseAudio: Using version %s\n", pa_get_library_version()));
+ }
+ else
+ {
+ LogRel(("PulseAudio: Failed to load the PulseAudio shared library! Error %Rrc\n", rc));
+ return rc;
+ }
+
+ LogRel2(("PulseAudio: Starting PulseAudio main loop ...\n"));
+
+ /*
+ * Set up the basic pulse audio bits (remember the destructore is always called).
+ */
+ //pThis->fAbortLoop = false;
+ pThis->pMainLoop = pa_threaded_mainloop_new();
+ if (!pThis->pMainLoop)
+ {
+ LogRel(("PulseAudio: Failed to allocate main loop: %s\n", pa_strerror(pa_context_errno(pThis->pContext))));
+ return VERR_NO_MEMORY;
+ }
+
+ pThis->pContext = pa_context_new(pa_threaded_mainloop_get_api(pThis->pMainLoop), "VirtualBox");
+ if (!pThis->pContext)
+ {
+ LogRel(("PulseAudio: Failed to allocate context: %s\n", pa_strerror(pa_context_errno(pThis->pContext))));
+ return VERR_NO_MEMORY;
+ }
+
+ if (pa_threaded_mainloop_start(pThis->pMainLoop) < 0)
+ {
+ LogRel(("PulseAudio: Failed to start threaded mainloop: %s\n", pa_strerror(pa_context_errno(pThis->pContext))));
+ return VERR_AUDIO_BACKEND_INIT_FAILED;
+ }
+
+ LogRel2(("PulseAudio: Started PulseAudio main loop, connecting to server ...\n"));
+
+ /*
+ * Connect to the pulse audio server.
+ *
+ * We install an init state callback so we can do a timed wait in case
+ * connecting to the pulseaudio server should take too long.
+ */
+ pThis->InitStateChgCtx.hEvtInit = NIL_RTSEMEVENT;
+ pThis->InitStateChgCtx.enmCtxState = PA_CONTEXT_UNCONNECTED;
+ rc = RTSemEventCreate(&pThis->InitStateChgCtx.hEvtInit);
+ AssertLogRelRCReturn(rc, rc);
+
+ pa_threaded_mainloop_lock(pThis->pMainLoop);
+ pa_context_set_state_callback(pThis->pContext, drvHstAudPaCtxCallbackStateChangedInit, &pThis->InitStateChgCtx);
+ if (!pa_context_connect(pThis->pContext, NULL /* pszServer */, PA_CONTEXT_NOFLAGS, NULL))
+ {
+ pa_threaded_mainloop_unlock(pThis->pMainLoop);
+
+ rc = RTSemEventWait(pThis->InitStateChgCtx.hEvtInit, RT_MS_10SEC); /* 10 seconds should be plenty. */
+ if (RT_SUCCESS(rc))
+ {
+ if (pThis->InitStateChgCtx.enmCtxState == PA_CONTEXT_READY)
+ {
+ /* Install the main state changed callback to know if something happens to our acquired context. */
+ pa_threaded_mainloop_lock(pThis->pMainLoop);
+ pa_context_set_state_callback(pThis->pContext, drvHstAudPaCtxCallbackStateChanged, pThis /* pvUserData */);
+ pa_threaded_mainloop_unlock(pThis->pMainLoop);
+ }
+ else
+ {
+ LogRel(("PulseAudio: Failed to initialize context (state %d, rc=%Rrc)\n", pThis->InitStateChgCtx.enmCtxState, rc));
+ rc = VERR_AUDIO_BACKEND_INIT_FAILED;
+ }
+ }
+ else
+ {
+ LogRel(("PulseAudio: Waiting for context to become ready failed: %Rrc\n", rc));
+ rc = VERR_AUDIO_BACKEND_INIT_FAILED;
+ }
+ }
+ else
+ {
+ pa_threaded_mainloop_unlock(pThis->pMainLoop);
+ LogRel(("PulseAudio: Failed to connect to server: %s\n", pa_strerror(pa_context_errno(pThis->pContext))));
+ rc = VERR_AUDIO_BACKEND_INIT_FAILED; /* bird: This used to be VINF_SUCCESS. */
+ }
+
+ RTSemEventDestroy(pThis->InitStateChgCtx.hEvtInit);
+ pThis->InitStateChgCtx.hEvtInit = NIL_RTSEMEVENT;
+
+ /*
+ * Register statistics.
+ */
+ if (RT_SUCCESS(rc))
+ {
+ LogRel2(("PulseAudio: Connected to PulseAudio server\n"));
+
+ PDMDrvHlpSTAMRegister(pDrvIns, &pThis->StatOverruns, STAMTYPE_COUNTER, "Overruns", STAMUNIT_OCCURENCES,
+ "Pulse-server side buffer overruns (all streams)");
+ PDMDrvHlpSTAMRegister(pDrvIns, &pThis->StatUnderruns, STAMTYPE_COUNTER, "Underruns", STAMUNIT_OCCURENCES,
+ "Pulse-server side buffer underruns (all streams)");
+ }
+
+ LogRel2(("PulseAudio: Initialization ended with %Rrc\n", rc));
+ return rc;
+}
+
+
+/**
+ * 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(DRVHSTAUDPA),
+ /* pfnConstruct */
+ drvHstAudPaConstruct,
+ /* pfnDestruct */
+ drvHstAudPaDestruct,
+ /* pfnRelocate */
+ NULL,
+ /* pfnIOCtl */
+ NULL,
+ /* pfnPowerOn */
+ NULL,
+ /* pfnReset */
+ NULL,
+ /* pfnSuspend */
+ NULL,
+ /* pfnResume */
+ NULL,
+ /* pfnAttach */
+ NULL,
+ /* pfnDetach */
+ NULL,
+ /* pfnPowerOff */
+ NULL,
+ /* pfnSoftReset */
+ NULL,
+ /* u32EndVersion */
+ PDM_DRVREG_VERSION
+};
+