diff options
Diffstat (limited to 'src/VBox/Devices/Audio/AudioMixer.cpp')
-rw-r--r-- | src/VBox/Devices/Audio/AudioMixer.cpp | 2720 |
1 files changed, 2720 insertions, 0 deletions
diff --git a/src/VBox/Devices/Audio/AudioMixer.cpp b/src/VBox/Devices/Audio/AudioMixer.cpp new file mode 100644 index 00000000..9415b4ab --- /dev/null +++ b/src/VBox/Devices/Audio/AudioMixer.cpp @@ -0,0 +1,2720 @@ +/* $Id: AudioMixer.cpp $ */ +/** @file + * Audio mixing routines for multiplexing audio sources in device emulations. + */ + +/* + * Copyright (C) 2014-2022 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 + */ + +/** @page pg_audio_mixer Audio Mixer + * + * @section sec_audio_mixer_overview Overview + * + * This mixer acts as a layer between the audio connector interface and the + * actual device emulation, providing mechanisms for audio input sinks (sometime + * referred to as audio sources) and audio output sinks. + * + * Think of this mixer as kind of a higher level interface for the audio device + * to use in steado of PDMIAUDIOCONNECTOR, where it works with sinks rather than + * individual PDMAUDIOSTREAM instances. + * + * How and which audio streams are connected to the sinks depends on how the + * audio mixer has been set up by the device. Though, generally, each driver + * chain (LUN) has a mixer stream for each sink. + * + * An output sink can connect multiple output streams together, whereas an input + * sink (source) does this with input streams. Each of these mixer stream will + * in turn point to actual PDMAUDIOSTREAM instances. + * + * A mixing sink employs an own audio mixing buffer in a standard format (32-bit + * signed) with the virtual device's rate and channel configuration. The mixer + * streams will convert to/from this as they write and read from it. + * + * + * @section sec_audio_mixer_playback Playback + * + * For output sinks there can be one or more mixing stream attached. + * + * The backends are the consumers here and if they don't get samples when then + * need them we'll be having cracles, distortion and/or bits of silence in the + * actual output. The guest runs independently at it's on speed (see @ref + * sec_pdm_audio_timing for more details) and we're just inbetween trying to + * shuffle the data along as best as we can. If one or more of the backends + * for some reason isn't able to process data at a nominal speed (as defined by + * the others), we'll try detect this, mark it as bad and disregard it when + * calculating how much we can write to the backends in a buffer update call. + * + * This is called synchronous multiplexing. + * + * + * @section sec_audio_mixer_recording Recording + * + * For input sinks (sources) we blend the samples of all mixing streams + * together, however ignoring silent ones to avoid too much of a hit on the + * volume level. It is otherwise very similar to playback, only the direction + * is different and we don't multicast but blend. + * + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_AUDIO_MIXER +#include <VBox/log.h> +#include "AudioMixer.h" +#include "AudioMixBuffer.h" +#include "AudioHlp.h" + +#include <VBox/vmm/pdm.h> +#include <VBox/err.h> +#include <VBox/vmm/mm.h> +#include <VBox/vmm/pdmaudioifs.h> +#include <VBox/vmm/pdmaudioinline.h> + +#include <iprt/alloc.h> +#include <iprt/asm-math.h> +#include <iprt/assert.h> +#include <iprt/semaphore.h> +#include <iprt/string.h> +#include <iprt/thread.h> + +#ifdef VBOX_WITH_DTRACE +# include "dtrace/VBoxDD.h" +#endif + + +/********************************************************************************************************************************* +* Internal Functions * +*********************************************************************************************************************************/ +static int audioMixerRemoveSinkInternal(PAUDIOMIXER pMixer, PAUDMIXSINK pSink); + +static void audioMixerSinkDestroyInternal(PAUDMIXSINK pSink, PPDMDEVINS pDevIns); +static int audioMixerSinkUpdateVolume(PAUDMIXSINK pSink, PCPDMAUDIOVOLUME pVolMaster); +static int audioMixerSinkRemoveStreamInternal(PAUDMIXSINK pSink, PAUDMIXSTREAM pStream); +static void audioMixerSinkResetInternal(PAUDMIXSINK pSink); + +static int audioMixerStreamCtlInternal(PAUDMIXSTREAM pMixStream, PDMAUDIOSTREAMCMD enmCmd); +static void audioMixerStreamDestroyInternal(PAUDMIXSTREAM pStream, PPDMDEVINS pDevIns, bool fImmediate); +static int audioMixerStreamUpdateStatus(PAUDMIXSTREAM pMixStream); + + +/** size of output buffer for dbgAudioMixerSinkStatusToStr. */ +#define AUDIOMIXERSINK_STATUS_STR_MAX sizeof("RUNNING DRAINING DRAINED_DMA DRAINED_MIXBUF DIRTY 0x12345678") + +/** + * Converts a mixer sink status to a string. + * + * @returns pszDst + * @param fStatus The mixer sink status. + * @param pszDst The output buffer. Must be at least + * AUDIOMIXERSINK_STATUS_STR_MAX in length. + */ +static const char *dbgAudioMixerSinkStatusToStr(uint32_t fStatus, char pszDst[AUDIOMIXERSINK_STATUS_STR_MAX]) +{ + if (!fStatus) + return strcpy(pszDst, "NONE"); + static const struct + { + const char *pszMnemonic; + uint32_t cchMnemonic; + uint32_t fStatus; + } s_aFlags[] = + { + { RT_STR_TUPLE("RUNNING "), AUDMIXSINK_STS_RUNNING }, + { RT_STR_TUPLE("DRAINING "), AUDMIXSINK_STS_DRAINING }, + { RT_STR_TUPLE("DRAINED_DMA "), AUDMIXSINK_STS_DRAINED_DMA }, + { RT_STR_TUPLE("DRAINED_MIXBUF "), AUDMIXSINK_STS_DRAINED_MIXBUF }, + { RT_STR_TUPLE("DIRTY "), AUDMIXSINK_STS_DIRTY }, + }; + char *psz = pszDst; + for (size_t i = 0; i < RT_ELEMENTS(s_aFlags); i++) + if (fStatus & s_aFlags[i].fStatus) + { + memcpy(psz, s_aFlags[i].pszMnemonic, s_aFlags[i].cchMnemonic); + psz += s_aFlags[i].cchMnemonic; + fStatus &= ~s_aFlags[i].fStatus; + if (!fStatus) + { + psz[-1] = '\0'; + return pszDst; + } + } + RTStrPrintf(psz, AUDIOMIXERSINK_STATUS_STR_MAX - (psz - pszDst), "%#x", fStatus); + return pszDst; +} + + +/** + * Creates an audio mixer. + * + * @returns VBox status code. + * @param pszName Name of the audio mixer. + * @param fFlags Creation flags - AUDMIXER_FLAGS_XXX. + * @param ppMixer Pointer which returns the created mixer object. + */ +int AudioMixerCreate(const char *pszName, uint32_t fFlags, PAUDIOMIXER *ppMixer) +{ + AssertPtrReturn(pszName, VERR_INVALID_POINTER); + size_t const cchName = strlen(pszName); + AssertReturn(cchName > 0 && cchName < 128, VERR_INVALID_NAME); + AssertReturn (!(fFlags & ~AUDMIXER_FLAGS_VALID_MASK), VERR_INVALID_FLAGS); + AssertPtrReturn(ppMixer, VERR_INVALID_POINTER); + + int rc; + PAUDIOMIXER pMixer = (PAUDIOMIXER)RTMemAllocZVar(sizeof(AUDIOMIXER) + cchName + 1); + if (pMixer) + { + rc = RTCritSectInit(&pMixer->CritSect); + if (RT_SUCCESS(rc)) + { + pMixer->pszName = (const char *)memcpy(pMixer + 1, pszName, cchName + 1); + + pMixer->cSinks = 0; + RTListInit(&pMixer->lstSinks); + + pMixer->fFlags = fFlags; + pMixer->uMagic = AUDIOMIXER_MAGIC; + + if (pMixer->fFlags & AUDMIXER_FLAGS_DEBUG) + LogRel(("Audio Mixer: Debug mode enabled\n")); + + /* Set master volume to the max. */ + PDMAudioVolumeInitMax(&pMixer->VolMaster); + + LogFlowFunc(("Created mixer '%s'\n", pMixer->pszName)); + *ppMixer = pMixer; + return VINF_SUCCESS; + } + RTMemFree(pMixer); + } + else + rc = VERR_NO_MEMORY; + LogFlowFuncLeaveRC(rc); + return rc; +} + + +/** + * Destroys an audio mixer. + * + * @param pMixer Audio mixer to destroy. NULL is ignored. + * @param pDevIns The device instance the statistics are associated with. + */ +void AudioMixerDestroy(PAUDIOMIXER pMixer, PPDMDEVINS pDevIns) +{ + if (!pMixer) + return; + AssertPtrReturnVoid(pMixer); + AssertReturnVoid(pMixer->uMagic == AUDIOMIXER_MAGIC); + + int rc2 = RTCritSectEnter(&pMixer->CritSect); + AssertRCReturnVoid(rc2); + Assert(pMixer->uMagic == AUDIOMIXER_MAGIC); + + LogFlowFunc(("Destroying %s ...\n", pMixer->pszName)); + pMixer->uMagic = AUDIOMIXER_MAGIC_DEAD; + + PAUDMIXSINK pSink, pSinkNext; + RTListForEachSafe(&pMixer->lstSinks, pSink, pSinkNext, AUDMIXSINK, Node) + { + audioMixerRemoveSinkInternal(pMixer, pSink); + audioMixerSinkDestroyInternal(pSink, pDevIns); + } + Assert(pMixer->cSinks == 0); + + rc2 = RTCritSectLeave(&pMixer->CritSect); + AssertRC(rc2); + + RTCritSectDelete(&pMixer->CritSect); + RTMemFree(pMixer); +} + + +/** + * Helper function for the internal debugger to print the mixer's current + * state, along with the attached sinks. + * + * @param pMixer Mixer to print debug output for. + * @param pHlp Debug info helper to use. + * @param pszArgs Optional arguments. Not being used at the moment. + */ +void AudioMixerDebug(PAUDIOMIXER pMixer, PCDBGFINFOHLP pHlp, const char *pszArgs) +{ + RT_NOREF(pszArgs); + AssertReturnVoid(pMixer->uMagic == AUDIOMIXER_MAGIC); + + int rc = RTCritSectEnter(&pMixer->CritSect); + AssertRCReturnVoid(rc); + + /* Determin max sink name length for pretty formatting: */ + size_t cchMaxName = strlen(pMixer->pszName); + PAUDMIXSINK pSink; + RTListForEach(&pMixer->lstSinks, pSink, AUDMIXSINK, Node) + { + size_t const cchMixer = strlen(pSink->pszName); + cchMaxName = RT_MAX(cchMixer, cchMaxName); + } + + /* Do the displaying. */ + pHlp->pfnPrintf(pHlp, "[Master] %*s: fMuted=%#RTbool auChannels=%.*Rhxs\n", cchMaxName, pMixer->pszName, + pMixer->VolMaster.fMuted, sizeof(pMixer->VolMaster.auChannels), pMixer->VolMaster.auChannels); + unsigned iSink = 0; + RTListForEach(&pMixer->lstSinks, pSink, AUDMIXSINK, Node) + { + pHlp->pfnPrintf(pHlp, "[Sink %u] %*s: fMuted=%#RTbool auChannels=%.*Rhxs\n", iSink, cchMaxName, pSink->pszName, + pSink->Volume.fMuted, sizeof(pSink->Volume.auChannels), pSink->Volume.auChannels); + ++iSink; + } + + RTCritSectLeave(&pMixer->CritSect); +} + + +/** + * Sets the mixer's master volume. + * + * @returns VBox status code. + * @param pMixer Mixer to set master volume for. + * @param pVol Volume to set. + */ +int AudioMixerSetMasterVolume(PAUDIOMIXER pMixer, PCPDMAUDIOVOLUME pVol) +{ + AssertPtrReturn(pMixer, VERR_INVALID_POINTER); + AssertReturn(pMixer->uMagic == AUDIOMIXER_MAGIC, VERR_INVALID_MAGIC); + AssertPtrReturn(pVol, VERR_INVALID_POINTER); + + int rc = RTCritSectEnter(&pMixer->CritSect); + AssertRCReturn(rc, rc); + + /* + * Make a copy. + */ + LogFlowFunc(("[%s] fMuted=%RTbool auChannels=%.*Rhxs => fMuted=%RTbool auChannels=%.*Rhxs\n", pMixer->pszName, + pMixer->VolMaster.fMuted, sizeof(pMixer->VolMaster.auChannels), pMixer->VolMaster.auChannels, + pVol->fMuted, sizeof(pVol->auChannels), pVol->auChannels )); + memcpy(&pMixer->VolMaster, pVol, sizeof(PDMAUDIOVOLUME)); + + /* + * Propagate new master volume to all sinks. + */ + PAUDMIXSINK pSink; + RTListForEach(&pMixer->lstSinks, pSink, AUDMIXSINK, Node) + { + int rc2 = audioMixerSinkUpdateVolume(pSink, &pMixer->VolMaster); + AssertRC(rc2); + } + + RTCritSectLeave(&pMixer->CritSect); + return rc; +} + + +/** + * Removes an audio sink from the given audio mixer, internal version. + * + * Used by AudioMixerDestroy and AudioMixerSinkDestroy. + * + * Caller must hold the mixer lock. + * + * @returns VBox status code. + * @param pMixer Mixer to remove sink from. + * @param pSink Sink to remove. + */ +static int audioMixerRemoveSinkInternal(PAUDIOMIXER pMixer, PAUDMIXSINK pSink) +{ + LogFlowFunc(("[%s] pSink=%s, cSinks=%RU8\n", pMixer->pszName, pSink->pszName, pMixer->cSinks)); + Assert(RTCritSectIsOwner(&pMixer->CritSect)); + AssertMsgReturn(pSink->pParent == pMixer, + ("%s: Is not part of mixer '%s'\n", pSink->pszName, pMixer->pszName), VERR_INTERNAL_ERROR_4); + + /* Remove sink from mixer. */ + RTListNodeRemove(&pSink->Node); + + Assert(pMixer->cSinks); + pMixer->cSinks--; + + /* Set mixer to NULL so that we know we're not part of any mixer anymore. */ + pSink->pParent = NULL; + + return VINF_SUCCESS; +} + + +/********************************************************************************************************************************* +* Mixer Sink implementation. * +*********************************************************************************************************************************/ + +/** + * Creates an audio sink and attaches it to the given mixer. + * + * @returns VBox status code. + * @param pMixer Mixer to attach created sink to. + * @param pszName Name of the sink to create. + * @param enmDir Direction of the sink to create. + * @param pDevIns The device instance to register statistics under. + * @param ppSink Pointer which returns the created sink on success. + */ +int AudioMixerCreateSink(PAUDIOMIXER pMixer, const char *pszName, PDMAUDIODIR enmDir, PPDMDEVINS pDevIns, PAUDMIXSINK *ppSink) +{ + AssertPtrReturn(pMixer, VERR_INVALID_POINTER); + AssertPtrReturn(pszName, VERR_INVALID_POINTER); + size_t const cchName = strlen(pszName); + AssertReturn(cchName > 0 && cchName < 64, VERR_INVALID_NAME); + AssertPtrNullReturn(ppSink, VERR_INVALID_POINTER); + + int rc = RTCritSectEnter(&pMixer->CritSect); + AssertRCReturn(rc, rc); + + /** @todo limit the number of sinks? */ + + /* + * Allocate the data and initialize the critsect. + */ + PAUDMIXSINK pSink = (PAUDMIXSINK)RTMemAllocZVar(sizeof(AUDMIXSINK) + cchName + 1); + if (pSink) + { + rc = RTCritSectInit(&pSink->CritSect); + if (RT_SUCCESS(rc)) + { + /* + * Initialize it. + */ + pSink->uMagic = AUDMIXSINK_MAGIC; + pSink->pParent = NULL; + pSink->enmDir = enmDir; + pSink->pszName = (const char *)memcpy(pSink + 1, pszName, cchName + 1); + RTListInit(&pSink->lstStreams); + + /* Set initial volume to max. */ + PDMAudioVolumeInitMax(&pSink->Volume); + + /* Ditto for the combined volume. */ + PDMAudioVolumeInitMax(&pSink->VolumeCombined); + + /* AIO */ + AssertPtr(pDevIns); + pSink->AIO.pDevIns = pDevIns; + pSink->AIO.hThread = NIL_RTTHREAD; + pSink->AIO.hEvent = NIL_RTSEMEVENT; + pSink->AIO.fStarted = false; + pSink->AIO.fShutdown = false; + pSink->AIO.cUpdateJobs = 0; + + /* + * Add it to the mixer. + */ + RTListAppend(&pMixer->lstSinks, &pSink->Node); + pMixer->cSinks++; + pSink->pParent = pMixer; + + RTCritSectLeave(&pMixer->CritSect); + + /* + * Register stats and return. + */ + char szPrefix[128]; + RTStrPrintf(szPrefix, sizeof(szPrefix), "MixerSink-%s/", pSink->pszName); + PDMDevHlpSTAMRegisterF(pDevIns, &pSink->MixBuf.cFrames, STAMTYPE_U32, STAMVISIBILITY_USED, STAMUNIT_NONE, + "Sink mixer buffer size in frames.", "%sMixBufSize", szPrefix); + PDMDevHlpSTAMRegisterF(pDevIns, &pSink->MixBuf.cUsed, STAMTYPE_U32, STAMVISIBILITY_USED, STAMUNIT_NONE, + "Sink mixer buffer fill size in frames.", "%sMixBufUsed", szPrefix); + PDMDevHlpSTAMRegisterF(pDevIns, &pSink->cStreams, STAMTYPE_U8, STAMVISIBILITY_USED, STAMUNIT_NONE, + "Number of streams attached to the sink.", "%sStreams", szPrefix); + + if (ppSink) + *ppSink = pSink; + return VINF_SUCCESS; + } + + RTMemFree(pSink); + } + else + rc = VERR_NO_MEMORY; + + RTCritSectLeave(&pMixer->CritSect); + if (ppSink) + *ppSink = NULL; + return rc; +} + + +/** + * Starts playback/capturing on the mixer sink. + * + * @returns VBox status code. Generally always VINF_SUCCESS unless the input + * is invalid. Individual driver errors are suppressed and ignored. + * @param pSink Mixer sink to control. + */ +int AudioMixerSinkStart(PAUDMIXSINK pSink) +{ + AssertPtrReturn(pSink, VERR_INVALID_POINTER); + Assert(pSink->uMagic == AUDMIXSINK_MAGIC); + int rc = RTCritSectEnter(&pSink->CritSect); + AssertRCReturn(rc, rc); + char szStatus[AUDIOMIXERSINK_STATUS_STR_MAX]; + LogFunc(("Starting '%s'. Old status: %s\n", pSink->pszName, dbgAudioMixerSinkStatusToStr(pSink->fStatus, szStatus))); + + AssertReturnStmt(pSink->enmDir == PDMAUDIODIR_IN || pSink->enmDir == PDMAUDIODIR_OUT, + RTCritSectLeave(&pSink->CritSect), VERR_INTERNAL_ERROR_3); + + /* + * Make sure the sink and its streams are all stopped. + */ + if (!(pSink->fStatus & AUDMIXSINK_STS_RUNNING)) + Assert(pSink->fStatus == AUDMIXSINK_STS_NONE); + else + { + LogFunc(("%s: This sink is still running!! Stop it before starting it again.\n", pSink->pszName)); + + PAUDMIXSTREAM pStream; + RTListForEach(&pSink->lstStreams, pStream, AUDMIXSTREAM, Node) + { + /** @todo PDMAUDIOSTREAMCMD_STOP_NOW */ + audioMixerStreamCtlInternal(pStream, PDMAUDIOSTREAMCMD_DISABLE); + } + audioMixerSinkResetInternal(pSink); + } + + /* + * Send the command to the streams. + */ + PAUDMIXSTREAM pStream; + RTListForEach(&pSink->lstStreams, pStream, AUDMIXSTREAM, Node) + { + audioMixerStreamCtlInternal(pStream, PDMAUDIOSTREAMCMD_ENABLE); + } + + /* + * Update the sink status. + */ + pSink->fStatus = AUDMIXSINK_STS_RUNNING; + + LogRel2(("Audio Mixer: Started sink '%s': %s\n", pSink->pszName, dbgAudioMixerSinkStatusToStr(pSink->fStatus, szStatus))); + + RTCritSectLeave(&pSink->CritSect); + return VINF_SUCCESS; +} + + +/** + * Helper for AudioMixerSinkDrainAndStop that calculates the max length a drain + * operation should take. + * + * @returns The drain deadline (relative to RTTimeNanoTS). + * @param pSink The sink. + * @param cbDmaLeftToDrain The number of bytes in the DMA buffer left to + * transfer into the mixbuf. + */ +static uint64_t audioMixerSinkDrainDeadline(PAUDMIXSINK pSink, uint32_t cbDmaLeftToDrain) +{ + /* + * Calculate the max backend buffer size in mixbuf frames. + * (This is somewhat similar to audioMixerSinkUpdateOutputCalcFramesToRead.) + */ + uint32_t cFramesStreamMax = 0; + PAUDMIXSTREAM pMixStream; + RTListForEach(&pSink->lstStreams, pMixStream, AUDMIXSTREAM, Node) + { + /*LogFunc(("Stream '%s': %#x (%u frames)\n", pMixStream->pszName, pMixStream->fStatus, pMixStream->cFramesBackendBuffer));*/ + if (pMixStream->fStatus & AUDMIXSTREAM_STATUS_CAN_WRITE) + { + uint32_t cFrames = pMixStream->cFramesBackendBuffer; + if (PDMAudioPropsHz(&pMixStream->pStream->Cfg.Props) == PDMAudioPropsHz(&pSink->MixBuf.Props)) + { /* likely */ } + else + cFrames = cFrames * PDMAudioPropsHz(&pSink->MixBuf.Props) / PDMAudioPropsHz(&pMixStream->pStream->Cfg.Props); + if (cFrames > cFramesStreamMax) + { + Log4Func(("%s: cFramesStreamMax %u -> %u; %s\n", pSink->pszName, cFramesStreamMax, cFrames, pMixStream->pszName)); + cFramesStreamMax = cFrames; + } + } + } + + /* + * Combine that with the pending DMA and mixbuf content, then convert + * to nanoseconds and apply a fudge factor to get a generous deadline. + */ + uint32_t const cFramesDmaAndMixBuf = PDMAudioPropsBytesToFrames(&pSink->MixBuf.Props, cbDmaLeftToDrain) + + AudioMixBufUsed(&pSink->MixBuf); + uint64_t const cNsToDrainMax = PDMAudioPropsFramesToNano(&pSink->MixBuf.Props, cFramesDmaAndMixBuf + cFramesStreamMax); + uint64_t const nsDeadline = cNsToDrainMax * 2; + LogFlowFunc(("%s: cFramesStreamMax=%#x cFramesDmaAndMixBuf=%#x -> cNsToDrainMax=%RU64 -> %RU64\n", + pSink->pszName, cFramesStreamMax, cFramesDmaAndMixBuf, cNsToDrainMax, nsDeadline)); + return nsDeadline; +} + + +/** + * Kicks off the draining and stopping playback/capture on the mixer sink. + * + * For input streams this causes an immediate stop, as draining only makes sense + * to output stream in the VBox device context. + * + * @returns VBox status code. Generally always VINF_SUCCESS unless the input + * is invalid. Individual driver errors are suppressed and ignored. + * @param pSink Mixer sink to control. + * @param cbComming The number of bytes still left in the device's DMA + * buffers that the update job has yet to transfer. This + * is ignored for input streams. + */ +int AudioMixerSinkDrainAndStop(PAUDMIXSINK pSink, uint32_t cbComming) +{ + AssertPtrReturn(pSink, VERR_INVALID_POINTER); + Assert(pSink->uMagic == AUDMIXSINK_MAGIC); + + int rc = RTCritSectEnter(&pSink->CritSect); + AssertRCReturn(rc, rc); + char szStatus[AUDIOMIXERSINK_STATUS_STR_MAX]; + LogFunc(("Draining '%s' with %#x bytes left. Old status: %s\n", + pSink->pszName, cbComming, dbgAudioMixerSinkStatusToStr(pSink->fStatus, szStatus) )); + + AssertReturnStmt(pSink->enmDir == PDMAUDIODIR_IN || pSink->enmDir == PDMAUDIODIR_OUT, + RTCritSectLeave(&pSink->CritSect), VERR_INTERNAL_ERROR_3); + + if (pSink->fStatus & AUDMIXSINK_STS_RUNNING) + { + /* + * Output streams will be drained then stopped (all by the AIO thread). + * + * For streams we define that they shouldn't not be written to after we start draining, + * so we have to hold back sending the command to them till we've processed all the + * cbComming remaining bytes in the DMA buffer. + */ + if (pSink->enmDir == PDMAUDIODIR_OUT) + { + if (!(pSink->fStatus & AUDMIXSINK_STS_DRAINING)) + { + Assert(!(pSink->fStatus & (AUDMIXSINK_STS_DRAINED_DMA | AUDMIXSINK_STS_DRAINED_MIXBUF))); + + /* Update the status and draining member. */ + pSink->cbDmaLeftToDrain = cbComming; + pSink->nsDrainDeadline = audioMixerSinkDrainDeadline(pSink, cbComming); + if (pSink->nsDrainDeadline > 0) + { + pSink->nsDrainStarted = RTTimeNanoTS(); + pSink->nsDrainDeadline += pSink->nsDrainStarted; + pSink->fStatus |= AUDMIXSINK_STS_DRAINING; + + /* Kick the AIO thread so it can keep pushing data till we're out of this + status. (The device's DMA timer won't kick it any more, so we must.) */ + AudioMixerSinkSignalUpdateJob(pSink); + } + else + { + LogFunc(("%s: No active streams, doing an immediate stop.\n", pSink->pszName)); + PAUDMIXSTREAM pStream; + RTListForEach(&pSink->lstStreams, pStream, AUDMIXSTREAM, Node) + { + audioMixerStreamCtlInternal(pStream, PDMAUDIOSTREAMCMD_DISABLE); + } + audioMixerSinkResetInternal(pSink); + } + } + else + AssertMsgFailed(("Already draining '%s': %s\n", + pSink->pszName, dbgAudioMixerSinkStatusToStr(pSink->fStatus, szStatus))); + } + /* + * Input sinks are stopped immediately. + * + * It's the guest giving order here and we can't force it to accept data that's + * already in the buffer pipeline or anything. So, there can be no draining here. + */ + else + { + PAUDMIXSTREAM pStream; + RTListForEach(&pSink->lstStreams, pStream, AUDMIXSTREAM, Node) + { + audioMixerStreamCtlInternal(pStream, PDMAUDIOSTREAMCMD_DISABLE); + } + audioMixerSinkResetInternal(pSink); + } + } + else + LogFunc(("%s: Not running\n", pSink->pszName)); + + LogRel2(("Audio Mixer: Started draining sink '%s': %s\n", pSink->pszName, dbgAudioMixerSinkStatusToStr(pSink->fStatus, szStatus))); + RTCritSectLeave(&pSink->CritSect); + return VINF_SUCCESS; +} + + +/** + * Destroys and frees a mixer sink. + * + * Worker for AudioMixerSinkDestroy(), AudioMixerCreateSink() and + * AudioMixerDestroy(). + * + * @param pSink Mixer sink to destroy. + * @param pDevIns The device instance statistics are registered with. + */ +static void audioMixerSinkDestroyInternal(PAUDMIXSINK pSink, PPDMDEVINS pDevIns) +{ + AssertPtrReturnVoid(pSink); + + LogFunc(("%s\n", pSink->pszName)); + + /* + * Invalidate the sink instance. + */ + Assert(pSink->uMagic == AUDMIXSINK_MAGIC); + pSink->uMagic = AUDMIXSINK_MAGIC_DEAD; + + int rc = RTCritSectEnter(&pSink->CritSect); + AssertRCReturnVoid(rc); + + /* + * Destroy all streams. + */ + PAUDMIXSTREAM pStream, pStreamNext; + RTListForEachSafe(&pSink->lstStreams, pStream, pStreamNext, AUDMIXSTREAM, Node) + { + audioMixerSinkRemoveStreamInternal(pSink, pStream); + audioMixerStreamDestroyInternal(pStream, pDevIns, true /*fImmediate*/); + } + + rc = RTCritSectLeave(&pSink->CritSect); + AssertRCReturnVoid(rc); + + /* + * Destroy debug file and statistics. + */ + if (!pSink->Dbg.pFile) + { /* likely */ } + else + { + AudioHlpFileDestroy(pSink->Dbg.pFile); + pSink->Dbg.pFile = NULL; + } + + char szPrefix[128]; + RTStrPrintf(szPrefix, sizeof(szPrefix), "MixerSink-%s/", pSink->pszName); + PDMDevHlpSTAMDeregisterByPrefix(pDevIns, szPrefix); + + /* + * Shutdown the AIO thread if started: + */ + ASMAtomicWriteBool(&pSink->AIO.fShutdown, true); + if (pSink->AIO.hEvent != NIL_RTSEMEVENT) + { + int rc2 = RTSemEventSignal(pSink->AIO.hEvent); + AssertRC(rc2); + } + if (pSink->AIO.hThread != NIL_RTTHREAD) + { + LogFlowFunc(("Waiting for AIO thread for %s...\n", pSink->pszName)); + int rc2 = RTThreadWait(pSink->AIO.hThread, RT_MS_30SEC, NULL); + AssertRC(rc2); + pSink->AIO.hThread = NIL_RTTHREAD; + } + if (pSink->AIO.hEvent != NIL_RTSEMEVENT) + { + int rc2 = RTSemEventDestroy(pSink->AIO.hEvent); + AssertRC(rc2); + pSink->AIO.hEvent = NIL_RTSEMEVENT; + } + + /* + * Mixing buffer, critsect and the structure itself. + */ + AudioMixBufTerm(&pSink->MixBuf); + RTCritSectDelete(&pSink->CritSect); + RTMemFree(pSink); +} + + +/** + * Destroys a mixer sink and removes it from the attached mixer (if any). + * + * @param pSink Mixer sink to destroy. NULL is ignored. + * @param pDevIns The device instance that statistics are registered with. + */ +void AudioMixerSinkDestroy(PAUDMIXSINK pSink, PPDMDEVINS pDevIns) +{ + if (!pSink) + return; + AssertReturnVoid(pSink->uMagic == AUDMIXSINK_MAGIC); + + /* + * Serializing paranoia. + */ + int rc = RTCritSectEnter(&pSink->CritSect); + AssertRCReturnVoid(rc); + RTCritSectLeave(&pSink->CritSect); + + /* + * Unlink from parent. + */ + PAUDIOMIXER pMixer = pSink->pParent; + if ( RT_VALID_PTR(pMixer) + && pMixer->uMagic == AUDIOMIXER_MAGIC) + { + RTCritSectEnter(&pMixer->CritSect); + audioMixerRemoveSinkInternal(pMixer, pSink); + RTCritSectLeave(&pMixer->CritSect); + } + else if (pMixer) + AssertFailed(); + + /* + * Actually destroy it. + */ + audioMixerSinkDestroyInternal(pSink, pDevIns); +} + + +/** + * Get the number of bytes that can be read from the sink. + * + * @returns Number of bytes. + * @param pSink The mixer sink. + * + * @note Only applicable to input sinks, will assert and return zero for + * other sink directions. + */ +uint32_t AudioMixerSinkGetReadable(PAUDMIXSINK pSink) +{ + AssertPtrReturn(pSink, 0); + AssertReturn(pSink->uMagic == AUDMIXSINK_MAGIC, 0); + AssertMsgReturn(pSink->enmDir == PDMAUDIODIR_IN, ("%s: Can't read from a non-input sink\n", pSink->pszName), 0); + + int rc = RTCritSectEnter(&pSink->CritSect); + AssertRCReturn(rc, 0); + + uint32_t cbReadable = 0; + if (pSink->fStatus & AUDMIXSINK_STS_RUNNING) + cbReadable = AudioMixBufUsedBytes(&pSink->MixBuf); + + RTCritSectLeave(&pSink->CritSect); + Log3Func(("[%s] cbReadable=%#x\n", pSink->pszName, cbReadable)); + return cbReadable; +} + + +/** + * Get the number of bytes that can be written to be sink. + * + * @returns Number of bytes. + * @param pSink The mixer sink. + * + * @note Only applicable to output sinks, will assert and return zero for + * other sink directions. + */ +uint32_t AudioMixerSinkGetWritable(PAUDMIXSINK pSink) +{ + AssertPtrReturn(pSink, 0); + AssertReturn(pSink->uMagic == AUDMIXSINK_MAGIC, 0); + AssertMsgReturn(pSink->enmDir == PDMAUDIODIR_OUT, ("%s: Can't write to a non-output sink\n", pSink->pszName), 0); + + int rc = RTCritSectEnter(&pSink->CritSect); + AssertRCReturn(rc, 0); + + uint32_t cbWritable = 0; + if ((pSink->fStatus & (AUDMIXSINK_STS_RUNNING | AUDMIXSINK_STS_DRAINING)) == AUDMIXSINK_STS_RUNNING) + cbWritable = AudioMixBufFreeBytes(&pSink->MixBuf); + + RTCritSectLeave(&pSink->CritSect); + Log3Func(("[%s] cbWritable=%#x (%RU64ms)\n", pSink->pszName, cbWritable, + PDMAudioPropsBytesToMilli(&pSink->PCMProps, cbWritable) )); + return cbWritable; +} + + +/** + * Get the sink's mixing direction. + * + * @returns Mixing direction. + * @param pSink The mixer sink. + */ +PDMAUDIODIR AudioMixerSinkGetDir(PCAUDMIXSINK pSink) +{ + AssertPtrReturn(pSink, PDMAUDIODIR_INVALID); + AssertReturn(pSink->uMagic == AUDMIXSINK_MAGIC, PDMAUDIODIR_INVALID); + + /* The sink direction cannot be changed after creation, so no need for locking here. */ + return pSink->enmDir; +} + + +/** + * Get the sink status. + * + * @returns AUDMIXSINK_STS_XXX + * @param pSink The mixer sink. + */ +uint32_t AudioMixerSinkGetStatus(PAUDMIXSINK pSink) +{ + AssertPtrReturn(pSink, AUDMIXSINK_STS_NONE); + AssertReturn(pSink->uMagic == AUDMIXSINK_MAGIC, AUDMIXSINK_STS_NONE); + + int rc = RTCritSectEnter(&pSink->CritSect); + AssertRCReturn(rc, AUDMIXSINK_STS_NONE); + + uint32_t const fStsSink = pSink->fStatus; + + RTCritSectLeave(&pSink->CritSect); + return fStsSink; +} + + +/** + * Checks if the sink is active not. + * + * @note The pending disable state also counts as active. + * + * @retval true if active. + * @retval false if not active. + * @param pSink The mixer sink. NULL is okay (returns false). + */ +bool AudioMixerSinkIsActive(PAUDMIXSINK pSink) +{ + if (!pSink) + return false; + AssertPtr(pSink); + AssertReturn(pSink->uMagic == AUDMIXSINK_MAGIC, false); + + int rc = RTCritSectEnter(&pSink->CritSect); + AssertRCReturn(rc, false); + + bool const fIsActive = RT_BOOL(pSink->fStatus & AUDMIXSINK_STS_RUNNING); + + RTCritSectLeave(&pSink->CritSect); + Log3Func(("[%s] returns %RTbool\n", pSink->pszName, fIsActive)); + return fIsActive; +} + + +/** + * Resets the sink's state. + * + * @param pSink The sink to reset. + * @note Must own sink lock. + */ +static void audioMixerSinkResetInternal(PAUDMIXSINK pSink) +{ + Assert(RTCritSectIsOwner(&pSink->CritSect)); + LogFunc(("[%s]\n", pSink->pszName)); + + /* Drop mixing buffer content. */ + AudioMixBufDrop(&pSink->MixBuf); + + /* Reset status. */ + pSink->fStatus = AUDMIXSINK_STS_NONE; + pSink->tsLastUpdatedMs = 0; +} + + +/** + * Resets a sink. This will immediately stop all processing. + * + * @param pSink Sink to reset. + */ +void AudioMixerSinkReset(PAUDMIXSINK pSink) +{ + if (!pSink) + return; + AssertReturnVoid(pSink->uMagic == AUDMIXSINK_MAGIC); + + int rc = RTCritSectEnter(&pSink->CritSect); + AssertRCReturnVoid(rc); + + LogFlowFunc(("[%s]\n", pSink->pszName)); + + /* + * Stop any stream that's enabled before resetting the state. + */ + PAUDMIXSTREAM pStream; + RTListForEach(&pSink->lstStreams, pStream, AUDMIXSTREAM, Node) + { + if (pStream->fStatus & AUDMIXSTREAM_STATUS_ENABLED) + audioMixerStreamCtlInternal(pStream, PDMAUDIOSTREAMCMD_DISABLE); + } + + /* + * Reset the state. + */ + audioMixerSinkResetInternal(pSink); + + RTCritSectLeave(&pSink->CritSect); +} + + +/** + * Sets the audio format of a mixer sink. + * + * @returns VBox status code. + * @param pSink The sink to set audio format for. + * @param pProps The properties of the new audio format (guest side). + * @param cMsSchedulingHint Scheduling hint for mixer buffer sizing. + */ +int AudioMixerSinkSetFormat(PAUDMIXSINK pSink, PCPDMAUDIOPCMPROPS pProps, uint32_t cMsSchedulingHint) +{ + AssertPtrReturn(pSink, VERR_INVALID_POINTER); + AssertReturn(pSink->uMagic == AUDMIXSINK_MAGIC, VERR_INVALID_MAGIC); + AssertPtrReturn(pProps, VERR_INVALID_POINTER); + AssertReturn(AudioHlpPcmPropsAreValidAndSupported(pProps), VERR_INVALID_PARAMETER); + + /* + * Calculate the mixer buffer size so we can force a recreation if it changes. + * + * This used to be fixed at 100ms, however that's usually too generous and can + * in theory be too small. Generally, we size the buffer at 3 DMA periods as + * that seems reasonable. Now, since the we don't quite trust the scheduling + * hint we're getting, make sure we're got a minimum of 30ms buffer space, but + * no more than 500ms. + */ + if (cMsSchedulingHint <= 10) + cMsSchedulingHint = 30; + else + { + cMsSchedulingHint *= 3; + if (cMsSchedulingHint > 500) + cMsSchedulingHint = 500; + } + uint32_t const cBufferFrames = PDMAudioPropsMilliToFrames(pProps, cMsSchedulingHint); + /** @todo configuration override on the buffer size? */ + + int rc = RTCritSectEnter(&pSink->CritSect); + AssertRCReturn(rc, rc); + + /* + * Do nothing unless the format actually changed. + * The buffer size must not match exactly, within +/- 2% is okay. + */ + uint32_t cOldBufferFrames; + if ( !PDMAudioPropsAreEqual(&pSink->PCMProps, pProps) + || ( cBufferFrames != (cOldBufferFrames = AudioMixBufSize(&pSink->MixBuf)) + && (uint32_t)RT_ABS((int32_t)(cBufferFrames - cOldBufferFrames)) > cBufferFrames / 50) ) + { +#ifdef LOG_ENABLED + char szTmp[PDMAUDIOPROPSTOSTRING_MAX]; +#endif + if (PDMAudioPropsHz(&pSink->PCMProps) != 0) + LogFlowFunc(("[%s] Old format: %s; buffer: %u frames\n", pSink->pszName, + PDMAudioPropsToString(&pSink->PCMProps, szTmp, sizeof(szTmp)), AudioMixBufSize(&pSink->MixBuf) )); + pSink->PCMProps = *pProps; + LogFlowFunc(("[%s] New format: %s; buffer: %u frames\n", pSink->pszName, + PDMAudioPropsToString(&pSink->PCMProps, szTmp, sizeof(szTmp)), cBufferFrames )); + + /* + * Also update the sink's mixing buffer format. + */ + AudioMixBufTerm(&pSink->MixBuf); + + rc = AudioMixBufInit(&pSink->MixBuf, pSink->pszName, &pSink->PCMProps, cBufferFrames); + if (RT_SUCCESS(rc)) + { + /* + * Input sinks must init their (mostly dummy) peek state. + */ + if (pSink->enmDir == PDMAUDIODIR_IN) + rc = AudioMixBufInitPeekState(&pSink->MixBuf, &pSink->In.State, &pSink->PCMProps); + else + rc = AudioMixBufInitWriteState(&pSink->MixBuf, &pSink->Out.State, &pSink->PCMProps); + if (RT_SUCCESS(rc)) + { + /* + * Re-initialize the peek/write states as the frequency, channel count + * and other things may have changed now. + */ + PAUDMIXSTREAM pMixStream; + if (pSink->enmDir == PDMAUDIODIR_IN) + { + RTListForEach(&pSink->lstStreams, pMixStream, AUDMIXSTREAM, Node) + { + int rc2 = AudioMixBufInitWriteState(&pSink->MixBuf, &pMixStream->WriteState, &pMixStream->pStream->Cfg.Props); + /** @todo remember this. */ + AssertLogRelRC(rc2); + } + } + else + { + RTListForEach(&pSink->lstStreams, pMixStream, AUDMIXSTREAM, Node) + { + int rc2 = AudioMixBufInitPeekState(&pSink->MixBuf, &pMixStream->PeekState, &pMixStream->pStream->Cfg.Props); + /** @todo remember this. */ + AssertLogRelRC(rc2); + } + } + + /* + * Debug. + */ + if (!(pSink->pParent->fFlags & AUDMIXER_FLAGS_DEBUG)) + { /* likely */ } + else + { + AudioHlpFileClose(pSink->Dbg.pFile); + + char szName[64]; + RTStrPrintf(szName, sizeof(szName), "MixerSink-%s", pSink->pszName); + AudioHlpFileCreateAndOpen(&pSink->Dbg.pFile, NULL /*pszDir - use temp dir*/, szName, + 0 /*iInstance*/, &pSink->PCMProps); + } + } + else + LogFunc(("%s failed: %Rrc\n", + pSink->enmDir == PDMAUDIODIR_IN ? "AudioMixBufInitPeekState" : "AudioMixBufInitWriteState", rc)); + } + else + LogFunc(("AudioMixBufInit failed: %Rrc\n", rc)); + } + + RTCritSectLeave(&pSink->CritSect); + LogFlowFuncLeaveRC(rc); + return rc; +} + + +/** + * Updates the combined volume (sink + mixer) of a mixer sink. + * + * @returns VBox status code. + * @param pSink The mixer sink to update volume for (valid). + * @param pVolMaster The master (mixer) volume (valid). + */ +static int audioMixerSinkUpdateVolume(PAUDMIXSINK pSink, PCPDMAUDIOVOLUME pVolMaster) +{ + AssertPtr(pSink); + Assert(pSink->uMagic == AUDMIXSINK_MAGIC); + AssertPtr(pVolMaster); + LogFlowFunc(("[%s] Master fMuted=%RTbool auChannels=%.*Rhxs\n", + pSink->pszName, pVolMaster->fMuted, sizeof(pVolMaster->auChannels), pVolMaster->auChannels)); + + PDMAudioVolumeCombine(&pSink->VolumeCombined, &pSink->Volume, pVolMaster); + + LogFlowFunc(("[%s] fMuted=%RTbool auChannels=%.*Rhxs -> fMuted=%RTbool auChannels=%.*Rhxs\n", pSink->pszName, + pSink->Volume.fMuted, sizeof(pSink->Volume.auChannels), pSink->Volume.auChannels, + pSink->VolumeCombined.fMuted, sizeof(pSink->VolumeCombined.auChannels), pSink->VolumeCombined.auChannels )); + + AudioMixBufSetVolume(&pSink->MixBuf, &pSink->VolumeCombined); + return VINF_SUCCESS; +} + + +/** + * Sets the volume a mixer sink. + * + * @returns VBox status code. + * @param pSink The sink to set volume for. + * @param pVol New volume settings. + */ +int AudioMixerSinkSetVolume(PAUDMIXSINK pSink, PCPDMAUDIOVOLUME pVol) +{ + AssertPtrReturn(pSink, VERR_INVALID_POINTER); + AssertReturn(pSink->uMagic == AUDMIXSINK_MAGIC, VERR_INVALID_MAGIC); + AssertPtrReturn(pVol, VERR_INVALID_POINTER); + + int rc = RTCritSectEnter(&pSink->CritSect); + AssertRCReturn(rc, rc); + + memcpy(&pSink->Volume, pVol, sizeof(PDMAUDIOVOLUME)); + + LogRel2(("Audio Mixer: Setting volume of sink '%s' to fMuted=%RTbool auChannels=%.*Rhxs\n", + pSink->pszName, pVol->fMuted, sizeof(pVol->auChannels), pVol->auChannels)); + + Assert(pSink->pParent); + if (pSink->pParent) + rc = audioMixerSinkUpdateVolume(pSink, &pSink->pParent->VolMaster); + + RTCritSectLeave(&pSink->CritSect); + + return rc; +} + + +/** + * Helper for audioMixerSinkUpdateInput that determins now many frames it can + * transfer from the drivers and into the sink's mixer buffer. + * + * This also updates the mixer stream status, which may involve stream re-inits. + * + * @returns Number of frames. + * @param pSink The sink. + * @param pcReadableStreams Where to return the number of readable streams. + */ +static uint32_t audioMixerSinkUpdateInputCalcFramesToTransfer(PAUDMIXSINK pSink, uint32_t *pcReadableStreams) +{ + uint32_t cFramesToRead = AudioMixBufFree(&pSink->MixBuf); + uint32_t cReadableStreams = 0; + PAUDMIXSTREAM pMixStream; + RTListForEach(&pSink->lstStreams, pMixStream, AUDMIXSTREAM, Node) + { + int rc2 = audioMixerStreamUpdateStatus(pMixStream); + AssertRC(rc2); + + if (pMixStream->fStatus & AUDMIXSTREAM_STATUS_CAN_READ) + { + PPDMIAUDIOCONNECTOR const pIConnector = pMixStream->pConn; + PPDMAUDIOSTREAM const pStream = pMixStream->pStream; + pIConnector->pfnStreamIterate(pIConnector, pStream); + + uint32_t const cbReadable = pIConnector->pfnStreamGetReadable(pIConnector, pStream); + uint32_t cFrames = PDMAudioPropsBytesToFrames(&pStream->Cfg.Props, cbReadable); + pMixStream->cFramesLastAvail = cFrames; + if (PDMAudioPropsHz(&pStream->Cfg.Props) == PDMAudioPropsHz(&pSink->MixBuf.Props)) + { /* likely */ } + else + { + cFrames = cFrames * PDMAudioPropsHz(&pSink->MixBuf.Props) / PDMAudioPropsHz(&pStream->Cfg.Props); + cFrames = cFrames > 2 ? cFrames - 2 : 0; /* rounding safety fudge */ + } + if (cFramesToRead > cFrames && !pMixStream->fUnreliable) + { + Log4Func(("%s: cFramesToRead %u -> %u; %s (%u bytes readable)\n", + pSink->pszName, cFramesToRead, cFrames, pMixStream->pszName, cbReadable)); + cFramesToRead = cFrames; + } + cReadableStreams++; + } + } + + *pcReadableStreams = cReadableStreams; + return cFramesToRead; +} + + +/** + * Updates an input mixer sink. + * + * @returns VBox status code. + * @param pSink Mixer sink to update. + * @param cbDmaBuf The number of bytes in the DMA buffer. For detecting + * underruns. Zero if we don't know. + * @param cbDmaPeriod The minimum number of bytes required for reliable DMA + * operation. Zero if we don't know. + */ +static int audioMixerSinkUpdateInput(PAUDMIXSINK pSink, uint32_t cbDmaBuf, uint32_t cbDmaPeriod) +{ + PAUDMIXSTREAM pMixStream; + Assert(!(pSink->fStatus & AUDMIXSINK_STS_DRAINED_MIXBUF)); /* (can't drain input sink) */ + + /* + * Iterate, update status and check each mixing sink stream for how much + * we can transfer. + * + * We're currently using the minimum size of all streams, however this + * isn't a smart approach as it means one disfunctional stream can block + * working ones. So, if we end up with zero frames and a full mixer + * buffer we'll disregard the stream that accept the smallest amount and + * try again. + */ + uint32_t cReadableStreams = 0; + uint32_t cFramesToXfer = audioMixerSinkUpdateInputCalcFramesToTransfer(pSink, &cReadableStreams); + if ( cFramesToXfer != 0 + || cReadableStreams <= 1 + || cbDmaPeriod == 0 /* Insufficient info to decide. The update function will call us again, at least for HDA. */ + || cbDmaBuf + PDMAudioPropsFramesToBytes(&pSink->PCMProps, AudioMixBufUsed(&pSink->MixBuf)) >= cbDmaPeriod) + Log3Func(("%s: cFreeFrames=%#x cFramesToXfer=%#x cReadableStreams=%#x\n", pSink->pszName, + AudioMixBufFree(&pSink->MixBuf), cFramesToXfer, cReadableStreams)); + else + { + Log3Func(("%s: MixBuf is underrunning but one or more streams only provides zero frames. Try disregarding those...\n", pSink->pszName)); + uint32_t cReliableStreams = 0; + uint32_t cMarkedUnreliable = 0; + PAUDMIXSTREAM pMixStreamMin = NULL; + RTListForEach(&pSink->lstStreams, pMixStream, AUDMIXSTREAM, Node) + { + if (pMixStream->fStatus & AUDMIXSTREAM_STATUS_CAN_READ) + { + if (!pMixStream->fUnreliable) + { + if (pMixStream->cFramesLastAvail == 0) + { + cMarkedUnreliable++; + pMixStream->fUnreliable = true; + Log3Func(("%s: Marked '%s' as unreliable.\n", pSink->pszName, pMixStream->pszName)); + pMixStreamMin = pMixStream; + } + else + { + if (!pMixStreamMin || pMixStream->cFramesLastAvail < pMixStreamMin->cFramesLastAvail) + pMixStreamMin = pMixStream; + cReliableStreams++; + } + } + } + } + + if (cMarkedUnreliable == 0 && cReliableStreams > 1 && pMixStreamMin != NULL) + { + cReliableStreams--; + cMarkedUnreliable++; + pMixStreamMin->fUnreliable = true; + Log3Func(("%s: Marked '%s' as unreliable (%u frames).\n", + pSink->pszName, pMixStreamMin->pszName, pMixStreamMin->cFramesLastAvail)); + } + + if (cMarkedUnreliable > 0) + { + cReadableStreams = 0; + cFramesToXfer = audioMixerSinkUpdateInputCalcFramesToTransfer(pSink, &cReadableStreams); + } + + Log3Func(("%s: cFreeFrames=%#x cFramesToXfer=%#x cReadableStreams=%#x cMarkedUnreliable=%#x cReliableStreams=%#x\n", + pSink->pszName, AudioMixBufFree(&pSink->MixBuf), cFramesToXfer, + cReadableStreams, cMarkedUnreliable, cReliableStreams)); + } + + if (cReadableStreams > 0) + { + if (cFramesToXfer > 0) + { +/*#define ELECTRIC_INPUT_BUFFER*/ /* if buffer code is misbehaving, enable this to catch overflows. */ +#ifndef ELECTRIC_INPUT_BUFFER + union + { + uint8_t ab[8192]; + uint64_t au64[8192 / sizeof(uint64_t)]; /* Use uint64_t to ensure good alignment. */ + } Buf; + void * const pvBuf = &Buf; + uint32_t const cbBuf = sizeof(Buf); +#else + uint32_t const cbBuf = 0x2000 - 16; + void * const pvBuf = RTMemEfAlloc(cbBuf, RTMEM_TAG, RT_SRC_POS); +#endif + + /* + * For each of the enabled streams, read cFramesToXfer frames worth + * of samples from them and merge that into the mixing buffer. + */ + bool fAssign = true; + RTListForEach(&pSink->lstStreams, pMixStream, AUDMIXSTREAM, Node) + { + if (pMixStream->fStatus & AUDMIXSTREAM_STATUS_CAN_READ) + { + PPDMIAUDIOCONNECTOR const pIConnector = pMixStream->pConn; + PPDMAUDIOSTREAM const pStream = pMixStream->pStream; + + /* Calculate how many bytes we should read from this stream. */ + bool const fResampleSrc = PDMAudioPropsHz(&pStream->Cfg.Props) != PDMAudioPropsHz(&pSink->MixBuf.Props); + uint32_t const cbSrcToXfer = !fResampleSrc + ? PDMAudioPropsFramesToBytes(&pStream->Cfg.Props, cFramesToXfer) + : PDMAudioPropsFramesToBytes(&pStream->Cfg.Props, /** @todo check rounding errors here... */ + cFramesToXfer * PDMAudioPropsHz(&pSink->MixBuf.Props) + / PDMAudioPropsHz(&pStream->Cfg.Props)); + + /* Do the reading. */ + uint32_t offSrc = 0; + uint32_t offDstFrame = 0; + do + { + /* + * Read a chunk from the backend. + */ + uint32_t const cbSrcToRead = RT_MIN(cbBuf, cbSrcToXfer - offSrc); + uint32_t cbSrcRead = 0; + if (cbSrcToRead > 0) + { + int rc2 = pIConnector->pfnStreamCapture(pIConnector, pStream, pvBuf, cbSrcToRead, &cbSrcRead); + Log3Func(("%s: %#x L %#x => %#x bytes; rc2=%Rrc %s\n", + pSink->pszName, offSrc, cbSrcToRead, cbSrcRead, rc2, pMixStream->pszName)); + + if (RT_SUCCESS(rc2)) + AssertLogRelMsg(cbSrcRead == cbSrcToRead || pMixStream->fUnreliable, + ("cbSrcRead=%#x cbSrcToRead=%#x - (sink '%s')\n", + cbSrcRead, cbSrcToRead, pSink->pszName)); + else if (rc2 == VERR_AUDIO_STREAM_NOT_READY) + { + LogRel2(("Audio Mixer: '%s' (sink '%s'): Stream not ready - skipping.\n", + pMixStream->pszName, pSink->pszName)); /* must've changed status, stop processing */ + break; + } + else + { + Assert(rc2 != VERR_BUFFER_OVERFLOW); + LogRel2(("Audio Mixer: Reading from mixer stream '%s' (sink '%s') failed, rc=%Rrc\n", + pMixStream->pszName, pSink->pszName, rc2)); + break; + } + offSrc += cbSrcRead; + } + else + Assert(fResampleSrc); /** @todo test this case */ + + /* + * Assign or blend it into the mixer buffer. + */ + uint32_t cFramesDstTransferred = 0; + if (fAssign) + { + /** @todo could complicate this by detecting silence here too and stay in + * assign mode till we get a stream with non-silence... */ + AudioMixBufWrite(&pSink->MixBuf, &pMixStream->WriteState, pvBuf, cbSrcRead, + offDstFrame, cFramesToXfer - offDstFrame, &cFramesDstTransferred); + } + /* We don't need to blend silence buffers. For simplicity, always blend + when we're resampling (for rounding). */ + else if (fResampleSrc || !PDMAudioPropsIsBufferSilence(&pStream->Cfg.Props, pvBuf, cbSrcRead)) + { + AudioMixBufBlend(&pSink->MixBuf, &pMixStream->WriteState, pvBuf, cbSrcRead, + offDstFrame, cFramesToXfer - offDstFrame, &cFramesDstTransferred); + } + else + { + cFramesDstTransferred = PDMAudioPropsBytesToFrames(&pStream->Cfg.Props, cbSrcRead); + AudioMixBufBlendGap(&pSink->MixBuf, &pMixStream->WriteState, cFramesDstTransferred); + } + AssertBreak(cFramesDstTransferred > 0); + + /* Advance. */ + offDstFrame += cFramesDstTransferred; + } while (offDstFrame < cFramesToXfer); + + /* + * In case the first stream is misbehaving, make sure we written the entire area. + */ + if (offDstFrame >= cFramesToXfer) + { /* likely */ } + else if (fAssign) + AudioMixBufSilence(&pSink->MixBuf, &pMixStream->WriteState, offDstFrame, cFramesToXfer - offDstFrame); + else + AudioMixBufBlendGap(&pSink->MixBuf, &pMixStream->WriteState, cFramesToXfer - offDstFrame); + fAssign = false; + } + } + + /* + * Commit the buffer area we've written and blended into. + */ + AudioMixBufCommit(&pSink->MixBuf, cFramesToXfer); + +#ifdef ELECTRIC_INPUT_BUFFER + RTMemEfFree(pvBuf, RT_SRC_POS); +#endif + } + + /* + * Set the dirty flag for what it's worth. + */ + pSink->fStatus |= AUDMIXSINK_STS_DIRTY; + } + else + { + /* + * No readable stream. Clear the dirty flag if empty (pointless flag). + */ + if (!AudioMixBufUsed(&pSink->MixBuf)) + pSink->fStatus &= ~AUDMIXSINK_STS_DIRTY; + } + + /* Update last updated timestamp. */ + pSink->tsLastUpdatedMs = RTTimeMilliTS(); + + return VINF_SUCCESS; +} + + +/** + * Helper for audioMixerSinkUpdateOutput that determins now many frames it + * can transfer from the sink's mixer buffer and to the drivers. + * + * This also updates the mixer stream status, which may involve stream re-inits. + * + * @returns Number of frames. + * @param pSink The sink. + * @param pcWritableStreams Where to return the number of writable streams. + */ +static uint32_t audioMixerSinkUpdateOutputCalcFramesToRead(PAUDMIXSINK pSink, uint32_t *pcWritableStreams) +{ + uint32_t cFramesToRead = AudioMixBufUsed(&pSink->MixBuf); /* (to read from the mixing buffer) */ + uint32_t cWritableStreams = 0; + PAUDMIXSTREAM pMixStream; + RTListForEach(&pSink->lstStreams, pMixStream, AUDMIXSTREAM, Node) + { +#if 0 /** @todo this conceptually makes sense, but may mess up the pending-disable logic ... */ + if (pMixStream->fStatus & AUDMIXSTREAM_STATUS_ENABLED) + pConn->pfnStreamIterate(pConn, pStream); +#endif + + int rc2 = audioMixerStreamUpdateStatus(pMixStream); + AssertRC(rc2); + + if (pMixStream->fStatus & AUDMIXSTREAM_STATUS_CAN_WRITE) + { + uint32_t const cbWritable = pMixStream->pConn->pfnStreamGetWritable(pMixStream->pConn, pMixStream->pStream); + uint32_t cFrames = PDMAudioPropsBytesToFrames(&pMixStream->pStream->Cfg.Props, cbWritable); + pMixStream->cFramesLastAvail = cFrames; + if (PDMAudioPropsHz(&pMixStream->pStream->Cfg.Props) == PDMAudioPropsHz(&pSink->MixBuf.Props)) + { /* likely */ } + else + { + cFrames = cFrames * PDMAudioPropsHz(&pSink->MixBuf.Props) / PDMAudioPropsHz(&pMixStream->pStream->Cfg.Props); + cFrames = cFrames > 2 ? cFrames - 2 : 0; /* rounding safety fudge */ + } + if (cFramesToRead > cFrames && !pMixStream->fUnreliable) + { + Log4Func(("%s: cFramesToRead %u -> %u; %s (%u bytes writable)\n", + pSink->pszName, cFramesToRead, cFrames, pMixStream->pszName, cbWritable)); + cFramesToRead = cFrames; + } + cWritableStreams++; + } + } + + *pcWritableStreams = cWritableStreams; + return cFramesToRead; +} + + +/** + * Updates an output mixer sink. + * + * @returns VBox status code. + * @param pSink Mixer sink to update. + */ +static int audioMixerSinkUpdateOutput(PAUDMIXSINK pSink) +{ + PAUDMIXSTREAM pMixStream; + Assert(!(pSink->fStatus & AUDMIXSINK_STS_DRAINED_MIXBUF) || AudioMixBufUsed(&pSink->MixBuf) == 0); + + /* + * Update each mixing sink stream's status and check how much we can + * write into them. + * + * We're currently using the minimum size of all streams, however this + * isn't a smart approach as it means one disfunctional stream can block + * working ones. So, if we end up with zero frames and a full mixer + * buffer we'll disregard the stream that accept the smallest amount and + * try again. + */ + uint32_t cWritableStreams = 0; + uint32_t cFramesToRead = audioMixerSinkUpdateOutputCalcFramesToRead(pSink, &cWritableStreams); + if ( cFramesToRead != 0 + || cWritableStreams <= 1 + || AudioMixBufFree(&pSink->MixBuf) > 2) + Log3Func(("%s: cLiveFrames=%#x cFramesToRead=%#x cWritableStreams=%#x\n", pSink->pszName, + AudioMixBufUsed(&pSink->MixBuf), cFramesToRead, cWritableStreams)); + else + { + Log3Func(("%s: MixBuf is full but one or more streams only want zero frames. Try disregarding those...\n", pSink->pszName)); + uint32_t cReliableStreams = 0; + uint32_t cMarkedUnreliable = 0; + PAUDMIXSTREAM pMixStreamMin = NULL; + RTListForEach(&pSink->lstStreams, pMixStream, AUDMIXSTREAM, Node) + { + if (pMixStream->fStatus & AUDMIXSTREAM_STATUS_CAN_WRITE) + { + if (!pMixStream->fUnreliable) + { + if (pMixStream->cFramesLastAvail == 0) + { + cMarkedUnreliable++; + pMixStream->fUnreliable = true; + Log3Func(("%s: Marked '%s' as unreliable.\n", pSink->pszName, pMixStream->pszName)); + pMixStreamMin = pMixStream; + } + else + { + if (!pMixStreamMin || pMixStream->cFramesLastAvail < pMixStreamMin->cFramesLastAvail) + pMixStreamMin = pMixStream; + cReliableStreams++; + } + } + } + } + + if (cMarkedUnreliable == 0 && cReliableStreams > 1 && pMixStreamMin != NULL) + { + cReliableStreams--; + cMarkedUnreliable++; + pMixStreamMin->fUnreliable = true; + Log3Func(("%s: Marked '%s' as unreliable (%u frames).\n", + pSink->pszName, pMixStreamMin->pszName, pMixStreamMin->cFramesLastAvail)); + } + + if (cMarkedUnreliable > 0) + { + cWritableStreams = 0; + cFramesToRead = audioMixerSinkUpdateOutputCalcFramesToRead(pSink, &cWritableStreams); + } + + Log3Func(("%s: cLiveFrames=%#x cFramesToRead=%#x cWritableStreams=%#x cMarkedUnreliable=%#x cReliableStreams=%#x\n", + pSink->pszName, AudioMixBufUsed(&pSink->MixBuf), cFramesToRead, + cWritableStreams, cMarkedUnreliable, cReliableStreams)); + } + + if (cWritableStreams > 0) + { + if (cFramesToRead > 0) + { + /* + * For each of the enabled streams, convert cFramesToRead frames from + * the mixing buffer and write that to the downstream driver. + */ + RTListForEach(&pSink->lstStreams, pMixStream, AUDMIXSTREAM, Node) + { + if (pMixStream->fStatus & AUDMIXSTREAM_STATUS_CAN_WRITE) + { + uint32_t offSrcFrame = 0; + do + { + /* Convert a chunk from the mixer buffer. */ +/*#define ELECTRIC_PEEK_BUFFER*/ /* if buffer code is misbehaving, enable this to catch overflows. */ +#ifndef ELECTRIC_PEEK_BUFFER + union + { + uint8_t ab[8192]; + uint64_t au64[8192 / sizeof(uint64_t)]; /* Use uint64_t to ensure good alignment. */ + } Buf; + void * const pvBuf = &Buf; + uint32_t const cbBuf = sizeof(Buf); +#else + uint32_t const cbBuf = 0x2000 - 16; + void * const pvBuf = RTMemEfAlloc(cbBuf, RTMEM_TAG, RT_SRC_POS); +#endif + uint32_t cbDstPeeked = cbBuf; + uint32_t cSrcFramesPeeked = cFramesToRead - offSrcFrame; + AudioMixBufPeek(&pSink->MixBuf, offSrcFrame, cSrcFramesPeeked, &cSrcFramesPeeked, + &pMixStream->PeekState, pvBuf, cbBuf, &cbDstPeeked); + offSrcFrame += cSrcFramesPeeked; + + /* Write it to the backend. Since've checked that there is buffer + space available, this should always write the whole buffer unless + it's an unreliable stream. */ + uint32_t cbDstWritten = 0; + int rc2 = pMixStream->pConn->pfnStreamPlay(pMixStream->pConn, pMixStream->pStream, + pvBuf, cbDstPeeked, &cbDstWritten); + Log3Func(("%s: %#x L %#x => %#x bytes; wrote %#x rc2=%Rrc %s\n", pSink->pszName, offSrcFrame, + cSrcFramesPeeked - cSrcFramesPeeked, cbDstPeeked, cbDstWritten, rc2, pMixStream->pszName)); +#ifdef ELECTRIC_PEEK_BUFFER + RTMemEfFree(pvBuf, RT_SRC_POS); +#endif + if (RT_SUCCESS(rc2)) + AssertLogRelMsg(cbDstWritten == cbDstPeeked || pMixStream->fUnreliable, + ("cbDstWritten=%#x cbDstPeeked=%#x - (sink '%s')\n", + cbDstWritten, cbDstPeeked, pSink->pszName)); + else if (rc2 == VERR_AUDIO_STREAM_NOT_READY) + { + LogRel2(("Audio Mixer: '%s' (sink '%s'): Stream not ready - skipping.\n", + pMixStream->pszName, pSink->pszName)); + break; /* must've changed status, stop processing */ + } + else + { + Assert(rc2 != VERR_BUFFER_OVERFLOW); + LogRel2(("Audio Mixer: Writing to mixer stream '%s' (sink '%s') failed, rc=%Rrc\n", + pMixStream->pszName, pSink->pszName, rc2)); + break; + } + } while (offSrcFrame < cFramesToRead); + } + } + + AudioMixBufAdvance(&pSink->MixBuf, cFramesToRead); + } + + /* + * Update the dirty flag for what it's worth. + */ + if (AudioMixBufUsed(&pSink->MixBuf) > 0) + pSink->fStatus |= AUDMIXSINK_STS_DIRTY; + else + pSink->fStatus &= ~AUDMIXSINK_STS_DIRTY; + } + else + { + /* + * If no writable streams, just drop the mixer buffer content. + */ + AudioMixBufDrop(&pSink->MixBuf); + pSink->fStatus &= ~AUDMIXSINK_STS_DIRTY; + } + + /* + * Iterate buffers. + */ + RTListForEach(&pSink->lstStreams, pMixStream, AUDMIXSTREAM, Node) + { + if (pMixStream->fStatus & AUDMIXSTREAM_STATUS_ENABLED) + pMixStream->pConn->pfnStreamIterate(pMixStream->pConn, pMixStream->pStream); + } + + /* Update last updated timestamp. */ + uint64_t const nsNow = RTTimeNanoTS(); + pSink->tsLastUpdatedMs = nsNow / RT_NS_1MS; + + /* + * Deal with pending disable. + * We reset the sink when all streams have been disabled. + */ + if (!(pSink->fStatus & AUDMIXSINK_STS_DRAINING)) + { /* likely, till we get to the end */ } + else if (nsNow <= pSink->nsDrainDeadline) + { + /* Have we drained the mixbuf now? If so, update status and send drain + command to streams. (As mentioned elsewhere we don't want to confuse + driver code by sending drain command while there is still data to write.) */ + Assert((pSink->fStatus & AUDMIXSINK_STS_DIRTY) == (AudioMixBufUsed(&pSink->MixBuf) > 0 ? AUDMIXSINK_STS_DIRTY : 0)); + if ((pSink->fStatus & (AUDMIXSINK_STS_DRAINED_MIXBUF | AUDMIXSINK_STS_DIRTY)) == 0) + { + LogFunc(("Sink '%s': Setting AUDMIXSINK_STS_DRAINED_MIXBUF and sending drain command to streams (after %RU64 ns).\n", + pSink->pszName, nsNow - pSink->nsDrainStarted)); + pSink->fStatus |= AUDMIXSINK_STS_DRAINED_MIXBUF; + + RTListForEach(&pSink->lstStreams, pMixStream, AUDMIXSTREAM, Node) + { + pMixStream->pConn->pfnStreamControl(pMixStream->pConn, pMixStream->pStream, PDMAUDIOSTREAMCMD_DRAIN); + } + } + + /* Check if all streams has stopped, and if so we stop the sink. */ + uint32_t const cStreams = pSink->cStreams; + uint32_t cStreamsDisabled = pSink->cStreams; + RTListForEach(&pSink->lstStreams, pMixStream, AUDMIXSTREAM, Node) + { + if (pMixStream->fStatus & AUDMIXSTREAM_STATUS_ENABLED) + { + PDMAUDIOSTREAMSTATE const enmState = pMixStream->pConn->pfnStreamGetState(pMixStream->pConn, pMixStream->pStream); + if (enmState >= PDMAUDIOSTREAMSTATE_ENABLED) + cStreamsDisabled--; + } + } + + if (cStreamsDisabled != cStreams) + Log3Func(("Sink '%s': %u out of %u streams disabled (after %RU64 ns).\n", + pSink->pszName, cStreamsDisabled, cStreams, nsNow - pSink->nsDrainStarted)); + else + { + LogFunc(("Sink '%s': All %u streams disabled. Drain done after %RU64 ns.\n", + pSink->pszName, cStreamsDisabled, nsNow - pSink->nsDrainStarted)); + audioMixerSinkResetInternal(pSink); /* clears the status */ + } + } + else + { + /* Draining timed out. Just do an instant stop. */ + LogFunc(("Sink '%s': pending disable timed out after %RU64 ns!\n", pSink->pszName, nsNow - pSink->nsDrainStarted)); + RTListForEach(&pSink->lstStreams, pMixStream, AUDMIXSTREAM, Node) + { + pMixStream->pConn->pfnStreamControl(pMixStream->pConn, pMixStream->pStream, PDMAUDIOSTREAMCMD_DISABLE); + } + audioMixerSinkResetInternal(pSink); /* clears the status */ + } + + return VINF_SUCCESS; +} + +/** + * Updates (invalidates) a mixer sink. + * + * @returns VBox status code. + * @param pSink Mixer sink to update. + * @param cbDmaUsed The DMA buffer fill for input stream, ignored for + * output sinks. + * @param cbDmaPeriod The DMA period in bytes for input stream, ignored + * for output sinks. + */ +int AudioMixerSinkUpdate(PAUDMIXSINK pSink, uint32_t cbDmaUsed, uint32_t cbDmaPeriod) +{ + AssertPtrReturn(pSink, VERR_INVALID_POINTER); + Assert(pSink->uMagic == AUDMIXSINK_MAGIC); + int rc = RTCritSectEnter(&pSink->CritSect); + AssertRCReturn(rc, rc); + +#ifdef LOG_ENABLED + char szStatus[AUDIOMIXERSINK_STATUS_STR_MAX]; +#endif + Log3Func(("[%s] fStatus=%s\n", pSink->pszName, dbgAudioMixerSinkStatusToStr(pSink->fStatus, szStatus))); + + /* Only process running sinks. */ + if (pSink->fStatus & AUDMIXSINK_STS_RUNNING) + { + /* Do separate processing for input and output sinks. */ + if (pSink->enmDir == PDMAUDIODIR_OUT) + rc = audioMixerSinkUpdateOutput(pSink); + else if (pSink->enmDir == PDMAUDIODIR_IN) + rc = audioMixerSinkUpdateInput(pSink, cbDmaUsed, cbDmaPeriod); + else + AssertFailedStmt(rc = VERR_INTERNAL_ERROR_3); + } + else + rc = VINF_SUCCESS; /* disabled */ + + RTCritSectLeave(&pSink->CritSect); + return rc; +} + + +/** + * @callback_method_impl{FNRTTHREAD, Audio Mixer Sink asynchronous I/O thread} + */ +static DECLCALLBACK(int) audioMixerSinkAsyncIoThread(RTTHREAD hThreadSelf, void *pvUser) +{ + PAUDMIXSINK pSink = (PAUDMIXSINK)pvUser; + AssertPtr(pSink); + Assert(pSink->uMagic == AUDMIXSINK_MAGIC); + RT_NOREF(hThreadSelf); + + /* + * The run loop. + */ + LogFlowFunc(("%s: Entering run loop...\n", pSink->pszName)); + while (!pSink->AIO.fShutdown) + { + RTMSINTERVAL cMsSleep = RT_INDEFINITE_WAIT; + + RTCritSectEnter(&pSink->CritSect); + if (pSink->fStatus & (AUDMIXSINK_STS_RUNNING | AUDMIXSINK_STS_DRAINING)) + { + /* + * Before doing jobs, always update input sinks. + */ + if (pSink->enmDir == PDMAUDIODIR_IN) + audioMixerSinkUpdateInput(pSink, 0 /*cbDmaUsed*/, 0 /*cbDmaPeriod*/); + + /* + * Do the device specific updating. + */ + uintptr_t const cUpdateJobs = RT_MIN(pSink->AIO.cUpdateJobs, RT_ELEMENTS(pSink->AIO.aUpdateJobs)); + for (uintptr_t iJob = 0; iJob < cUpdateJobs; iJob++) + pSink->AIO.aUpdateJobs[iJob].pfnUpdate(pSink->AIO.pDevIns, pSink, pSink->AIO.aUpdateJobs[iJob].pvUser); + + /* + * Update output sinks after the updating. + */ + if (pSink->enmDir == PDMAUDIODIR_OUT) + audioMixerSinkUpdateOutput(pSink); + + /* + * If we're in draining mode, we use the smallest typical interval of the + * jobs for the next wait as we're unlikly to be woken up again by any + * DMA timer as it has normally stopped running at this point. + */ + if (!(pSink->fStatus & AUDMIXSINK_STS_DRAINING)) + { /* likely */ } + else + { + /** @todo Also do some kind of timeout here and do a forced stream disable w/o + * any draining if we exceed it. */ + cMsSleep = pSink->AIO.cMsMinTypicalInterval; + } + + } + RTCritSectLeave(&pSink->CritSect); + + /* + * Now block till we're signalled or + */ + if (!pSink->AIO.fShutdown) + { + int rc = RTSemEventWait(pSink->AIO.hEvent, cMsSleep); + AssertLogRelMsgReturn(RT_SUCCESS(rc) || rc == VERR_TIMEOUT, ("%s: RTSemEventWait -> %Rrc\n", pSink->pszName, rc), rc); + } + } + + LogFlowFunc(("%s: returnining normally.\n", pSink->pszName)); + return VINF_SUCCESS; +} + + +/** + * Adds an AIO update job to the sink. + * + * @returns VBox status code. + * @retval VERR_ALREADY_EXISTS if already registered job with same @a pvUser + * and @a pfnUpdate. + * + * @param pSink The mixer sink to remove the AIO job from. + * @param pfnUpdate The update callback for the job. + * @param pvUser The user parameter to pass to @a pfnUpdate. This should + * identify the job unique together with @a pfnUpdate. + * @param cMsTypicalInterval A typical interval between jobs in milliseconds. + * This is used when draining. + */ +int AudioMixerSinkAddUpdateJob(PAUDMIXSINK pSink, PFNAUDMIXSINKUPDATE pfnUpdate, void *pvUser, uint32_t cMsTypicalInterval) +{ + AssertPtrReturn(pSink, VERR_INVALID_POINTER); + Assert(pSink->uMagic == AUDMIXSINK_MAGIC); + int rc = RTCritSectEnter(&pSink->CritSect); + AssertRCReturn(rc, rc); + + /* + * Check that the job hasn't already been added. + */ + uintptr_t const iEnd = pSink->AIO.cUpdateJobs; + for (uintptr_t i = 0; i < iEnd; i++) + AssertReturnStmt( pvUser != pSink->AIO.aUpdateJobs[i].pvUser + || pfnUpdate != pSink->AIO.aUpdateJobs[i].pfnUpdate, + RTCritSectLeave(&pSink->CritSect), + VERR_ALREADY_EXISTS); + + AssertReturnStmt(iEnd < RT_ELEMENTS(pSink->AIO.aUpdateJobs), + RTCritSectLeave(&pSink->CritSect), + VERR_ALREADY_EXISTS); + + /* + * Create the thread if not already running or if it stopped. + */ +/** @todo move this to the sink "enable" code */ + if (pSink->AIO.hThread != NIL_RTTHREAD) + { + int rcThread = VINF_SUCCESS; + rc = RTThreadWait(pSink->AIO.hThread, 0, &rcThread); + if (RT_FAILURE_NP(rc)) + { /* likely */ } + else + { + LogRel(("Audio: AIO thread for '%s' died? rcThread=%Rrc\n", pSink->pszName, rcThread)); + pSink->AIO.hThread = NIL_RTTHREAD; + } + } + if (pSink->AIO.hThread == NIL_RTTHREAD) + { + LogFlowFunc(("%s: Starting AIO thread...\n", pSink->pszName)); + if (pSink->AIO.hEvent == NIL_RTSEMEVENT) + { + rc = RTSemEventCreate(&pSink->AIO.hEvent); + AssertRCReturnStmt(rc, RTCritSectLeave(&pSink->CritSect), rc); + } + static uint32_t volatile s_idxThread = 0; + uint32_t idxThread = ASMAtomicIncU32(&s_idxThread); + rc = RTThreadCreateF(&pSink->AIO.hThread, audioMixerSinkAsyncIoThread, pSink, 0 /*cbStack*/, RTTHREADTYPE_IO, + RTTHREADFLAGS_WAITABLE | RTTHREADFLAGS_COM_MTA, "MixAIO-%u", idxThread); + AssertRCReturnStmt(rc, RTCritSectLeave(&pSink->CritSect), rc); + } + + /* + * Finally, actually add the job. + */ + pSink->AIO.aUpdateJobs[iEnd].pfnUpdate = pfnUpdate; + pSink->AIO.aUpdateJobs[iEnd].pvUser = pvUser; + pSink->AIO.aUpdateJobs[iEnd].cMsTypicalInterval = cMsTypicalInterval; + pSink->AIO.cUpdateJobs = (uint8_t)(iEnd + 1); + if (cMsTypicalInterval < pSink->AIO.cMsMinTypicalInterval) + pSink->AIO.cMsMinTypicalInterval = cMsTypicalInterval; + LogFlowFunc(("%s: [#%zu]: Added pfnUpdate=%p pvUser=%p typically every %u ms (min %u ms)\n", + pSink->pszName, iEnd, pfnUpdate, pvUser, cMsTypicalInterval, pSink->AIO.cMsMinTypicalInterval)); + + RTCritSectLeave(&pSink->CritSect); + return VINF_SUCCESS; + +} + + +/** + * Removes an update job previously registered via AudioMixerSinkAddUpdateJob(). + * + * @returns VBox status code. + * @retval VERR_NOT_FOUND if not found. + * + * @param pSink The mixer sink to remove the AIO job from. + * @param pfnUpdate The update callback of the job. + * @param pvUser The user parameter identifying the job. + */ +int AudioMixerSinkRemoveUpdateJob(PAUDMIXSINK pSink, PFNAUDMIXSINKUPDATE pfnUpdate, void *pvUser) +{ + AssertPtrReturn(pSink, VERR_INVALID_POINTER); + Assert(pSink->uMagic == AUDMIXSINK_MAGIC); + int rc = RTCritSectEnter(&pSink->CritSect); + AssertRCReturn(rc, rc); + + rc = VERR_NOT_FOUND; + for (uintptr_t iJob = 0; iJob < pSink->AIO.cUpdateJobs; iJob++) + if ( pvUser == pSink->AIO.aUpdateJobs[iJob].pvUser + && pfnUpdate == pSink->AIO.aUpdateJobs[iJob].pfnUpdate) + { + pSink->AIO.cUpdateJobs--; + if (iJob != pSink->AIO.cUpdateJobs) + memmove(&pSink->AIO.aUpdateJobs[iJob], &pSink->AIO.aUpdateJobs[iJob + 1], + (pSink->AIO.cUpdateJobs - iJob) * sizeof(pSink->AIO.aUpdateJobs[0])); + LogFlowFunc(("%s: [#%zu]: Removed pfnUpdate=%p pvUser=%p => cUpdateJobs=%u\n", + pSink->pszName, iJob, pfnUpdate, pvUser, pSink->AIO.cUpdateJobs)); + rc = VINF_SUCCESS; + break; + } + AssertRC(rc); + + /* Recalc the minimum sleep interval (do it always). */ + pSink->AIO.cMsMinTypicalInterval = RT_MS_1SEC / 2; + for (uintptr_t iJob = 0; iJob < pSink->AIO.cUpdateJobs; iJob++) + if (pSink->AIO.aUpdateJobs[iJob].cMsTypicalInterval < pSink->AIO.cMsMinTypicalInterval) + pSink->AIO.cMsMinTypicalInterval = pSink->AIO.aUpdateJobs[iJob].cMsTypicalInterval; + + + RTCritSectLeave(&pSink->CritSect); + return rc; +} + + +/** + * Writes data to a mixer output sink. + * + * @param pSink The sink to write data to. + * @param pvBuf Buffer containing the audio data to write. + * @param cbBuf How many bytes to write. + * @param pcbWritten Number of bytes written. + * + * @todo merge with caller. + */ +static void audioMixerSinkWrite(PAUDMIXSINK pSink, const void *pvBuf, uint32_t cbBuf, uint32_t *pcbWritten) +{ + uint32_t cFrames = AudioMixBufFree(&pSink->MixBuf); + uint32_t cbToWrite = PDMAudioPropsFramesToBytes(&pSink->PCMProps, cFrames); + cbToWrite = RT_MIN(cbToWrite, cbBuf); + AudioMixBufWrite(&pSink->MixBuf, &pSink->Out.State, pvBuf, cbToWrite, 0 /*offDstFrame*/, cFrames, &cFrames); + Assert(cbToWrite == PDMAudioPropsFramesToBytes(&pSink->PCMProps, cFrames)); + AudioMixBufCommit(&pSink->MixBuf, cFrames); + *pcbWritten = cbToWrite; + + /* Update the sink's last written time stamp. */ + pSink->tsLastReadWrittenNs = RTTimeNanoTS(); + + Log3Func(("[%s] cbBuf=%#x -> cbWritten=%#x\n", pSink->pszName, cbBuf, cbToWrite)); +} + + +/** + * Transfer data from the device's DMA buffer and into the sink. + * + * The caller is already holding the mixer sink's critical section, either by + * way of being the AIO thread doing update jobs or by explicit locking calls. + * + * @returns The new stream offset. + * @param pSink The mixer sink to transfer samples to. + * @param pCircBuf The internal DMA buffer to move samples from. + * @param offStream The stream current offset (logging, dtrace, return). + * @param idStream Device specific audio stream identifier (logging, dtrace). + * @param pDbgFile Debug file, NULL if disabled. + */ +uint64_t AudioMixerSinkTransferFromCircBuf(PAUDMIXSINK pSink, PRTCIRCBUF pCircBuf, uint64_t offStream, + uint32_t idStream, PAUDIOHLPFILE pDbgFile) +{ + /* + * Sanity. + */ + AssertReturn(pSink, offStream); + Assert(pSink->uMagic == AUDMIXSINK_MAGIC); + AssertReturn(pCircBuf, offStream); + Assert(RTCritSectIsOwner(&pSink->CritSect)); + Assert(pSink->enmDir == PDMAUDIODIR_OUT); + RT_NOREF(idStream); + + /* + * Figure how much that we can push down. + */ + uint32_t const cbSinkWritable = AudioMixerSinkGetWritable(pSink); + uint32_t const cbCircBufReadable = (uint32_t)RTCircBufUsed(pCircBuf); + uint32_t cbToTransfer = RT_MIN(cbCircBufReadable, cbSinkWritable); + /* Make sure that we always align the number of bytes when reading to the stream's PCM properties. */ + uint32_t const cbToTransfer2 = cbToTransfer = PDMAudioPropsFloorBytesToFrame(&pSink->PCMProps, cbToTransfer); + + Log3Func(("idStream=%u: cbSinkWritable=%#RX32 cbCircBufReadable=%#RX32 -> cbToTransfer=%#RX32 @%#RX64\n", + idStream, cbSinkWritable, cbCircBufReadable, cbToTransfer, offStream)); + AssertMsg(!(pSink->fStatus & AUDMIXSINK_STS_DRAINING) || cbCircBufReadable == pSink->cbDmaLeftToDrain, + ("cbCircBufReadable=%#x cbDmaLeftToDrain=%#x\n", cbCircBufReadable, pSink->cbDmaLeftToDrain)); + + /* + * Do the pushing. + */ + while (cbToTransfer > 0) + { + void /*const*/ *pvSrcBuf; + size_t cbSrcBuf; + RTCircBufAcquireReadBlock(pCircBuf, cbToTransfer, &pvSrcBuf, &cbSrcBuf); + + uint32_t cbWritten = 0; + audioMixerSinkWrite(pSink, pvSrcBuf, (uint32_t)cbSrcBuf, &cbWritten); + Assert(cbWritten <= cbSrcBuf); + + Log2Func(("idStream=%u: %#RX32/%#zx bytes read @%#RX64\n", idStream, cbWritten, cbSrcBuf, offStream)); +#ifdef VBOX_WITH_DTRACE + VBOXDD_AUDIO_MIXER_SINK_AIO_OUT(idStream, cbWritten, offStream); +#endif + offStream += cbWritten; + + if (!pDbgFile) + { /* likely */ } + else + AudioHlpFileWrite(pDbgFile, pvSrcBuf, cbSrcBuf); + + + RTCircBufReleaseReadBlock(pCircBuf, cbWritten); + + /* advance */ + cbToTransfer -= cbWritten; + } + + /* + * Advance drain status. + */ + if (!(pSink->fStatus & AUDMIXSINK_STS_DRAINING)) + { /* likely for most of the playback time ... */ } + else if (!(pSink->fStatus & AUDMIXSINK_STS_DRAINED_DMA)) + { + if (cbToTransfer2 >= pSink->cbDmaLeftToDrain) + { + Assert(cbToTransfer2 == pSink->cbDmaLeftToDrain); + Log3Func(("idStream=%u/'%s': Setting AUDMIXSINK_STS_DRAINED_DMA.\n", idStream, pSink->pszName)); + pSink->cbDmaLeftToDrain = 0; + pSink->fStatus |= AUDMIXSINK_STS_DRAINED_DMA; + } + else + { + pSink->cbDmaLeftToDrain -= cbToTransfer2; + Log3Func(("idStream=%u/'%s': still %#x bytes left in the DMA buffer\n", + idStream, pSink->pszName, pSink->cbDmaLeftToDrain)); + } + } + else + Assert(cbToTransfer2 == 0); + + return offStream; +} + + +/** + * Transfer data to the device's DMA buffer from the sink. + * + * The caller is already holding the mixer sink's critical section, either by + * way of being the AIO thread doing update jobs or by explicit locking calls. + * + * @returns The new stream offset. + * @param pSink The mixer sink to transfer samples from. + * @param pCircBuf The internal DMA buffer to move samples to. + * @param offStream The stream current offset (logging, dtrace, return). + * @param idStream Device specific audio stream identifier (logging, dtrace). + * @param pDbgFile Debug file, NULL if disabled. + */ +uint64_t AudioMixerSinkTransferToCircBuf(PAUDMIXSINK pSink, PRTCIRCBUF pCircBuf, uint64_t offStream, + uint32_t idStream, PAUDIOHLPFILE pDbgFile) +{ + /* + * Sanity. + */ + AssertReturn(pSink, offStream); + Assert(pSink->uMagic == AUDMIXSINK_MAGIC); + AssertReturn(pCircBuf, offStream); + Assert(RTCritSectIsOwner(&pSink->CritSect)); + + /* + * Figure out how much we can transfer. + */ + const uint32_t cbSinkReadable = AudioMixerSinkGetReadable(pSink); + const uint32_t cbCircBufWritable = (uint32_t)RTCircBufFree(pCircBuf); + uint32_t cbToTransfer = RT_MIN(cbCircBufWritable, cbSinkReadable); + uint32_t cFramesToTransfer = PDMAudioPropsBytesToFrames(&pSink->PCMProps, cbToTransfer); + cbToTransfer = PDMAudioPropsFramesToBytes(&pSink->PCMProps, cFramesToTransfer); + + Log3Func(("idStream=%u: cbSinkReadable=%#RX32 cbCircBufWritable=%#RX32 -> cbToTransfer=%#RX32 (%RU32 frames) @%#RX64\n", + idStream, cbSinkReadable, cbCircBufWritable, cbToTransfer, cFramesToTransfer, offStream)); + RT_NOREF(idStream); + + /** @todo should we throttle (read less) this if we're far ahead? */ + + /* + * Copy loop. + */ + while (cbToTransfer > 0) + { +/** @todo We should be able to read straight into the circular buffer here + * as it should have a frame aligned size. */ + + /* Read a chunk of data. */ + uint8_t abBuf[4096]; + uint32_t cbRead = 0; + uint32_t cFramesRead = 0; + AudioMixBufPeek(&pSink->MixBuf, 0, cFramesToTransfer, &cFramesRead, + &pSink->In.State, abBuf, RT_MIN(cbToTransfer, sizeof(abBuf)), &cbRead); + AssertBreak(cFramesRead > 0); + Assert(cbRead > 0); + + cFramesToTransfer -= cFramesRead; + AudioMixBufAdvance(&pSink->MixBuf, cFramesRead); + + /* Write it to the internal DMA buffer. */ + uint32_t off = 0; + while (off < cbRead) + { + void *pvDstBuf; + size_t cbDstBuf; + RTCircBufAcquireWriteBlock(pCircBuf, cbRead - off, &pvDstBuf, &cbDstBuf); + + memcpy(pvDstBuf, &abBuf[off], cbDstBuf); + +#ifdef VBOX_WITH_DTRACE + VBOXDD_AUDIO_MIXER_SINK_AIO_IN(idStream, (uint32_t)cbDstBuf, offStream); +#endif + offStream += cbDstBuf; + + RTCircBufReleaseWriteBlock(pCircBuf, cbDstBuf); + + off += (uint32_t)cbDstBuf; + } + Assert(off == cbRead); + + /* Write to debug file? */ + if (RT_LIKELY(!pDbgFile)) + { /* likely */ } + else + AudioHlpFileWrite(pDbgFile, abBuf, cbRead); + + /* Advance. */ + Assert(cbRead <= cbToTransfer); + cbToTransfer -= cbRead; + } + + return offStream; +} + + +/** + * Signals the AIO thread to perform updates. + * + * @returns VBox status code. + * @param pSink The mixer sink which AIO thread needs to do chores. + */ +int AudioMixerSinkSignalUpdateJob(PAUDMIXSINK pSink) +{ + AssertPtrReturn(pSink, VERR_INVALID_POINTER); + Assert(pSink->uMagic == AUDMIXSINK_MAGIC); + return RTSemEventSignal(pSink->AIO.hEvent); +} + + +/** + * Locks the mixer sink for purposes of serializing with the AIO thread. + * + * @returns VBox status code. + * @param pSink The mixer sink to lock. + */ +int AudioMixerSinkLock(PAUDMIXSINK pSink) +{ + AssertPtrReturn(pSink, VERR_INVALID_POINTER); + Assert(pSink->uMagic == AUDMIXSINK_MAGIC); + return RTCritSectEnter(&pSink->CritSect); +} + + +/** + * Try to lock the mixer sink for purposes of serializing with the AIO thread. + * + * @returns VBox status code. + * @param pSink The mixer sink to lock. + */ +int AudioMixerSinkTryLock(PAUDMIXSINK pSink) +{ + AssertPtrReturn(pSink, VERR_INVALID_POINTER); + Assert(pSink->uMagic == AUDMIXSINK_MAGIC); + return RTCritSectTryEnter(&pSink->CritSect); +} + + +/** + * Unlocks the sink. + * + * @returns VBox status code. + * @param pSink The mixer sink to unlock. + */ +int AudioMixerSinkUnlock(PAUDMIXSINK pSink) +{ + AssertPtrReturn(pSink, VERR_INVALID_POINTER); + return RTCritSectLeave(&pSink->CritSect); +} + + +/** + * Creates an audio mixer stream. + * + * @returns VBox status code. + * @param pSink Sink to use for creating the stream. + * @param pConn Audio connector interface to use. + * @param pCfg Audio stream configuration to use. This may be modified + * in some unspecified way (see + * PDMIAUDIOCONNECTOR::pfnStreamCreate). + * @param pDevIns The device instance to register statistics with. + * @param ppStream Pointer which receives the newly created audio stream. + */ +int AudioMixerSinkCreateStream(PAUDMIXSINK pSink, PPDMIAUDIOCONNECTOR pConn, PCPDMAUDIOSTREAMCFG pCfg, + PPDMDEVINS pDevIns, PAUDMIXSTREAM *ppStream) +{ + AssertPtrReturn(pSink, VERR_INVALID_POINTER); + Assert(pSink->uMagic == AUDMIXSINK_MAGIC); + AssertPtrReturn(pConn, VERR_INVALID_POINTER); + AssertPtrReturn(pCfg, VERR_INVALID_POINTER); + AssertPtrNullReturn(ppStream, VERR_INVALID_POINTER); + Assert(pSink->AIO.pDevIns == pDevIns); RT_NOREF(pDevIns); /* we'll probably be adding more statistics */ + AssertReturn(pCfg->enmDir == pSink->enmDir, VERR_MISMATCH); + + /* + * Check status and get the host driver config. + */ + if (pConn->pfnGetStatus(pConn, PDMAUDIODIR_DUPLEX) == PDMAUDIOBACKENDSTS_NOT_ATTACHED) + return VERR_AUDIO_BACKEND_NOT_ATTACHED; + + PDMAUDIOBACKENDCFG BackendCfg; + int rc = pConn->pfnGetConfig(pConn, &BackendCfg); + AssertRCReturn(rc, rc); + + /* + * Allocate the instance. + */ + PAUDMIXSTREAM pMixStream = (PAUDMIXSTREAM)RTMemAllocZ(sizeof(AUDMIXSTREAM)); + AssertReturn(pMixStream, VERR_NO_MEMORY); + + /* Assign the backend's name to the mixer stream's name for easier identification in the (release) log. */ + pMixStream->pszName = RTStrAPrintf2("[%s] %s", pCfg->szName, BackendCfg.szName); + pMixStream->pszStatPrefix = RTStrAPrintf2("MixerSink-%s/%s/", pSink->pszName, BackendCfg.szName); + if (pMixStream->pszName && pMixStream->pszStatPrefix) + { + rc = RTCritSectInit(&pMixStream->CritSect); + if (RT_SUCCESS(rc)) + { + /* + * Lock the sink so we can safely get it's properties and call + * down into the audio driver to create that end of the stream. + */ + rc = RTCritSectEnter(&pSink->CritSect); + AssertRC(rc); + if (RT_SUCCESS(rc)) + { + LogFlowFunc(("[%s] (enmDir=%ld, %u bits, %RU8 channels, %RU32Hz)\n", pSink->pszName, pCfg->enmDir, + PDMAudioPropsSampleBits(&pCfg->Props), PDMAudioPropsChannels(&pCfg->Props), pCfg->Props.uHz)); + + /* + * Initialize the host-side configuration for the stream to be created, + * this is the sink format & direction with the src/dir, layout, name + * and device specific config copied from the guest side config (pCfg). + * We disregard any Backend settings here. + * + * (Note! pfnStreamCreate used to get both CfgHost and pCfg (aka pCfgGuest) + * passed in, but that became unnecessary with DrvAudio stoppping + * mixing. The mixing is done here and we bridge guest & host configs.) + */ + AssertMsg(AudioHlpPcmPropsAreValidAndSupported(&pSink->PCMProps), + ("%s: Does not (yet) have a (valid and supported) format set when it must\n", pSink->pszName)); + + PDMAUDIOSTREAMCFG CfgHost; + rc = PDMAudioStrmCfgInitWithProps(&CfgHost, &pSink->PCMProps); + AssertRC(rc); /* cannot fail */ + CfgHost.enmDir = pSink->enmDir; + CfgHost.enmPath = pCfg->enmPath; + CfgHost.Device = pCfg->Device; + RTStrCopy(CfgHost.szName, sizeof(CfgHost.szName), pCfg->szName); + + /* + * Create the stream. + * + * Output streams are not using any mixing buffers in DrvAudio. This will + * become the norm after we move the input mixing here and convert DevSB16 + * to use this mixer code too. + */ + PPDMAUDIOSTREAM pStream; + rc = pConn->pfnStreamCreate(pConn, 0 /*fFlags*/, &CfgHost, &pStream); + if (RT_SUCCESS(rc)) + { + pMixStream->cFramesBackendBuffer = pStream->Cfg.Backend.cFramesBufferSize; + + /* Set up the mixing buffer conversion state. */ + if (pSink->enmDir == PDMAUDIODIR_IN) + rc = AudioMixBufInitWriteState(&pSink->MixBuf, &pMixStream->WriteState, &pStream->Cfg.Props); + else + rc = AudioMixBufInitPeekState(&pSink->MixBuf, &pMixStream->PeekState, &pStream->Cfg.Props); + if (RT_SUCCESS(rc)) + { + /* Save the audio stream pointer to this mixing stream. */ + pMixStream->pStream = pStream; + + /* Increase the stream's reference count to let others know + * we're relying on it to be around now. */ + pConn->pfnStreamRetain(pConn, pStream); + pMixStream->pConn = pConn; + pMixStream->uMagic = AUDMIXSTREAM_MAGIC; + + RTCritSectLeave(&pSink->CritSect); + + if (ppStream) + *ppStream = pMixStream; + return VINF_SUCCESS; + } + + rc = pConn->pfnStreamDestroy(pConn, pStream, true /*fImmediate*/); + } + + /* + * Failed. Tear down the stream. + */ + int rc2 = RTCritSectLeave(&pSink->CritSect); + AssertRC(rc2); + } + RTCritSectDelete(&pMixStream->CritSect); + } + } + else + rc = VERR_NO_STR_MEMORY; + + RTStrFree(pMixStream->pszStatPrefix); + pMixStream->pszStatPrefix = NULL; + RTStrFree(pMixStream->pszName); + pMixStream->pszName = NULL; + RTMemFree(pMixStream); + return rc; +} + + +/** + * Adds an audio stream to a specific audio sink. + * + * @returns VBox status code. + * @param pSink Sink to add audio stream to. + * @param pStream Stream to add. + */ +int AudioMixerSinkAddStream(PAUDMIXSINK pSink, PAUDMIXSTREAM pStream) +{ + LogFlowFuncEnter(); + AssertPtrReturn(pSink, VERR_INVALID_POINTER); + Assert(pSink->uMagic == AUDMIXSINK_MAGIC); + AssertPtrReturn(pStream, VERR_INVALID_POINTER); + Assert(pStream->uMagic == AUDMIXSTREAM_MAGIC); + AssertPtrReturn(pStream->pConn, VERR_AUDIO_STREAM_NOT_READY); + AssertReturn(pStream->pSink == NULL, VERR_ALREADY_EXISTS); + + int rc = RTCritSectEnter(&pSink->CritSect); + AssertRCReturn(rc, rc); + + AssertLogRelMsgReturnStmt(pSink->cStreams < UINT8_MAX, ("too many streams!\n"), RTCritSectLeave(&pSink->CritSect), + VERR_TOO_MANY_OPEN_FILES); + + /* + * If the sink is running and not in pending disable mode, make sure that + * the added stream also is enabled. Ignore any failure to enable it. + */ + if ( (pSink->fStatus & AUDMIXSINK_STS_RUNNING) + && !(pSink->fStatus & AUDMIXSINK_STS_DRAINING)) + { + audioMixerStreamCtlInternal(pStream, PDMAUDIOSTREAMCMD_ENABLE); + } + + /* Save pointer to sink the stream is attached to. */ + pStream->pSink = pSink; + + /* Append stream to sink's list. */ + RTListAppend(&pSink->lstStreams, &pStream->Node); + pSink->cStreams++; + + LogFlowFunc(("[%s] cStreams=%RU8, rc=%Rrc\n", pSink->pszName, pSink->cStreams, rc)); + RTCritSectLeave(&pSink->CritSect); + return rc; +} + + +/** + * Removes a mixer stream from a mixer sink, internal version. + * + * @returns VBox status code. + * @param pSink The mixer sink (valid). + * @param pStream The stream to remove (valid). + * + * @note Caller must own the sink lock. + */ +static int audioMixerSinkRemoveStreamInternal(PAUDMIXSINK pSink, PAUDMIXSTREAM pStream) +{ + AssertPtr(pSink); + AssertPtr(pStream); + AssertMsgReturn(pStream->pSink == pSink, ("Stream '%s' is not part of sink '%s'\n", + pStream->pszName, pSink->pszName), VERR_NOT_FOUND); + Assert(RTCritSectIsOwner(&pSink->CritSect)); + LogFlowFunc(("[%s] (Stream = %s), cStreams=%RU8\n", pSink->pszName, pStream->pStream->Cfg.szName, pSink->cStreams)); + + /* + * Remove stream from sink, update the count and set the pSink member to NULL. + */ + RTListNodeRemove(&pStream->Node); + + Assert(pSink->cStreams > 0); + pSink->cStreams--; + + pStream->pSink = NULL; + + return VINF_SUCCESS; +} + + +/** + * Removes a mixer stream from a mixer sink. + * + * @param pSink The mixer sink. + * @param pStream The stream to remove. + */ +void AudioMixerSinkRemoveStream(PAUDMIXSINK pSink, PAUDMIXSTREAM pStream) +{ + AssertPtrReturnVoid(pSink); + AssertReturnVoid(pSink->uMagic == AUDMIXSINK_MAGIC); + AssertPtrReturnVoid(pStream); + AssertReturnVoid(pStream->uMagic == AUDMIXSTREAM_MAGIC); + + int rc = RTCritSectEnter(&pSink->CritSect); + AssertRCReturnVoid(rc); + + audioMixerSinkRemoveStreamInternal(pSink, pStream); + + RTCritSectLeave(&pSink->CritSect); +} + + +/** + * Removes all streams from a given sink. + * + * @param pSink The mixer sink. NULL is ignored. + */ +void AudioMixerSinkRemoveAllStreams(PAUDMIXSINK pSink) +{ + if (!pSink) + return; + AssertReturnVoid(pSink->uMagic == AUDMIXSINK_MAGIC); + + int rc = RTCritSectEnter(&pSink->CritSect); + AssertRCReturnVoid(rc); + + LogFunc(("%s\n", pSink->pszName)); + + PAUDMIXSTREAM pStream, pStreamNext; + RTListForEachSafe(&pSink->lstStreams, pStream, pStreamNext, AUDMIXSTREAM, Node) + { + audioMixerSinkRemoveStreamInternal(pSink, pStream); + } + AssertStmt(pSink->cStreams == 0, pSink->cStreams = 0); + + RTCritSectLeave(&pSink->CritSect); +} + + + +/********************************************************************************************************************************* + * Mixer Stream implementation. + ********************************************************************************************************************************/ + +/** + * Controls a mixer stream, internal version. + * + * @returns VBox status code (generally ignored). + * @param pMixStream Mixer stream to control. + * @param enmCmd Mixer stream command to use. + */ +static int audioMixerStreamCtlInternal(PAUDMIXSTREAM pMixStream, PDMAUDIOSTREAMCMD enmCmd) +{ + Assert(pMixStream->uMagic == AUDMIXSTREAM_MAGIC); + AssertPtrReturn(pMixStream->pConn, VERR_AUDIO_STREAM_NOT_READY); + AssertPtrReturn(pMixStream->pStream, VERR_AUDIO_STREAM_NOT_READY); + + int rc = pMixStream->pConn->pfnStreamControl(pMixStream->pConn, pMixStream->pStream, enmCmd); + + LogFlowFunc(("[%s] enmCmd=%d, rc=%Rrc\n", pMixStream->pszName, enmCmd, rc)); + + return rc; +} + +/** + * Updates a mixer stream's internal status. + * + * This may perform a stream re-init if the driver requests it, in which case + * this may take a little while longer than usual... + * + * @returns VBox status code. + * @param pMixStream Mixer stream to to update internal status for. + */ +static int audioMixerStreamUpdateStatus(PAUDMIXSTREAM pMixStream) +{ + Assert(pMixStream->uMagic == AUDMIXSTREAM_MAGIC); + + /* + * Reset the mixer status to start with. + */ + pMixStream->fStatus = AUDMIXSTREAM_STATUS_NONE; + + PPDMIAUDIOCONNECTOR const pConn = pMixStream->pConn; + if (pConn) /* Audio connector available? */ + { + PPDMAUDIOSTREAM const pStream = pMixStream->pStream; + + /* + * Get the stream status. + * Do re-init if needed and fetch the status again afterwards. + */ + PDMAUDIOSTREAMSTATE enmState = pConn->pfnStreamGetState(pConn, pStream); + if (enmState != PDMAUDIOSTREAMSTATE_NEED_REINIT) + { /* likely */ } + else + { + LogFunc(("[%s] needs re-init...\n", pMixStream->pszName)); + int rc = pConn->pfnStreamReInit(pConn, pStream); + enmState = pConn->pfnStreamGetState(pConn, pStream); + LogFunc(("[%s] re-init returns %Rrc and %s.\n", pMixStream->pszName, rc, PDMAudioStreamStateGetName(enmState))); + + PAUDMIXSINK const pSink = pMixStream->pSink; + AssertPtr(pSink); + if (pSink->enmDir == PDMAUDIODIR_OUT) + { + rc = AudioMixBufInitPeekState(&pSink->MixBuf, &pMixStream->PeekState, &pStream->Cfg.Props); + /** @todo we need to remember this, don't we? */ + AssertLogRelRCReturn(rc, VINF_SUCCESS); + } + else + { + rc = AudioMixBufInitWriteState(&pSink->MixBuf, &pMixStream->WriteState, &pStream->Cfg.Props); + /** @todo we need to remember this, don't we? */ + AssertLogRelRCReturn(rc, VINF_SUCCESS); + } + } + + /* + * Translate the status to mixer speak. + */ + AssertMsg(enmState > PDMAUDIOSTREAMSTATE_INVALID && enmState < PDMAUDIOSTREAMSTATE_END, ("%d\n", enmState)); + switch (enmState) + { + case PDMAUDIOSTREAMSTATE_NOT_WORKING: + case PDMAUDIOSTREAMSTATE_NEED_REINIT: + case PDMAUDIOSTREAMSTATE_INACTIVE: + pMixStream->fStatus = AUDMIXSTREAM_STATUS_NONE; + break; + case PDMAUDIOSTREAMSTATE_ENABLED: + pMixStream->fStatus = AUDMIXSTREAM_STATUS_ENABLED; + break; + case PDMAUDIOSTREAMSTATE_ENABLED_READABLE: + Assert(pMixStream->pSink->enmDir == PDMAUDIODIR_IN); + pMixStream->fStatus = AUDMIXSTREAM_STATUS_ENABLED | AUDMIXSTREAM_STATUS_CAN_READ; + break; + case PDMAUDIOSTREAMSTATE_ENABLED_WRITABLE: + Assert(pMixStream->pSink->enmDir == PDMAUDIODIR_OUT); + pMixStream->fStatus = AUDMIXSTREAM_STATUS_ENABLED | AUDMIXSTREAM_STATUS_CAN_WRITE; + break; + /* no default */ + case PDMAUDIOSTREAMSTATE_INVALID: + case PDMAUDIOSTREAMSTATE_END: + case PDMAUDIOSTREAMSTATE_32BIT_HACK: + break; + } + } + + LogFlowFunc(("[%s] -> 0x%x\n", pMixStream->pszName, pMixStream->fStatus)); + return VINF_SUCCESS; +} + + +/** + * Destroys & frees a mixer stream, internal version. + * + * Worker for audioMixerSinkDestroyInternal and AudioMixerStreamDestroy. + * + * @param pMixStream Mixer stream to destroy. + * @param pDevIns The device instance the statistics are registered with. + * @param fImmediate How to handle still draining streams, whether to let + * them complete (@c false) or destroy them immediately (@c + * true). + */ +static void audioMixerStreamDestroyInternal(PAUDMIXSTREAM pMixStream, PPDMDEVINS pDevIns, bool fImmediate) +{ + AssertPtr(pMixStream); + LogFunc(("%s\n", pMixStream->pszName)); + Assert(pMixStream->uMagic == AUDMIXSTREAM_MAGIC); + + /* + * Invalidate it. + */ + pMixStream->uMagic = AUDMIXSTREAM_MAGIC_DEAD; + + /* + * Destroy the driver stream (if any). + */ + if (pMixStream->pConn) /* Stream has a connector interface present? */ + { + if (pMixStream->pStream) + { + pMixStream->pConn->pfnStreamRelease(pMixStream->pConn, pMixStream->pStream); + pMixStream->pConn->pfnStreamDestroy(pMixStream->pConn, pMixStream->pStream, fImmediate); + + pMixStream->pStream = NULL; + } + + pMixStream->pConn = NULL; + } + + /* + * Stats. Doing it by prefix is soo much faster than individually, btw. + */ + if (pMixStream->pszStatPrefix) + { + PDMDevHlpSTAMDeregisterByPrefix(pDevIns, pMixStream->pszStatPrefix); + RTStrFree(pMixStream->pszStatPrefix); + pMixStream->pszStatPrefix = NULL; + } + + /* + * Delete the critsect and free the memory. + */ + int rc2 = RTCritSectDelete(&pMixStream->CritSect); + AssertRC(rc2); + + RTStrFree(pMixStream->pszName); + pMixStream->pszName = NULL; + + RTMemFree(pMixStream); +} + + +/** + * Destroys a mixer stream. + * + * @param pMixStream Mixer stream to destroy. + * @param pDevIns The device instance statistics are registered with. + * @param fImmediate How to handle still draining streams, whether to let + * them complete (@c false) or destroy them immediately (@c + * true). + */ +void AudioMixerStreamDestroy(PAUDMIXSTREAM pMixStream, PPDMDEVINS pDevIns, bool fImmediate) +{ + if (!pMixStream) + return; + AssertReturnVoid(pMixStream->uMagic == AUDMIXSTREAM_MAGIC); + LogFunc(("%s\n", pMixStream->pszName)); + + /* + * Serializing paranoia. + */ + int rc = RTCritSectEnter(&pMixStream->CritSect); + AssertRCReturnVoid(rc); + RTCritSectLeave(&pMixStream->CritSect); + + /* + * Unlink from sink if associated with one. + */ + PAUDMIXSINK pSink = pMixStream->pSink; + if ( RT_VALID_PTR(pSink) + && pSink->uMagic == AUDMIXSINK_MAGIC) + { + RTCritSectEnter(&pSink->CritSect); + audioMixerSinkRemoveStreamInternal(pMixStream->pSink, pMixStream); + RTCritSectLeave(&pSink->CritSect); + } + else if (pSink) + AssertFailed(); + + /* + * Do the actual stream destruction. + */ + audioMixerStreamDestroyInternal(pMixStream, pDevIns, fImmediate); + LogFlowFunc(("returns\n")); +} + |